From 29e0fce2a58dca8588fcd1ebd2676a4d78c11de7 Mon Sep 17 00:00:00 2001 From: Milan <38429578+mloncarevich@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:51:57 +0100 Subject: [PATCH] chore: add CI workflow and ruff linting Co-authored-by: Cursor --- .github/workflows/ci.yml | 57 ++++++++++++++++++++++++ README.md | 4 +- gitstats/cli.py | 42 +++++++++--------- gitstats/parser.py | 96 ++++++++++++++++++++-------------------- pyproject.toml | 11 +++++ 5 files changed, 142 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f889480 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Test CLI runs + run: | + gitstats --version + gitstats --help + + - name: Test on a git repo + run: | + gitstats stats . + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install linters + run: | + python -m pip install --upgrade pip + pip install ruff + + - name: Run Ruff linter + run: ruff check . + + - name: Run Ruff formatter check + run: ruff format --check . diff --git a/README.md b/README.md index efa40bf..5f788d4 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,15 @@ > Beautiful git statistics in your terminal +[![CI](https://github.com/mloncarevich/gitstats/actions/workflows/ci.yml/badge.svg)](https://github.com/mloncarevich/gitstats/actions/workflows/ci.yml) [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## ✨ Features - 📈 Commit statistics at a glance -- 🗓️ Activity heatmap (coming soon) +- 🗓️ Activity heatmap by day of week +- ⏰ Peak coding hours analysis - 👥 Contributor insights - 🎨 Beautiful terminal output diff --git a/gitstats/cli.py b/gitstats/cli.py index 92896c7..27e23b2 100644 --- a/gitstats/cli.py +++ b/gitstats/cli.py @@ -42,25 +42,25 @@ def stats( ) -> None: """Show commit statistics for a git repository.""" from gitstats.parser import get_commit_stats - + console.print(f"\n[bold]📊 Git Statistics for:[/] [cyan]{path}[/]\n") - + stats = get_commit_stats(path) - + if not stats: console.print("[red]No commits found or not a git repository.[/]") raise typer.Exit(1) - + console.print(f"[bold]Total commits:[/] [green]{stats['total_commits']}[/]") console.print(f"[bold]Contributors:[/] [green]{stats['total_authors']}[/]") console.print(f"[bold]First commit:[/] [yellow]{stats['first_commit']}[/]") console.print(f"[bold]Latest commit:[/] [yellow]{stats['last_commit']}[/]") console.print() - + # Show author breakdown table if stats.get("author_stats"): _print_author_table(stats["author_stats"]) - + # Show activity heatmap _print_activity_heatmap(stats["commits"]) @@ -68,58 +68,60 @@ def stats( def _print_author_table(author_stats: list[dict]) -> None: """Print a table of author statistics.""" table = Table(title="👥 Commits by Author", show_header=True, header_style="bold cyan") - + table.add_column("Author", style="white", no_wrap=True) table.add_column("Commits", justify="right", style="green") table.add_column("Percentage", justify="right", style="yellow") table.add_column("", justify="left", style="blue") # Progress bar - + for stat in author_stats: bar_width = int(stat["percentage"] / 2) # Max 50 chars for 100% bar = "█" * bar_width - + table.add_row( stat["author"], str(stat["commits"]), f"{stat['percentage']:.1f}%", bar, ) - + console.print(table) console.print() def _print_activity_heatmap(commits: list[dict]) -> None: """Print activity heatmap by day and hour.""" - from gitstats.parser import get_weekly_activity, get_hourly_activity - + from gitstats.parser import get_hourly_activity, get_weekly_activity + # Weekly activity weekly = get_weekly_activity(commits) - + console.print("[bold]📅 Activity by Day of Week[/]\n") - + max_commits = max(d["commits"] for d in weekly) if weekly else 1 - + for day_stat in weekly: bar_width = int((day_stat["commits"] / max_commits) * 30) if max_commits else 0 bar = "█" * bar_width - + console.print( f" [cyan]{day_stat['day']}[/] │ [green]{bar:<30}[/] {day_stat['commits']:>3} commits" ) - + console.print() - + # Hourly activity (simplified - peak hours) hourly = get_hourly_activity(commits) peak_hours = sorted(hourly, key=lambda x: x["commits"], reverse=True)[:3] - + if peak_hours and peak_hours[0]["commits"] > 0: console.print("[bold]⏰ Peak Coding Hours[/]\n") for h in peak_hours: if h["commits"] > 0: hour_str = f"{h['hour']:02d}:00" - console.print(f" [yellow]{hour_str}[/] - {h['commits']} commits ({h['percentage']:.1f}%)") + console.print( + f" [yellow]{hour_str}[/] - {h['commits']} commits ({h['percentage']:.1f}%)" + ) console.print() diff --git a/gitstats/parser.py b/gitstats/parser.py index a1035b4..4f85c19 100644 --- a/gitstats/parser.py +++ b/gitstats/parser.py @@ -8,18 +8,18 @@ def get_commit_stats(repo_path: str = ".") -> dict | None: """ Parse git log and return commit statistics. - + Args: repo_path: Path to the git repository - + Returns: Dictionary with commit statistics or None if not a git repo """ path = Path(repo_path).resolve() - + if not (path / ".git").exists(): return None - + try: # Get all commits with author and date result = subprocess.run( @@ -31,15 +31,15 @@ def get_commit_stats(repo_path: str = ".") -> dict | None: ) except subprocess.CalledProcessError: return None - + lines = result.stdout.strip().split("\n") - + if not lines or lines[0] == "": return None - + commits = [] authors = set() - + for line in lines: if not line: continue @@ -47,22 +47,24 @@ def get_commit_stats(repo_path: str = ".") -> dict | None: if len(parts) >= 4: commit_hash, author_name, author_email, date_str = parts[:4] authors.add(author_name) - commits.append({ - "hash": commit_hash, - "author": author_name, - "email": author_email, - "date": datetime.fromisoformat(date_str), - }) - + commits.append( + { + "hash": commit_hash, + "author": author_name, + "email": author_email, + "date": datetime.fromisoformat(date_str), + } + ) + if not commits: return None - + # Sort by date commits.sort(key=lambda x: x["date"]) - + # Calculate author statistics author_stats = get_author_stats(commits) - + return { "total_commits": len(commits), "total_authors": len(authors), @@ -77,58 +79,58 @@ def get_commit_stats(repo_path: str = ".") -> dict | None: def get_author_stats(commits: list[dict]) -> list[dict]: """ Calculate commit statistics per author. - + Args: commits: List of commit dictionaries - + Returns: List of author statistics sorted by commit count (descending) """ from collections import Counter - + author_counts = Counter(commit["author"] for commit in commits) total = len(commits) - + stats = [] for author, count in author_counts.most_common(): percentage = (count / total) * 100 - stats.append({ - "author": author, - "commits": count, - "percentage": percentage, - }) - + stats.append( + { + "author": author, + "commits": count, + "percentage": percentage, + } + ) + return stats def get_activity_heatmap(commits: list[dict]) -> dict: """ Calculate commit activity by day of week and hour. - + Args: commits: List of commit dictionaries - + Returns: Dictionary with heatmap data (day -> hour -> count) """ from collections import defaultdict - + # Initialize heatmap: 7 days x 24 hours heatmap = defaultdict(lambda: defaultdict(int)) - + for commit in commits: date = commit["date"] day = date.weekday() # 0=Monday, 6=Sunday hour = date.hour heatmap[day][hour] += 1 - + # Find max for normalization - max_count = max( - count - for day_data in heatmap.values() - for count in day_data.values() - ) if heatmap else 1 - + max_count = ( + max(count for day_data in heatmap.values() for count in day_data.values()) if heatmap else 1 + ) + return { "data": dict(heatmap), "max_count": max_count, @@ -138,19 +140,19 @@ def get_activity_heatmap(commits: list[dict]) -> dict: def get_weekly_activity(commits: list[dict]) -> list[dict]: """ Calculate commit activity by day of week. - + Args: commits: List of commit dictionaries - + Returns: List of day statistics """ from collections import Counter - + days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] day_counts = Counter(commit["date"].weekday() for commit in commits) total = len(commits) - + return [ { "day": days[i], @@ -164,18 +166,18 @@ def get_weekly_activity(commits: list[dict]) -> list[dict]: def get_hourly_activity(commits: list[dict]) -> list[dict]: """ Calculate commit activity by hour of day. - + Args: commits: List of commit dictionaries - + Returns: List of hourly statistics """ from collections import Counter - + hour_counts = Counter(commit["date"].hour for commit in commits) total = len(commits) - + return [ { "hour": h, diff --git a/pyproject.toml b/pyproject.toml index 9cbca04..bdb4e66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,14 @@ gitstats = "gitstats.cli:app" [project.urls] Homepage = "https://github.com/mloncarevich/gitstats" Repository = "https://github.com/mloncarevich/gitstats" + +[tool.ruff] +target-version = "py39" +line-length = 100 + +[tool.ruff.lint] +select = ["E", "F", "I", "UP"] +ignore = ["E501"] + +[tool.ruff.format] +quote-style = "double"