Загрузка данных


#!/usr/bin/env python3
"""
Скрипт сбора статистики по коммитам в Git-репозитории.

Собирает по указанным авторам:
- количество коммитов
- частоту (коммиты в неделю / средний интервал)
- объём затронутого кода в .py файлах (добавлено / удалено строк)

Период по умолчанию: с 2026-03-01 по сегодня.
Авторы: Владимиров Никита Михайлович, Ефимов Александр Сергеевич, OrlovaIU

Запуск:
    python git_python_stats.py [путь_к_репозиторию]
    (по умолчанию — текущая директория)
"""

import subprocess
import sys
import datetime
from collections import defaultdict
from pathlib import Path


AUTHORS = [
    "Владимиров Никита Михайлович",
    "Ефимов Александр Сергеевич",
    "OrlovaIU",
]

SINCE = "2026-03-01"
UNTIL = datetime.date.today().isoformat()  # сегодня


def run_git_log(repo_path: str) -> str:
    """Запускает git log и возвращает сырой вывод."""
    cmd = [
        "git", "-C", repo_path, "log",
        f"--since={SINCE}",
        f"--until={UNTIL}",
        "--pretty=format:%H|%an|%ae|%ad",
        "--date=short",
        "--numstat",
        "--", "*.py",
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
    if result.returncode != 0:
        print("Ошибка выполнения git log:", result.stderr, file=sys.stderr)
        sys.exit(1)
    return result.stdout


def parse_commits(raw: str) -> list[dict]:
    """Парсит вывод git log --numstat."""
    commits = []
    current = None

    for line in raw.splitlines():
        line = line.strip()
        if not line:
            continue

        # Заголовок коммита: hash|author|email|date
        if line.count("|") >= 3:
            parts = line.split("|", 3)
            if len(parts) >= 4:
                current = {
                    "hash": parts[0],
                    "author": parts[1],
                    "email": parts[2],
                    "date": parts[3],
                    "added": 0,
                    "deleted": 0,
                }
                commits.append(current)
            continue

        # Строка numstat: added\tdeleted\tfilename
        if current and "\t" in line:
            parts = line.split("\t")
            if len(parts) >= 2:
                try:
                    added = int(parts[0]) if parts[0] != "-" else 0
                    deleted = int(parts[1]) if parts[1] != "-" else 0
                    current["added"] += added
                    current["deleted"] += deleted
                except ValueError:
                    pass

    return commits


def matches_author(commit: dict, authors: list[str]) -> bool:
    """Проверяет, относится ли коммит к одному из интересующих авторов."""
    text = f"{commit['author']} {commit['email']}".lower()
    return any(a.lower() in text for a in authors)


def compute_frequency(dates: list[str]) -> dict:
    """Вычисляет простые метрики частоты по списку дат коммитов (YYYY-MM-DD)."""
    if not dates:
        return {"weeks": 0, "commits_per_week": 0.0, "avg_days_between": None}

    unique_dates = sorted(set(dates))
    start = datetime.date.fromisoformat(unique_dates[0])
    end = datetime.date.fromisoformat(unique_dates[-1])
    days = (end - start).days + 1
    weeks = max(days / 7.0, 1 / 7.0)

    # Средний интервал между коммитами (по всем датам, не только уникальным)
    if len(dates) > 1:
        sorted_all = sorted(datetime.date.fromisoformat(d) for d in dates)
        intervals = [(sorted_all[i] - sorted_all[i - 1]).days for i in range(1, len(sorted_all))]
        avg_interval = sum(intervals) / len(intervals) if intervals else None
    else:
        avg_interval = None

    return {
        "span_days": days,
        "weeks": round(weeks, 1),
        "commits_per_week": round(len(dates) / weeks, 2),
        "avg_days_between": round(avg_interval, 1) if avg_interval is not None else None,
        "active_days": len(unique_dates),
    }


def main():
    repo = sys.argv[1] if len(sys.argv) > 1 else "."
    repo_path = Path(repo).resolve()

    if not (repo_path / ".git").exists():
        print(f"Не найден git-репозиторий по пути: {repo_path}", file=sys.stderr)
        sys.exit(1)

    print(f"Анализ репозитория: {repo_path}")
    print(f"Период: {SINCE} — {UNTIL}")
    print(f"Авторы: {', '.join(AUTHORS)}")
    print("-" * 70)

    raw = run_git_log(str(repo_path))
    all_commits = parse_commits(raw)
    filtered = [c for c in all_commits if matches_author(c, AUTHORS)]

    # Группировка по реальному имени автора
    stats = defaultdict(lambda: {
        "commits": 0,
        "added": 0,
        "deleted": 0,
        "dates": [],
        "emails": set(),
    })

    for c in filtered:
        key = c["author"]
        stats[key]["commits"] += 1
        stats[key]["added"] += c["added"]
        stats[key]["deleted"] += c["deleted"]
        stats[key]["dates"].append(c["date"])
        stats[key]["emails"].add(c["email"])

    if not stats:
        print("Коммитов по указанным авторам за период не найдено.")
        return

    # Вывод
    total_commits = 0
    total_added = 0
    total_deleted = 0

    for author, data in sorted(stats.items(), key=lambda x: -x[1]["commits"]):
        freq = compute_frequency(data["dates"])
        net = data["added"] - data["deleted"]
        total_commits += data["commits"]
        total_added += data["added"]
        total_deleted += data["deleted"]

        print(f"\nАвтор: {author}")
        print(f"  Email(ы): {', '.join(sorted(data['emails']))}")
        print(f"  Коммитов: {data['commits']}")
        print(f"  Строк в .py: +{data['added']} / -{data['deleted']} (чистые: {net:+d})")
        print(f"  Частота: {freq['commits_per_week']} коммитов/неделю "
              f"(период активности {freq['span_days']} дн., {freq['active_days']} активных дней)")
        if freq["avg_days_between"] is not None:
            print(f"  Средний интервал между коммитами: {freq['avg_days_between']} дней")

    print("\n" + "=" * 70)
    print(f"ИТОГО по всем авторам:")
    print(f"  Коммитов: {total_commits}")
    print(f"  Строк в .py: +{total_added} / -{total_deleted} "
          f"(чистые: {total_added - total_deleted:+d})")


if __name__ == "__main__":
    main()