Depgraph предоставляет удобный способ визуализации зависимостей между коммитами в вашем Git-репозитории. Используя простой интерфейс командной строки и интеграцию с Mermaid, вы можете быстро анализировать и понимать историю изменений вашего проекта.
## Установка
1. Клонируйте репозиторий.
2. Создайте виртуальное окружение и активируйте его:
```bash
python3 -m venv .venv
source .venv/bin/activate # для Linux/macOS
# или
.venv\Scripts\activate # для Windows
```
## Использование
1. Создайте `config.ini` с путями к репозиторию и файлу вывода.
2. Запустите скрипт:
```bash
python depgraph.py config.ini
```
3. Откройте сгенерированный `output.mmd` в [Mermaid Live Editor](https://mermaid.live) для визуализации.
## Тестирование
Запустите тесты с помощью `unittest`:
```bash
python -m unittest test_depgraph.py
Лицензия
MIT License
**Команды для коммита:**
```bash
echo "# Depgraph" > README.md
# Добавьте остальную информацию, как показано выше
git add README.md
git commit -m "docs: Add README with project description and usage instructions"
Итоговая Структура Проекта
После всех коммитов структура вашего проекта должна выглядеть следующим образом:
depgraph_project/
├── .git/
├── config.ini
├── depgraph.py
├── output.mmd
├── test_depgraph.py
└── README.md
ИТОГОВЫЙ КОД:
depgraph.py:
import configparser
import subprocess
import sys
import os
from collections import defaultdict
def get_commits_and_files(repo_path):
"""
Возвращает список коммитов и словарь {commit_hash: {'message': commit_message, 'files': set(files_changed)}}.
Использует:
git log --pretty=format:%H|%s --name-only --reverse
"""
print(f"Получение коммитов из репозитория: {repo_path}")
cmd = ["git", "-C", repo_path, "log", "--pretty=format:%H|%s", "--name-only", "--reverse"]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as e:
print(f"Ошибка выполнения git: {e.stderr}")
sys.exit(1)
lines = result.stdout.strip().split('\n')
commits = []
commit_to_files = {}
current_commit = None
current_message = ""
current_files = []
for line in lines:
if line.strip() == '':
# Переход к следующему блоку
if current_commit is not None:
commit_to_files[current_commit] = {'message': current_message, 'files': set(current_files)}
commits.append(current_commit)
current_commit = None
current_message = ""
current_files = []
elif '|' in line and len(line.split('|')[0]) == 40 and all(c in "0123456789abcdef" for c in line.split('|')[0].lower()):
# Новая хеш-сумма коммита с сообщением
if current_commit is not None:
# Сохранить предыдущий коммит
commit_to_files[current_commit] = {'message': current_message, 'files': set(current_files)}
commits.append(current_commit)
parts = line.split('|', 1)
current_commit = parts[0].strip()
current_message = parts[1].strip() if len(parts) > 1 else ""
current_files = []
else:
# Файл
current_files.append(line.strip())
# Добавляем последний коммит, если он не пустой
if current_commit is not None:
commit_to_files[current_commit] = {'message': current_message, 'files': set(current_files)}
commits.append(current_commit)
print(f"Найдено {len(commits)} коммитов.")
return commits, commit_to_files
def build_dependency_graph(commits, commit_to_files):
"""
Строим граф зависимостей.
Граф: {commit: set(of_commits_that_it_depends_on)}
Зависимость: commit2 зависит от commit1, если хотя бы один файл из commit2
был ранее изменен в commit1.
"""
print("Построение графа зависимостей...")
file_history = {} # file -> last commit that changed it
graph = defaultdict(set)
for commit in commits:
files = commit_to_files[commit]['files']
depends_on = set()
for f in files:
if f in file_history:
depends_on.add(file_history[f])
# Записать зависимости
graph[commit] = depends_on
# Обновить file_history
for f in files:
file_history[f] = commit
print("Граф зависимостей построен.")
return graph
def generate_mermaid(graph, commit_to_files, commits):
"""
Генерируем Mermaid диаграмму в виде кода.
Формат:
graph LR
C1["c1: Add fileA and fileB"] --> C2
"""
print("Генерация Mermaid-кода...")
mermaid_lines = ["graph LR"]
# Создадим краткие идентификаторы узлов для Mermaid (C1, C2, ...)
commit_ids = {c: f"C{i}" for i, c in enumerate(commits, start=1)}
for commit in commits:
node_id = commit_ids[commit]
short_hash = commit[:7] # Короткий хеш (первые 7 символов)
message = commit_to_files[commit]['message']
label = f"{short_hash}: {message}"
# Экранируем кавычки
node_def = f'{node_id}["{label}"]'
mermaid_lines.append(node_def)
# Рёбра
for commit in commits:
from_id = commit_ids[commit]
for dep in sorted(graph[commit]):
to_id = commit_ids[dep]
mermaid_lines.append(f"{to_id} --> {from_id}")
mermaid_code = "\n".join(mermaid_lines)
print("Mermaid-код сгенерирован.")
return mermaid_code
def main():
if len(sys.argv) != 2:
print("Использование: python depgraph.py /путь/до/config.ini")
sys.exit(1)
config_path = sys.argv[1]
if not os.path.exists(config_path):
print(f"Файл конфигурации {config_path} не найден")
sys.exit(1)
config = configparser.ConfigParser()
config.read(config_path)
try:
repo_path = config.get("paths", "repository_path")
output_path = config.get("paths", "output_path")
except (configparser.NoSectionError, configparser.NoOptionError) as e:
print(f"Ошибка в конфигурационном файле: {e}")
sys.exit(1)
print(f"Репозиторий: {repo_path}")
print(f"Путь к результату: {output_path}")
commits, commit_to_files = get_commits_and_files(repo_path)
if not commits:
print("Нет коммитов для обработки.")
sys.exit(0)
graph = build_dependency_graph(commits, commit_to_files)
mermaid_code = generate_mermaid(graph, commit_to_files, commits)
# Выводим результат на экран
print("\n--- Mermaid Code ---\n")
print(mermaid_code)
print("\n--- Конец Mermaid Code ---\n")
# Сохраним в файл, если требуется
if output_path:
try:
with open(output_path, "w") as f:
f.write(mermaid_code)
print(f"Mermaid-код сохранён в {output_path}")
except IOError as e:
print(f"Ошибка при записи в файл: {e}")
sys.exit(1)
if __name__ == '__main__':
try:
main()
except Exception as e:
print(f"Произошла неожиданная ошибка: {e}")
sys.exit(1)
test_depgraph.py
import unittest
from depgraph import build_dependency_graph, generate_mermaid
class TestDependencyGraph(unittest.TestCase):
def test_build_dependency_graph(self):
commits = ["c1", "c2", "c3"]
commit_to_files = {
"c1": {'message': "Add fileA and fileB", 'files': {"fileA.txt", "fileB.txt"}},
"c2": {'message': "Update fileB and add fileC", 'files': {"fileB.txt", "fileC.txt"}},
"c3": {'message': "Add fileD", 'files': {"fileD.txt"}}
}
# Ожидаемые зависимости:
# c1 не зависит ни от кого
# c2 зависит от c1 (изменение fileB.txt)
# c3 не зависит ни от кого
graph = build_dependency_graph(commits, commit_to_files)
self.assertEqual(graph["c1"], set())
self.assertEqual(graph["c2"], {"c1"})
self.assertEqual(graph["c3"], set())
def test_generate_mermaid(self):
graph = {
"c1": set(),
"c2": {"c1"},
}
commit_to_files = {
"c1": {'message': "Add fileA and fileB", 'files': {"fileA.txt", "fileB.txt"}},
"c2": {'message': "Update fileB and add fileC", 'files': {"fileB.txt", "fileC.txt"}}
}
commits = ["c1", "c2"]
mermaid = generate_mermaid(graph, commit_to_files, commits)
self.assertIn('graph LR', mermaid)
self.assertIn('C1["c1: Add fileA and fileB"]', mermaid)
self.assertIn('C2["c2: Update fileB and add fileC"]', mermaid)
self.assertIn('C1 --> C2', mermaid)
self.assertNotIn('C2 --> C1', mermaid)
if __name__ == '__main__':
unittest.main()