git2report 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git2report-0.1.0/LICENSE +21 -0
- git2report-0.1.0/PKG-INFO +142 -0
- git2report-0.1.0/README.md +111 -0
- git2report-0.1.0/git2report/__init__.py +2 -0
- git2report-0.1.0/git2report/__main__.py +4 -0
- git2report-0.1.0/git2report/branches.py +34 -0
- git2report-0.1.0/git2report/cache.py +78 -0
- git2report-0.1.0/git2report/charts.py +155 -0
- git2report-0.1.0/git2report/chunker.py +47 -0
- git2report-0.1.0/git2report/cli.py +173 -0
- git2report-0.1.0/git2report/clustering.py +56 -0
- git2report-0.1.0/git2report/config.py +28 -0
- git2report-0.1.0/git2report/eval_tools.py +123 -0
- git2report-0.1.0/git2report/extractor.py +37 -0
- git2report-0.1.0/git2report/filtering.py +34 -0
- git2report-0.1.0/git2report/metrics.py +111 -0
- git2report-0.1.0/git2report/models.py +45 -0
- git2report-0.1.0/git2report/pipeline.py +188 -0
- git2report-0.1.0/git2report/report.py +884 -0
- git2report-0.1.0/git2report/rules.py +71 -0
- git2report-0.1.0/git2report/summarizer.py +359 -0
- git2report-0.1.0/git2report/timeutil.py +19 -0
- git2report-0.1.0/git2report/web.py +1207 -0
- git2report-0.1.0/git2report.egg-info/PKG-INFO +142 -0
- git2report-0.1.0/git2report.egg-info/SOURCES.txt +35 -0
- git2report-0.1.0/git2report.egg-info/dependency_links.txt +1 -0
- git2report-0.1.0/git2report.egg-info/entry_points.txt +4 -0
- git2report-0.1.0/git2report.egg-info/requires.txt +8 -0
- git2report-0.1.0/git2report.egg-info/top_level.txt +1 -0
- git2report-0.1.0/pyproject.toml +52 -0
- git2report-0.1.0/setup.cfg +4 -0
- git2report-0.1.0/tests/test_chunker.py +30 -0
- git2report-0.1.0/tests/test_end_to_end.py +73 -0
- git2report-0.1.0/tests/test_eval_tools.py +36 -0
- git2report-0.1.0/tests/test_filter_cluster_metrics.py +65 -0
- git2report-0.1.0/tests/test_rules.py +54 -0
- git2report-0.1.0/tests/test_summarizer.py +128 -0
git2report-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vijay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git2report
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turn Git history into a plain-language, evidence-linked progress report with visual charts and Terminal Web Studio.
|
|
5
|
+
Author-email: Vijay <vijaykd04@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vijaykd04/Git2Report
|
|
8
|
+
Project-URL: Repository, https://github.com/vijaykd04/Git2Report.git
|
|
9
|
+
Project-URL: Issues, https://github.com/vijaykd04/Git2Report/issues
|
|
10
|
+
Keywords: git,report,changelog,ai,gemini,ollama,terminal,metrics
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Environment :: Console
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pydriller>=2.5
|
|
24
|
+
Requires-Dist: typer>=0.9
|
|
25
|
+
Requires-Dist: jinja2>=3.1
|
|
26
|
+
Requires-Dist: reportlab>=4.0
|
|
27
|
+
Requires-Dist: ollama>=0.3
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Git2Report
|
|
33
|
+
|
|
34
|
+
Turn a Git repository's history into a plain-language weekly progress report, where **every number is
|
|
35
|
+
counted by code** and **every line carries commit IDs as proof**. The AI (optional, local) only writes
|
|
36
|
+
sentences.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
git2report --repo . --since 7d --audience guide --out report.pdf
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
python -m venv .venv
|
|
46
|
+
.venv\Scripts\activate # Windows (Linux/macOS: source .venv/bin/activate)
|
|
47
|
+
pip install -e ".[dev]" # core + pytest
|
|
48
|
+
pip install -e ".[ollama]" # only if you want AI sentences (also install Ollama from ollama.com)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Use
|
|
52
|
+
|
|
53
|
+
### 🖥️ Interactive Web Studio GUI
|
|
54
|
+
Launch the browser-based dashboard to configure, run, preview, and download reports with 1 click:
|
|
55
|
+
```
|
|
56
|
+
git2report --ui # Opens Web Studio at http://localhost:5000
|
|
57
|
+
# or: git2report-ui
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 💻 Command Line (Terminal)
|
|
61
|
+
```bash
|
|
62
|
+
# 1. Standard full-format generation (PDF + HTML + Markdown)
|
|
63
|
+
git2report --repo ./myproject --since 7d --all --out report.html
|
|
64
|
+
|
|
65
|
+
# 2. Local AI with Ollama (Private, offline, zero API keys)
|
|
66
|
+
git2report --repo ./myproject --llm ollama --model llama3:8b
|
|
67
|
+
# Or shortcut:
|
|
68
|
+
git2report --repo ./myproject --ollama --model mistral
|
|
69
|
+
|
|
70
|
+
# 3. Cloud AI with Google Gemini (Fast, smart summary caching)
|
|
71
|
+
git2report --repo ./myproject --llm gemini --model gemini-2.5-flash
|
|
72
|
+
|
|
73
|
+
# 4. Pure offline deterministic mode (Zero AI, instant)
|
|
74
|
+
git2report --repo ./myproject --llm offline --audience guide
|
|
75
|
+
|
|
76
|
+
# 5. Targeted audience personas
|
|
77
|
+
git2report --repo ./myproject --since 2w --audience client --both # Business & user-facing
|
|
78
|
+
git2report --repo ./myproject --since 2w --audience team --both # Engineering release notes
|
|
79
|
+
|
|
80
|
+
# 6. Sprint goals & caching
|
|
81
|
+
git2report --repo . --next "Finish payment gateway" --next "Write tests" # Authentic developer goals
|
|
82
|
+
git2report --clear-cache # Reset summary cache
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### AI Engines (`--llm`)
|
|
86
|
+
- **`--llm ollama`**: Uses a local model running on your machine via [Ollama](https://ollama.com/) (e.g., `llama3:8b`, `llama3.2`, `mistral`, `deepseek-r1:8b`, `phi4`). Zero data leaves your machine. Automatically discovers available local models. If Ollama is offline or uninstalled, it gracefully falls back to deterministic offline wording without crashing.
|
|
87
|
+
- **`--llm gemini`**: Uses Google Gemini (`gemini-2.5-flash`) with smart SHA-256 caching (`.git2report_cache.json`) for fast execution and zero duplicate token costs. Set `GEMINI_API_KEY` in your environment or `.env` file.
|
|
88
|
+
- **`--llm offline`** (default): Builds clean human sentences directly from commit messages without invoking any AI.
|
|
89
|
+
|
|
90
|
+
Every AI output is validated by code: if the model returns invalid JSON, mentions raw numbers, or mentions file paths, Git2Report automatically retries once and falls back safely.
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## Evaluation (manual vs tool vs baseline)
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
git2report-eval sheet --repo ./proj --since 45d --n 25 --out eval.csv # builds the sheet
|
|
97
|
+
# open eval.csv in Excel: write manual_summary, give 1-5 scores, count unsupported claims
|
|
98
|
+
git2report-eval score eval.csv --out results.md # averages: tool vs baseline
|
|
99
|
+
```
|
|
100
|
+
The baseline is what you get without the pipeline: the raw commit message (offline) or the raw commit pasted
|
|
101
|
+
to the AI (`--llm ollama`). Be honest in the project report about the sample size and whose repo it was.
|
|
102
|
+
|
|
103
|
+
## Audiences (offline mode)
|
|
104
|
+
|
|
105
|
+
`guide` = full list, `client` = only user-facing work (features, fixes, performance), `team` = release-notes
|
|
106
|
+
grouped by type. A commit's type comes from `type(scope):` if present, otherwise it is guessed from its first
|
|
107
|
+
word (Add = feature, Fix = fix, Remove = refactor...). It is a heuristic, so check it on your own repo.
|
|
108
|
+
|
|
109
|
+
## How it works (7 steps, one file each)
|
|
110
|
+
|
|
111
|
+
| Step | File | Output |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| 1 Extract | `extractor.py` | list of `CommitRecord` |
|
|
114
|
+
| 2 Filter | `filtering.py` | kept commits + a reason for every decision |
|
|
115
|
+
| 3 Cluster | `clustering.py` | `{module: [commits]}` (scope, then folder, then commit type) |
|
|
116
|
+
| 4 Metrics | `metrics.py` | numbers (junk files excluded) |
|
|
117
|
+
| 5 Chunk | `chunker.py` | short code text per module, within a token budget |
|
|
118
|
+
| 6 Summarise | `summarizer.py` | one sentence per module (AI or offline), checked by code |
|
|
119
|
+
| 7 Report | `report.py` | Markdown (Jinja2) and PDF (ReportLab) |
|
|
120
|
+
|
|
121
|
+
Tunable rules (junk files, noise words, aliases, budget) live in `config.py`.
|
|
122
|
+
|
|
123
|
+
## Develop
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
pytest -q # 38 tests, uses a throw-away real Git repo
|
|
127
|
+
bash scripts/make_demo_repo.sh # 10-commit demo repo in ./demo_repo
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Status
|
|
131
|
+
|
|
132
|
+
Done and tested:
|
|
133
|
+
- Steps 1-7 deterministic pipeline with verified commit hashes.
|
|
134
|
+
- 🖥️ **Interactive Web Studio GUI** (`git2report --ui`) with real-time progress, live preview, and direct PDF/HTML/MD downloads.
|
|
135
|
+
- ⚡ **Smart Summary Cache** (`.git2report_cache.json`) for instant sub-second re-runs and 0 API token waste.
|
|
136
|
+
- 📊 **Visual Analytics & Charts** (Commit type distribution segmented bar & module churn chart embedded in PDF & HTML).
|
|
137
|
+
- Offline summaries per audience, unmerged "in progress" branches, developer-written sprint goals.
|
|
138
|
+
- 🦙 **Ollama Local AI & Google Gemini Cloud AI**: Choose between 100% private local models (`llama3:8b`, `mistral`, `deepseek-r1`) or Google Gemini (`gemini-2.5-flash`), with automated code-validation and fallback.
|
|
139
|
+
- ReportLab PDF, interactive HTML, Markdown, and 43 automated tests.
|
|
140
|
+
- Tried on demo repo, pallets/click, and real full-stack project (CampusThrift).
|
|
141
|
+
|
|
142
|
+
**Optional future work:** config file, multi-repo aggregator.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Git2Report
|
|
2
|
+
|
|
3
|
+
Turn a Git repository's history into a plain-language weekly progress report, where **every number is
|
|
4
|
+
counted by code** and **every line carries commit IDs as proof**. The AI (optional, local) only writes
|
|
5
|
+
sentences.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
git2report --repo . --since 7d --audience guide --out report.pdf
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
python -m venv .venv
|
|
15
|
+
.venv\Scripts\activate # Windows (Linux/macOS: source .venv/bin/activate)
|
|
16
|
+
pip install -e ".[dev]" # core + pytest
|
|
17
|
+
pip install -e ".[ollama]" # only if you want AI sentences (also install Ollama from ollama.com)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
### 🖥️ Interactive Web Studio GUI
|
|
23
|
+
Launch the browser-based dashboard to configure, run, preview, and download reports with 1 click:
|
|
24
|
+
```
|
|
25
|
+
git2report --ui # Opens Web Studio at http://localhost:5000
|
|
26
|
+
# or: git2report-ui
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 💻 Command Line (Terminal)
|
|
30
|
+
```bash
|
|
31
|
+
# 1. Standard full-format generation (PDF + HTML + Markdown)
|
|
32
|
+
git2report --repo ./myproject --since 7d --all --out report.html
|
|
33
|
+
|
|
34
|
+
# 2. Local AI with Ollama (Private, offline, zero API keys)
|
|
35
|
+
git2report --repo ./myproject --llm ollama --model llama3:8b
|
|
36
|
+
# Or shortcut:
|
|
37
|
+
git2report --repo ./myproject --ollama --model mistral
|
|
38
|
+
|
|
39
|
+
# 3. Cloud AI with Google Gemini (Fast, smart summary caching)
|
|
40
|
+
git2report --repo ./myproject --llm gemini --model gemini-2.5-flash
|
|
41
|
+
|
|
42
|
+
# 4. Pure offline deterministic mode (Zero AI, instant)
|
|
43
|
+
git2report --repo ./myproject --llm offline --audience guide
|
|
44
|
+
|
|
45
|
+
# 5. Targeted audience personas
|
|
46
|
+
git2report --repo ./myproject --since 2w --audience client --both # Business & user-facing
|
|
47
|
+
git2report --repo ./myproject --since 2w --audience team --both # Engineering release notes
|
|
48
|
+
|
|
49
|
+
# 6. Sprint goals & caching
|
|
50
|
+
git2report --repo . --next "Finish payment gateway" --next "Write tests" # Authentic developer goals
|
|
51
|
+
git2report --clear-cache # Reset summary cache
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### AI Engines (`--llm`)
|
|
55
|
+
- **`--llm ollama`**: Uses a local model running on your machine via [Ollama](https://ollama.com/) (e.g., `llama3:8b`, `llama3.2`, `mistral`, `deepseek-r1:8b`, `phi4`). Zero data leaves your machine. Automatically discovers available local models. If Ollama is offline or uninstalled, it gracefully falls back to deterministic offline wording without crashing.
|
|
56
|
+
- **`--llm gemini`**: Uses Google Gemini (`gemini-2.5-flash`) with smart SHA-256 caching (`.git2report_cache.json`) for fast execution and zero duplicate token costs. Set `GEMINI_API_KEY` in your environment or `.env` file.
|
|
57
|
+
- **`--llm offline`** (default): Builds clean human sentences directly from commit messages without invoking any AI.
|
|
58
|
+
|
|
59
|
+
Every AI output is validated by code: if the model returns invalid JSON, mentions raw numbers, or mentions file paths, Git2Report automatically retries once and falls back safely.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Evaluation (manual vs tool vs baseline)
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
git2report-eval sheet --repo ./proj --since 45d --n 25 --out eval.csv # builds the sheet
|
|
66
|
+
# open eval.csv in Excel: write manual_summary, give 1-5 scores, count unsupported claims
|
|
67
|
+
git2report-eval score eval.csv --out results.md # averages: tool vs baseline
|
|
68
|
+
```
|
|
69
|
+
The baseline is what you get without the pipeline: the raw commit message (offline) or the raw commit pasted
|
|
70
|
+
to the AI (`--llm ollama`). Be honest in the project report about the sample size and whose repo it was.
|
|
71
|
+
|
|
72
|
+
## Audiences (offline mode)
|
|
73
|
+
|
|
74
|
+
`guide` = full list, `client` = only user-facing work (features, fixes, performance), `team` = release-notes
|
|
75
|
+
grouped by type. A commit's type comes from `type(scope):` if present, otherwise it is guessed from its first
|
|
76
|
+
word (Add = feature, Fix = fix, Remove = refactor...). It is a heuristic, so check it on your own repo.
|
|
77
|
+
|
|
78
|
+
## How it works (7 steps, one file each)
|
|
79
|
+
|
|
80
|
+
| Step | File | Output |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| 1 Extract | `extractor.py` | list of `CommitRecord` |
|
|
83
|
+
| 2 Filter | `filtering.py` | kept commits + a reason for every decision |
|
|
84
|
+
| 3 Cluster | `clustering.py` | `{module: [commits]}` (scope, then folder, then commit type) |
|
|
85
|
+
| 4 Metrics | `metrics.py` | numbers (junk files excluded) |
|
|
86
|
+
| 5 Chunk | `chunker.py` | short code text per module, within a token budget |
|
|
87
|
+
| 6 Summarise | `summarizer.py` | one sentence per module (AI or offline), checked by code |
|
|
88
|
+
| 7 Report | `report.py` | Markdown (Jinja2) and PDF (ReportLab) |
|
|
89
|
+
|
|
90
|
+
Tunable rules (junk files, noise words, aliases, budget) live in `config.py`.
|
|
91
|
+
|
|
92
|
+
## Develop
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
pytest -q # 38 tests, uses a throw-away real Git repo
|
|
96
|
+
bash scripts/make_demo_repo.sh # 10-commit demo repo in ./demo_repo
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Status
|
|
100
|
+
|
|
101
|
+
Done and tested:
|
|
102
|
+
- Steps 1-7 deterministic pipeline with verified commit hashes.
|
|
103
|
+
- 🖥️ **Interactive Web Studio GUI** (`git2report --ui`) with real-time progress, live preview, and direct PDF/HTML/MD downloads.
|
|
104
|
+
- ⚡ **Smart Summary Cache** (`.git2report_cache.json`) for instant sub-second re-runs and 0 API token waste.
|
|
105
|
+
- 📊 **Visual Analytics & Charts** (Commit type distribution segmented bar & module churn chart embedded in PDF & HTML).
|
|
106
|
+
- Offline summaries per audience, unmerged "in progress" branches, developer-written sprint goals.
|
|
107
|
+
- 🦙 **Ollama Local AI & Google Gemini Cloud AI**: Choose between 100% private local models (`llama3:8b`, `mistral`, `deepseek-r1`) or Google Gemini (`gemini-2.5-flash`), with automated code-validation and fallback.
|
|
108
|
+
- ReportLab PDF, interactive HTML, Markdown, and 43 automated tests.
|
|
109
|
+
- Tried on demo repo, pallets/click, and real full-stack project (CampusThrift).
|
|
110
|
+
|
|
111
|
+
**Optional future work:** config file, multi-repo aggregator.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Open branches = work that exists but is not merged yet. Used for the 'In progress' section."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from git2report.models import BranchInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _git(repo: Path, *args: str) -> str:
|
|
12
|
+
r = subprocess.run(["git", *args], cwd=repo, capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
13
|
+
if r.returncode != 0:
|
|
14
|
+
raise RuntimeError(r.stderr.strip())
|
|
15
|
+
return r.stdout
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def open_branches(repo: Path, since: datetime, base: str = "HEAD") -> list[BranchInfo]:
|
|
19
|
+
"""Branches not merged into `base` with activity since `since`. Never raises: returns [] on any problem."""
|
|
20
|
+
try:
|
|
21
|
+
fmt = "%(refname:short)|%(committerdate:short)|%(subject)"
|
|
22
|
+
out = _git(Path(repo), "for-each-ref", f"--no-merged={base}", f"--format={fmt}", "refs/heads", "refs/remotes")
|
|
23
|
+
found: dict[str, BranchInfo] = {}
|
|
24
|
+
for line in out.splitlines():
|
|
25
|
+
name, date, subject = line.split("|", 2)
|
|
26
|
+
short = name.split("/", 1)[1] if name.startswith("origin/") else name
|
|
27
|
+
if name == "origin" or short == "HEAD" or short in found or date < since.date().isoformat():
|
|
28
|
+
continue
|
|
29
|
+
ahead = int(_git(Path(repo), "rev-list", "--count", f"{base}..{name}").strip() or 0)
|
|
30
|
+
if ahead:
|
|
31
|
+
found[short] = BranchInfo(short, date, ahead, subject)
|
|
32
|
+
return sorted(found.values(), key=lambda b: b.last_date, reverse=True)
|
|
33
|
+
except Exception:
|
|
34
|
+
return []
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Smart summary cache for Git2Report.
|
|
2
|
+
|
|
3
|
+
Caches AI module summaries and executive summaries by commit hashes,
|
|
4
|
+
preventing redundant LLM calls and enabling sub-second re-runs.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("git2report.cache")
|
|
15
|
+
DEFAULT_CACHE_FILE = Path(".git2report_cache.json")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compute_cache_key(prefix: str, module: str, commit_hashes: list[str], audience: str, model: str) -> str:
|
|
19
|
+
"""Create a deterministic SHA-256 cache key based on inputs."""
|
|
20
|
+
sorted_hashes = sorted(commit_hashes)
|
|
21
|
+
payload = f"{prefix}::{module}::{','.join(sorted_hashes)}::{audience}::{model}"
|
|
22
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SummaryCache:
|
|
26
|
+
def __init__(self, cache_file: Path | str = DEFAULT_CACHE_FILE, enabled: bool = True):
|
|
27
|
+
self.cache_file = Path(cache_file)
|
|
28
|
+
self.enabled = enabled
|
|
29
|
+
self._data: dict[str, dict[str, Any]] = {}
|
|
30
|
+
self._load()
|
|
31
|
+
|
|
32
|
+
def _load(self) -> None:
|
|
33
|
+
if not self.enabled or not self.cache_file.exists():
|
|
34
|
+
return
|
|
35
|
+
try:
|
|
36
|
+
raw = self.cache_file.read_text(encoding="utf-8")
|
|
37
|
+
self._data = json.loads(raw)
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
logger.debug(f"Failed to load cache: {exc}")
|
|
40
|
+
self._data = {}
|
|
41
|
+
|
|
42
|
+
def _save(self) -> None:
|
|
43
|
+
if not self.enabled:
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
self.cache_file.write_text(json.dumps(self._data, indent=2), encoding="utf-8")
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
logger.debug(f"Failed to save cache: {exc}")
|
|
49
|
+
|
|
50
|
+
def get(self, key: str) -> dict[str, Any] | None:
|
|
51
|
+
if not self.enabled:
|
|
52
|
+
return None
|
|
53
|
+
return self._data.get(key)
|
|
54
|
+
|
|
55
|
+
def set(self, key: str, summary: str, mode: str = "ai", meta: dict[str, Any] | None = None) -> None:
|
|
56
|
+
if not self.enabled:
|
|
57
|
+
return
|
|
58
|
+
entry = {
|
|
59
|
+
"summary": summary,
|
|
60
|
+
"mode": mode,
|
|
61
|
+
"meta": meta or {},
|
|
62
|
+
}
|
|
63
|
+
self._data[key] = entry
|
|
64
|
+
self._save()
|
|
65
|
+
|
|
66
|
+
def clear(self) -> int:
|
|
67
|
+
count = len(self._data)
|
|
68
|
+
self._data = {}
|
|
69
|
+
if self.cache_file.exists():
|
|
70
|
+
try:
|
|
71
|
+
self.cache_file.unlink()
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
return count
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Global default cache instance
|
|
78
|
+
default_cache = SummaryCache()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Visual analytics and chart generators for Git2Report.
|
|
2
|
+
|
|
3
|
+
Renders vector drawings for ReportLab PDF and embedded responsive SVG/HTML charts
|
|
4
|
+
without requiring external heavy chart dependencies.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from reportlab.lib import colors
|
|
12
|
+
from reportlab.graphics.shapes import Drawing, Group, Line, Rect, String
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
TYPE_COLORS = {
|
|
16
|
+
"feat": colors.HexColor("#059669"), # Emerald Green
|
|
17
|
+
"fix": colors.HexColor("#DC2626"), # Red
|
|
18
|
+
"perf": colors.HexColor("#7C3AED"), # Purple
|
|
19
|
+
"refactor": colors.HexColor("#D97706"), # Amber
|
|
20
|
+
"docs": colors.HexColor("#0284C7"), # Cyan
|
|
21
|
+
"test": colors.HexColor("#0D9488"), # Teal
|
|
22
|
+
"style": colors.HexColor("#64748B"), # Slate
|
|
23
|
+
"chore": colors.HexColor("#94A3B8"), # Light Slate
|
|
24
|
+
"other": colors.HexColor("#3B82F6"), # Blue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_type_breakdown_bar(commits_by_type: dict[str, int], width: float = 490, height: float = 38) -> Drawing:
|
|
29
|
+
"""Creates a sleek, segmented horizontal bar drawing showing commit type percentages."""
|
|
30
|
+
d = Drawing(width, height)
|
|
31
|
+
total = sum(commits_by_type.values())
|
|
32
|
+
if total <= 0:
|
|
33
|
+
return d
|
|
34
|
+
|
|
35
|
+
bar_y = 16
|
|
36
|
+
bar_h = 14
|
|
37
|
+
cur_x = 0.0
|
|
38
|
+
|
|
39
|
+
# Draw segments
|
|
40
|
+
for ctype, count in commits_by_type.items():
|
|
41
|
+
if count <= 0:
|
|
42
|
+
continue
|
|
43
|
+
seg_w = (count / total) * width
|
|
44
|
+
color = TYPE_COLORS.get(ctype.lower(), TYPE_COLORS["other"])
|
|
45
|
+
d.add(Rect(cur_x, bar_y, seg_w, bar_h, fillColor=color, strokeColor=colors.white, strokeWidth=1))
|
|
46
|
+
|
|
47
|
+
# Label percentage inside segment if wide enough
|
|
48
|
+
if seg_w > 26:
|
|
49
|
+
pct_text = f"{int(round((count / total) * 100))}%"
|
|
50
|
+
d.add(String(cur_x + seg_w / 2, bar_y + 3.5, pct_text, textAnchor="middle",
|
|
51
|
+
fontName="Helvetica-Bold", fontSize=7.5, fillColor=colors.white))
|
|
52
|
+
cur_x += seg_w
|
|
53
|
+
|
|
54
|
+
# Draw Legend below the bar
|
|
55
|
+
leg_x = 0.0
|
|
56
|
+
for ctype, count in list(commits_by_type.items())[:6]:
|
|
57
|
+
if count <= 0:
|
|
58
|
+
continue
|
|
59
|
+
color = TYPE_COLORS.get(ctype.lower(), TYPE_COLORS["other"])
|
|
60
|
+
# Dot
|
|
61
|
+
d.add(Rect(leg_x, 3, 7, 7, fillColor=color, strokeColor=color, rx=1.5, ry=1.5))
|
|
62
|
+
label = f"{ctype.upper()} ({count})"
|
|
63
|
+
d.add(String(leg_x + 10, 3, label, fontName="Helvetica", fontSize=7, fillColor=colors.HexColor("#475569")))
|
|
64
|
+
leg_x += len(label) * 4.4 + 16
|
|
65
|
+
|
|
66
|
+
return d
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_module_churn_chart(sections: list[Any], width: float = 490, height: float = 85) -> Drawing:
|
|
70
|
+
"""Creates a horizontal vector bar chart showing code churn (+added / -deleted) per module."""
|
|
71
|
+
d = Drawing(width, height)
|
|
72
|
+
if not sections:
|
|
73
|
+
return d
|
|
74
|
+
|
|
75
|
+
# Collect up to 4 top modules by line changes
|
|
76
|
+
module_data = []
|
|
77
|
+
for s in sections[:4]:
|
|
78
|
+
added = sum(getattr(cm, "added", 0) for cm in getattr(s, "commits", []))
|
|
79
|
+
deleted = sum(getattr(cm, "deleted", 0) for cm in getattr(s, "commits", []))
|
|
80
|
+
module_data.append((s.module, added, deleted))
|
|
81
|
+
|
|
82
|
+
if not module_data:
|
|
83
|
+
return d
|
|
84
|
+
|
|
85
|
+
max_val = max(max(a, d_) for _, a, d_ in module_data) or 1
|
|
86
|
+
row_h = 16
|
|
87
|
+
start_y = height - 12
|
|
88
|
+
label_w = 90
|
|
89
|
+
bar_max_w = width - label_w - 45
|
|
90
|
+
|
|
91
|
+
green = colors.HexColor("#059669")
|
|
92
|
+
red = colors.HexColor("#DC2626")
|
|
93
|
+
|
|
94
|
+
for i, (mod, added, deleted) in enumerate(module_data):
|
|
95
|
+
y = start_y - (i * (row_h + 3))
|
|
96
|
+
|
|
97
|
+
# Module Name label
|
|
98
|
+
mod_name = mod[:14] + ".." if len(mod) > 16 else mod
|
|
99
|
+
d.add(String(0, y + 2, mod_name, fontName="Helvetica-Bold", fontSize=7.5, fillColor=colors.HexColor("#1E293B")))
|
|
100
|
+
|
|
101
|
+
# Added bar (Green)
|
|
102
|
+
w_add = max(2.0, (added / max_val) * (bar_max_w / 2)) if added > 0 else 0
|
|
103
|
+
if w_add > 0:
|
|
104
|
+
d.add(Rect(label_w, y, w_add, 7, fillColor=green, strokeColor=green, rx=1, ry=1))
|
|
105
|
+
|
|
106
|
+
# Deleted bar (Red)
|
|
107
|
+
w_del = max(2.0, (deleted / max_val) * (bar_max_w / 2)) if deleted > 0 else 0
|
|
108
|
+
if w_del > 0:
|
|
109
|
+
d.add(Rect(label_w + w_add + 4, y, w_del, 7, fillColor=red, strokeColor=red, rx=1, ry=1))
|
|
110
|
+
|
|
111
|
+
# Stat text
|
|
112
|
+
stat_text = f"+{added} / -{deleted}"
|
|
113
|
+
d.add(String(label_w + w_add + w_del + 10, y + 1, stat_text, fontName="Courier", fontSize=6.5, fillColor=colors.HexColor("#64748B")))
|
|
114
|
+
|
|
115
|
+
return d
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def generate_svg_churn_chart(sections: list[Any]) -> str:
|
|
119
|
+
"""Generates an embedded responsive SVG for the interactive HTML report dashboard."""
|
|
120
|
+
module_data = []
|
|
121
|
+
for s in sections[:5]:
|
|
122
|
+
added = sum(getattr(cm, "added", 0) for cm in getattr(s, "commits", []))
|
|
123
|
+
deleted = sum(getattr(cm, "deleted", 0) for cm in getattr(s, "commits", []))
|
|
124
|
+
module_data.append((s.module, added, deleted))
|
|
125
|
+
|
|
126
|
+
if not module_data:
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
max_val = max(max(a, d_) for _, a, d_ in module_data) or 1
|
|
130
|
+
svg_height = len(module_data) * 36 + 20
|
|
131
|
+
rows = []
|
|
132
|
+
|
|
133
|
+
for i, (mod, added, deleted) in enumerate(module_data):
|
|
134
|
+
y = 20 + i * 36
|
|
135
|
+
w_add = (added / max_val) * 220 if added > 0 else 0
|
|
136
|
+
w_del = (deleted / max_val) * 220 if deleted > 0 else 0
|
|
137
|
+
|
|
138
|
+
rows.append(f"""
|
|
139
|
+
<g class="chart-row">
|
|
140
|
+
<text x="0" y="{y + 14}" fill="currentColor" font-size="12" font-weight="600">{mod}</text>
|
|
141
|
+
<rect x="130" y="{y}" width="{w_add:.1f}" height="16" rx="3" fill="#10b981" opacity="0.9">
|
|
142
|
+
<title>Added: +{added} lines</title>
|
|
143
|
+
</rect>
|
|
144
|
+
<rect x="{135 + w_add:.1f}" y="{y}" width="{w_del:.1f}" height="16" rx="3" fill="#ef4444" opacity="0.9">
|
|
145
|
+
<title>Deleted: -{deleted} lines</title>
|
|
146
|
+
</rect>
|
|
147
|
+
<text x="{145 + w_add + w_del:.1f}" y="{y + 13}" fill="var(--text-muted)" font-size="11" font-family="monospace">+{added} / -{deleted}</text>
|
|
148
|
+
</g>
|
|
149
|
+
""")
|
|
150
|
+
|
|
151
|
+
return f"""
|
|
152
|
+
<svg viewBox="0 0 650 {svg_height}" width="100%" height="{svg_height}" xmlns="http://www.w3.org/2000/svg" style="overflow: visible;">
|
|
153
|
+
{''.join(rows)}
|
|
154
|
+
</svg>
|
|
155
|
+
"""
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""STEP 5: shrink a module's code changes to fit the AI's limit.
|
|
2
|
+
Drop junk files, rank files (source before tests, big before small), split into hunks,
|
|
3
|
+
keep only the added/removed lines, and stop when the token budget is full."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from git2report.config import Config
|
|
10
|
+
from git2report.models import CommitRecord
|
|
11
|
+
from git2report.rules import is_junk, is_test, tokens
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Chunk:
|
|
16
|
+
text: str
|
|
17
|
+
raw_tokens: int
|
|
18
|
+
used_tokens: int
|
|
19
|
+
hunks_dropped: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def chunk_module(commits: list[CommitRecord], cfg: Config, budget: int | None = None) -> Chunk:
|
|
23
|
+
budget = cfg.token_budget if budget is None else budget
|
|
24
|
+
all_files = [f for c in commits for f in c.files]
|
|
25
|
+
raw = sum(tokens(f.diff) for f in all_files)
|
|
26
|
+
files = [f for f in all_files if not is_junk(f.path, cfg.junk) and f.diff.strip()]
|
|
27
|
+
files.sort(key=lambda f: (is_test(f.path), -(f.added + f.deleted)))
|
|
28
|
+
parts: list[str] = []
|
|
29
|
+
used = dropped = 0
|
|
30
|
+
for f in files:
|
|
31
|
+
for hunk in re.split(r"(?m)^(?=@@)", f.diff):
|
|
32
|
+
if not hunk.strip():
|
|
33
|
+
continue
|
|
34
|
+
body = "\n".join(
|
|
35
|
+
line for line in hunk.splitlines()
|
|
36
|
+
if line[:1] in ("+", "-") and not line.startswith(("+++", "---"))
|
|
37
|
+
)
|
|
38
|
+
if not body.strip():
|
|
39
|
+
continue
|
|
40
|
+
piece = f"# {f.path}\n{body}"
|
|
41
|
+
cost = tokens(piece)
|
|
42
|
+
if used + cost <= budget:
|
|
43
|
+
parts.append(piece)
|
|
44
|
+
used += cost
|
|
45
|
+
else:
|
|
46
|
+
dropped += 1
|
|
47
|
+
return Chunk("\n".join(parts), raw, used, dropped)
|