git-to-doc 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.
- git_to_doc-0.1.0/PKG-INFO +104 -0
- git_to_doc-0.1.0/README.md +94 -0
- git_to_doc-0.1.0/git_to_doc/__init__.py +6 -0
- git_to_doc-0.1.0/git_to_doc/cli.py +199 -0
- git_to_doc-0.1.0/git_to_doc/compare.py +164 -0
- git_to_doc-0.1.0/git_to_doc/model.py +62 -0
- git_to_doc-0.1.0/git_to_doc/renderer.py +171 -0
- git_to_doc-0.1.0/git_to_doc.egg-info/PKG-INFO +104 -0
- git_to_doc-0.1.0/git_to_doc.egg-info/SOURCES.txt +13 -0
- git_to_doc-0.1.0/git_to_doc.egg-info/dependency_links.txt +1 -0
- git_to_doc-0.1.0/git_to_doc.egg-info/entry_points.txt +3 -0
- git_to_doc-0.1.0/git_to_doc.egg-info/requires.txt +3 -0
- git_to_doc-0.1.0/git_to_doc.egg-info/top_level.txt +1 -0
- git_to_doc-0.1.0/pyproject.toml +23 -0
- git_to_doc-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-to-doc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate Conventional Commit messages & changelogs from git diffs using local Gemma
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: ollama>=0.4
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Requires-Dist: requests>=2.31
|
|
10
|
+
|
|
11
|
+
# git-to-doc ⚡
|
|
12
|
+
|
|
13
|
+
> Turn any git diff into a Conventional Commit message, changelog, and plain-English PR summary — powered by Gemma running locally via Ollama.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install git-to-doc
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Requires:** [Ollama](https://ollama.com) running locally with a Gemma model pulled:
|
|
22
|
+
```bash
|
|
23
|
+
ollama pull gemma4
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# From a GitHub PR URL
|
|
30
|
+
git-to-doc https://github.com/pallets/flask/pull/5000
|
|
31
|
+
|
|
32
|
+
# From a local .diff file
|
|
33
|
+
git-to-doc sample.diff
|
|
34
|
+
|
|
35
|
+
# Save output as markdown
|
|
36
|
+
git-to-doc https://github.com/pallets/flask/pull/5000 --output md
|
|
37
|
+
|
|
38
|
+
# Save both markdown and JSON
|
|
39
|
+
git-to-doc sample.diff --output both
|
|
40
|
+
|
|
41
|
+
# Use a different model
|
|
42
|
+
git-to-doc sample.diff --model gemma2:2b
|
|
43
|
+
|
|
44
|
+
# Benchmark multiple diffs
|
|
45
|
+
git-to-doc-compare sample.diff https://github.com/pallets/flask/pull/5000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Output
|
|
49
|
+
|
|
50
|
+
For each diff, `git-to-doc` produces:
|
|
51
|
+
|
|
52
|
+
1. **Conventional Commit message** — ready to paste into `git commit`
|
|
53
|
+
2. **Changelog entry** — ready to paste into `CHANGELOG.md`
|
|
54
|
+
3. **Plain-English summary** — for non-technical stakeholders
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
──────────────────────────────────────────────────────────
|
|
58
|
+
✨ CONVENTIONAL COMMIT MESSAGE
|
|
59
|
+
──────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
feat(parser): add null safety and safe_parse wrapper
|
|
62
|
+
|
|
63
|
+
──────────────────────────────────────────────────────────
|
|
64
|
+
📋 CHANGELOG ENTRY
|
|
65
|
+
──────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
## [Unreleased] — 2026-06-26
|
|
68
|
+
### Added
|
|
69
|
+
- feat(parser): add null safety checks and safe_parse fallback
|
|
70
|
+
|
|
71
|
+
──────────────────────────────────────────────────────────
|
|
72
|
+
🗣️ PLAIN ENGLISH SUMMARY
|
|
73
|
+
──────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
The parser now safely handles missing or null input instead of crashing.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Supported Inputs
|
|
79
|
+
|
|
80
|
+
| Input | Example |
|
|
81
|
+
|---|---|
|
|
82
|
+
| GitHub PR URL | `git-to-doc https://github.com/org/repo/pull/123` |
|
|
83
|
+
| Raw diff URL | `git-to-doc https://patch-diff.githubusercontent.com/raw/org/repo/pull/123.diff` |
|
|
84
|
+
| Local `.diff` file | `git-to-doc my_changes.diff` |
|
|
85
|
+
| Folder of diffs | `git-to-doc ./diffs/` |
|
|
86
|
+
|
|
87
|
+
## Flags
|
|
88
|
+
|
|
89
|
+
| Flag | Description |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `--output md` | Save markdown file (auto-named `PR-123-changelog-DATE.md`) |
|
|
92
|
+
| `--output json` | Save JSON file |
|
|
93
|
+
| `--output both` | Save both |
|
|
94
|
+
| `--model NAME` | Ollama model to use (default: `gemma4`) |
|
|
95
|
+
|
|
96
|
+
## Built With
|
|
97
|
+
|
|
98
|
+
- [Ollama](https://ollama.com) — local LLM runtime
|
|
99
|
+
- [Gemma](https://ai.google.dev/gemma) — Google's open model
|
|
100
|
+
- [Pydantic](https://docs.pydantic.dev) — structured output validation
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Built at AI-First Developer Efficiencies Hackathon · Track 1: The Automagic Documenter
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# git-to-doc ⚡
|
|
2
|
+
|
|
3
|
+
> Turn any git diff into a Conventional Commit message, changelog, and plain-English PR summary — powered by Gemma running locally via Ollama.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install git-to-doc
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Requires:** [Ollama](https://ollama.com) running locally with a Gemma model pulled:
|
|
12
|
+
```bash
|
|
13
|
+
ollama pull gemma4
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# From a GitHub PR URL
|
|
20
|
+
git-to-doc https://github.com/pallets/flask/pull/5000
|
|
21
|
+
|
|
22
|
+
# From a local .diff file
|
|
23
|
+
git-to-doc sample.diff
|
|
24
|
+
|
|
25
|
+
# Save output as markdown
|
|
26
|
+
git-to-doc https://github.com/pallets/flask/pull/5000 --output md
|
|
27
|
+
|
|
28
|
+
# Save both markdown and JSON
|
|
29
|
+
git-to-doc sample.diff --output both
|
|
30
|
+
|
|
31
|
+
# Use a different model
|
|
32
|
+
git-to-doc sample.diff --model gemma2:2b
|
|
33
|
+
|
|
34
|
+
# Benchmark multiple diffs
|
|
35
|
+
git-to-doc-compare sample.diff https://github.com/pallets/flask/pull/5000
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Output
|
|
39
|
+
|
|
40
|
+
For each diff, `git-to-doc` produces:
|
|
41
|
+
|
|
42
|
+
1. **Conventional Commit message** — ready to paste into `git commit`
|
|
43
|
+
2. **Changelog entry** — ready to paste into `CHANGELOG.md`
|
|
44
|
+
3. **Plain-English summary** — for non-technical stakeholders
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
──────────────────────────────────────────────────────────
|
|
48
|
+
✨ CONVENTIONAL COMMIT MESSAGE
|
|
49
|
+
──────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
feat(parser): add null safety and safe_parse wrapper
|
|
52
|
+
|
|
53
|
+
──────────────────────────────────────────────────────────
|
|
54
|
+
📋 CHANGELOG ENTRY
|
|
55
|
+
──────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
## [Unreleased] — 2026-06-26
|
|
58
|
+
### Added
|
|
59
|
+
- feat(parser): add null safety checks and safe_parse fallback
|
|
60
|
+
|
|
61
|
+
──────────────────────────────────────────────────────────
|
|
62
|
+
🗣️ PLAIN ENGLISH SUMMARY
|
|
63
|
+
──────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
The parser now safely handles missing or null input instead of crashing.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Supported Inputs
|
|
69
|
+
|
|
70
|
+
| Input | Example |
|
|
71
|
+
|---|---|
|
|
72
|
+
| GitHub PR URL | `git-to-doc https://github.com/org/repo/pull/123` |
|
|
73
|
+
| Raw diff URL | `git-to-doc https://patch-diff.githubusercontent.com/raw/org/repo/pull/123.diff` |
|
|
74
|
+
| Local `.diff` file | `git-to-doc my_changes.diff` |
|
|
75
|
+
| Folder of diffs | `git-to-doc ./diffs/` |
|
|
76
|
+
|
|
77
|
+
## Flags
|
|
78
|
+
|
|
79
|
+
| Flag | Description |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `--output md` | Save markdown file (auto-named `PR-123-changelog-DATE.md`) |
|
|
82
|
+
| `--output json` | Save JSON file |
|
|
83
|
+
| `--output both` | Save both |
|
|
84
|
+
| `--model NAME` | Ollama model to use (default: `gemma4`) |
|
|
85
|
+
|
|
86
|
+
## Built With
|
|
87
|
+
|
|
88
|
+
- [Ollama](https://ollama.com) — local LLM runtime
|
|
89
|
+
- [Gemma](https://ai.google.dev/gemma) — Google's open model
|
|
90
|
+
- [Pydantic](https://docs.pydantic.dev) — structured output validation
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
Built at AI-First Developer Efficiencies Hackathon · Track 1: The Automagic Documenter
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
git-to-doc — Generate Conventional Commit messages & changelogs from git diffs.
|
|
4
|
+
|
|
5
|
+
Accepts:
|
|
6
|
+
- A GitHub PR URL (github.com or patch-diff.githubusercontent.com)
|
|
7
|
+
- A local .diff or .txt file
|
|
8
|
+
- A folder of .diff files (processes all of them)
|
|
9
|
+
|
|
10
|
+
Flags:
|
|
11
|
+
--output [md|json|both] Save output file(s). Defaults to 'md' if omitted.
|
|
12
|
+
--model MODEL Ollama model to use (default: gemma4).
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
python doc.py https://github.com/pallets/flask/pull/5000
|
|
16
|
+
python doc.py https://github.com/pallets/flask/pull/5000 --output
|
|
17
|
+
python doc.py https://github.com/pallets/flask/pull/5000 --output json
|
|
18
|
+
python doc.py https://github.com/pallets/flask/pull/5000 --output both
|
|
19
|
+
python doc.py sample.diff --output md
|
|
20
|
+
python doc.py ./diffs/ --output both
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import sys, re, json, argparse, time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from datetime import date
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
import requests
|
|
29
|
+
|
|
30
|
+
from git_to_doc.model import analyze_diff, CommitDoc
|
|
31
|
+
from git_to_doc.renderer import render_full_output, render_markdown_file
|
|
32
|
+
|
|
33
|
+
# ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
34
|
+
RESET = "\033[0m"; BOLD = "\033[1m"; GREEN = "\033[32m"
|
|
35
|
+
RED = "\033[31m"; CYAN = "\033[36m"; DIM = "\033[2m"; YELLOW = "\033[33m"
|
|
36
|
+
|
|
37
|
+
def _c(t, *c): return "".join(c) + t + RESET
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── URL normalisation ─────────────────────────────────────────────────────────
|
|
41
|
+
_GH_PR = re.compile(r'https?://github\.com/([^/]+/[^/]+)/pull/(\d+)', re.I)
|
|
42
|
+
_GH_RAW = re.compile(r'https?://patch-diff\.githubusercontent\.com/raw/([^/]+/[^/]+)/pull/(\d+)', re.I)
|
|
43
|
+
|
|
44
|
+
def normalise_url(url: str) -> tuple:
|
|
45
|
+
"""Return (fetch_url, slug) for any GitHub PR URL format."""
|
|
46
|
+
url = url.rstrip("/")
|
|
47
|
+
|
|
48
|
+
m = _GH_PR.match(url)
|
|
49
|
+
if m:
|
|
50
|
+
repo, pr = m.group(1), m.group(2)
|
|
51
|
+
fetch = f"https://patch-diff.githubusercontent.com/raw/{repo}/pull/{pr}.diff"
|
|
52
|
+
return fetch, f"PR-{pr}"
|
|
53
|
+
|
|
54
|
+
m = _GH_RAW.match(url)
|
|
55
|
+
if m:
|
|
56
|
+
pr = m.group(2)
|
|
57
|
+
fetch = url if url.endswith(".diff") else url + ".diff"
|
|
58
|
+
return fetch, f"PR-{pr}"
|
|
59
|
+
|
|
60
|
+
# Generic URL — ensure .diff suffix
|
|
61
|
+
fetch = url if url.endswith(".diff") else url + ".diff"
|
|
62
|
+
slug = re.sub(r'[^a-zA-Z0-9_-]', '-', Path(url).stem)[:40]
|
|
63
|
+
return fetch, slug
|
|
64
|
+
|
|
65
|
+
def is_url(s: str) -> bool:
|
|
66
|
+
return s.startswith("http://") or s.startswith("https://")
|
|
67
|
+
|
|
68
|
+
def fetch_diff(url: str) -> str:
|
|
69
|
+
fetch_url, _ = normalise_url(url)
|
|
70
|
+
print(_c(f" ↓ {fetch_url}", DIM))
|
|
71
|
+
resp = requests.get(fetch_url, timeout=30, headers={"User-Agent": "git-to-doc/1.0"})
|
|
72
|
+
resp.raise_for_status()
|
|
73
|
+
return resp.text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── Diff stats ────────────────────────────────────────────────────────────────
|
|
77
|
+
def diff_stats(text: str) -> dict:
|
|
78
|
+
lines = text.splitlines()
|
|
79
|
+
files = [l[len("diff --git "):].split(" b/")[-1]
|
|
80
|
+
for l in lines if l.startswith("diff --git ")]
|
|
81
|
+
adds = sum(1 for l in lines if l.startswith("+") and not l.startswith("+++"))
|
|
82
|
+
dels = sum(1 for l in lines if l.startswith("-") and not l.startswith("---"))
|
|
83
|
+
return {"files": files, "additions": adds, "deletions": dels}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── Auto filename ─────────────────────────────────────────────────────────────
|
|
87
|
+
def auto_stem(input_str: str) -> str:
|
|
88
|
+
today = date.today().isoformat()
|
|
89
|
+
if is_url(input_str):
|
|
90
|
+
_, slug = normalise_url(input_str)
|
|
91
|
+
return f"{slug}-changelog-{today}"
|
|
92
|
+
p = Path(input_str)
|
|
93
|
+
if p.is_dir():
|
|
94
|
+
return f"{p.name}-changelog-{today}"
|
|
95
|
+
return f"{p.stem}-changelog-{today}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── Single diff processing ────────────────────────────────────────────────────
|
|
99
|
+
def process_diff(diff_text: str, stem: str, model: str,
|
|
100
|
+
fmt: Optional[str], source: str = "") -> CommitDoc:
|
|
101
|
+
"""
|
|
102
|
+
fmt: None → terminal only | 'md' → save .md | 'json' → save .json | 'both' → both
|
|
103
|
+
source: original input (URL or file path) embedded in the markdown file
|
|
104
|
+
"""
|
|
105
|
+
stats = diff_stats(diff_text)
|
|
106
|
+
print(_c(f" Files changed : {len(stats['files'])}", DIM))
|
|
107
|
+
for f in stats["files"]:
|
|
108
|
+
print(_c(f" • {f}", DIM))
|
|
109
|
+
print(_c(f" +{stats['additions']} additions -{stats['deletions']} deletions", DIM))
|
|
110
|
+
print()
|
|
111
|
+
print(_c(f" ⏳ Sending to {model}…", DIM))
|
|
112
|
+
|
|
113
|
+
t0 = time.time()
|
|
114
|
+
result = analyze_diff(diff_text, model=model)
|
|
115
|
+
elapsed = time.time() - t0
|
|
116
|
+
print(_c(f" ⚡ Inference done in {elapsed:.1f}s", DIM))
|
|
117
|
+
print(render_full_output(result))
|
|
118
|
+
|
|
119
|
+
if fmt in ("md", "both"):
|
|
120
|
+
md_path = Path(f"{stem}.md")
|
|
121
|
+
md_path.write_text(
|
|
122
|
+
render_markdown_file(result, model=model, source=source, stats=stats),
|
|
123
|
+
encoding="utf-8"
|
|
124
|
+
)
|
|
125
|
+
print(_c(f" ✓ Markdown saved → {md_path}", GREEN))
|
|
126
|
+
|
|
127
|
+
if fmt in ("json", "both"):
|
|
128
|
+
json_path = Path(f"{stem}.json")
|
|
129
|
+
json_path.write_text(result.model_dump_json(indent=2), encoding="utf-8")
|
|
130
|
+
print(_c(f" ✓ JSON saved → {json_path}", GREEN))
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
136
|
+
def main():
|
|
137
|
+
parser = argparse.ArgumentParser(
|
|
138
|
+
description="Generate Conventional Commit messages & changelogs from git diffs.",
|
|
139
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
140
|
+
epilog=__doc__,
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument("input",
|
|
143
|
+
help="GitHub PR URL, local .diff file, or folder of .diff files")
|
|
144
|
+
parser.add_argument("--output", nargs="?", const="md", default=None,
|
|
145
|
+
metavar="{md,json,both}",
|
|
146
|
+
help="Save output: 'md' (default when flag used), 'json', or 'both'. Auto-names file.")
|
|
147
|
+
parser.add_argument("--model", default="gemma4",
|
|
148
|
+
help="Ollama model name (default: gemma4)")
|
|
149
|
+
|
|
150
|
+
args = parser.parse_args()
|
|
151
|
+
|
|
152
|
+
# Validate --output value
|
|
153
|
+
valid_fmts = {"md", "json", "both", None}
|
|
154
|
+
if args.output not in valid_fmts:
|
|
155
|
+
print(_c(f" ✗ --output must be md, json, or both (got '{args.output}')", RED))
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
|
|
158
|
+
fmt = args.output # None | 'md' | 'json' | 'both'
|
|
159
|
+
|
|
160
|
+
# ── Banner ────────────────────────────────────────────────────────────────
|
|
161
|
+
print(_c("\n 🔍 git-to-doc", BOLD, CYAN) +
|
|
162
|
+
_c(f" powered by {args.model}", DIM))
|
|
163
|
+
print(_c(" " + "─" * 52, DIM))
|
|
164
|
+
|
|
165
|
+
# ── Dispatch: URL / folder / file ─────────────────────────────────────────
|
|
166
|
+
try:
|
|
167
|
+
if is_url(args.input):
|
|
168
|
+
print(_c(" ⚡ Source: URL", BOLD))
|
|
169
|
+
diff_text = fetch_diff(args.input)
|
|
170
|
+
process_diff(diff_text, auto_stem(args.input), args.model, fmt, source=args.input)
|
|
171
|
+
|
|
172
|
+
elif Path(args.input).is_dir():
|
|
173
|
+
diffs = sorted(Path(args.input).glob("**/*.diff"))
|
|
174
|
+
if not diffs:
|
|
175
|
+
print(_c(f" ✗ No .diff files found in {args.input}", RED))
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
print(_c(f" ⚡ Source: folder ({len(diffs)} diff files)", BOLD))
|
|
178
|
+
for diff_file in diffs:
|
|
179
|
+
print(_c(f"\n ── {diff_file.name} ──", BOLD, CYAN))
|
|
180
|
+
stem = f"{diff_file.stem}-changelog-{date.today().isoformat()}"
|
|
181
|
+
process_diff(diff_file.read_text(encoding="utf-8"), stem, args.model, fmt, source=str(diff_file))
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
path = Path(args.input)
|
|
185
|
+
if not path.exists():
|
|
186
|
+
print(_c(f" ✗ File not found: {args.input}", RED))
|
|
187
|
+
sys.exit(1)
|
|
188
|
+
print(_c(f" ⚡ Source: {path.name}", BOLD))
|
|
189
|
+
process_diff(path.read_text(encoding="utf-8"), auto_stem(args.input), args.model, fmt, source=str(path))
|
|
190
|
+
|
|
191
|
+
except requests.HTTPError as e:
|
|
192
|
+
print(_c(f"\n ✗ HTTP error: {e}", RED))
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
print()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
compare.py — Benchmark git-to-doc across multiple diffs or models.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python compare.py <diff1> [diff2 ...] [--models m1 m2]
|
|
7
|
+
python compare.py ./diffs/ --models gemma4 llama3
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
python compare.py sample.diff
|
|
11
|
+
python compare.py sample.diff https://github.com/pallets/flask/pull/5000
|
|
12
|
+
python compare.py ./diffs/ --models gemma4
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys, argparse, time, json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List
|
|
18
|
+
|
|
19
|
+
import requests
|
|
20
|
+
|
|
21
|
+
from git_to_doc.model import analyze_diff, CommitDoc
|
|
22
|
+
|
|
23
|
+
# ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
24
|
+
RESET = "\033[0m"; BOLD = "\033[1m"; GREEN = "\033[32m"
|
|
25
|
+
RED = "\033[31m"; CYAN = "\033[36m"; DIM = "\033[2m"
|
|
26
|
+
YELLOW = "\033[33m"; WHITE = "\033[97m"
|
|
27
|
+
|
|
28
|
+
def _c(t, *c): return "".join(c) + t + RESET
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── Fetch / read diff ─────────────────────────────────────────────────────────
|
|
32
|
+
def load_diff(source: str) -> str:
|
|
33
|
+
if source.startswith("http://") or source.startswith("https://"):
|
|
34
|
+
url = source.rstrip("/")
|
|
35
|
+
if not url.endswith(".diff"):
|
|
36
|
+
url += ".diff"
|
|
37
|
+
resp = requests.get(url, timeout=30, headers={"User-Agent": "git-to-doc/1.0"})
|
|
38
|
+
resp.raise_for_status()
|
|
39
|
+
return resp.text
|
|
40
|
+
return Path(source).read_text(encoding="utf-8")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── Benchmark single diff against one model ───────────────────────────────────
|
|
44
|
+
def benchmark(diff_text: str, label: str, model: str) -> dict:
|
|
45
|
+
t0 = time.time()
|
|
46
|
+
result = analyze_diff(diff_text, model=model)
|
|
47
|
+
elapsed = time.time() - t0
|
|
48
|
+
return {
|
|
49
|
+
"label": label,
|
|
50
|
+
"model": model,
|
|
51
|
+
"elapsed": round(elapsed, 2),
|
|
52
|
+
"type": result.type,
|
|
53
|
+
"scope": result.scope,
|
|
54
|
+
"subject": result.subject,
|
|
55
|
+
"breaking": result.breaking,
|
|
56
|
+
"doc": result,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── Pretty table ──────────────────────────────────────────────────────────────
|
|
61
|
+
def print_table(results: list):
|
|
62
|
+
print()
|
|
63
|
+
print(_c(" " + "─" * 78, DIM))
|
|
64
|
+
print(_c(f" {'SOURCE':<28} {'MODEL':<12} {'TIME':>6} {'TYPE':<10} SUBJECT", BOLD, CYAN))
|
|
65
|
+
print(_c(" " + "─" * 78, DIM))
|
|
66
|
+
for r in results:
|
|
67
|
+
label = r["label"][:27]
|
|
68
|
+
model = r["model"][:11]
|
|
69
|
+
elapsed = f"{r['elapsed']}s"
|
|
70
|
+
typ = r["type"]
|
|
71
|
+
subject = r["subject"][:38]
|
|
72
|
+
color = GREEN if r["elapsed"] < 30 else YELLOW
|
|
73
|
+
print(f" {label:<28} {_c(model, DIM):<12} {_c(elapsed, color):>6} {_c(typ, BOLD):<10} {subject}")
|
|
74
|
+
print(_c(" " + "─" * 78, DIM))
|
|
75
|
+
print()
|
|
76
|
+
|
|
77
|
+
if len(results) > 1:
|
|
78
|
+
fastest = min(results, key=lambda r: r["elapsed"])
|
|
79
|
+
print(_c(f" ⚡ Fastest: {fastest['model']} on '{fastest['label']}' "
|
|
80
|
+
f"({fastest['elapsed']}s)", BOLD, GREEN))
|
|
81
|
+
print()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
85
|
+
def main():
|
|
86
|
+
parser = argparse.ArgumentParser(
|
|
87
|
+
description="Benchmark git-to-doc across multiple diffs and/or models.",
|
|
88
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
89
|
+
epilog=__doc__,
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument("inputs", nargs="+",
|
|
92
|
+
help="One or more diff files, URLs, or a folder")
|
|
93
|
+
parser.add_argument("--models", nargs="+", default=["gemma4"],
|
|
94
|
+
help="One or more Ollama model names to compare (default: gemma4)")
|
|
95
|
+
parser.add_argument("--output", action="store_true",
|
|
96
|
+
help="Save a compare_results.json summary file")
|
|
97
|
+
args = parser.parse_args()
|
|
98
|
+
|
|
99
|
+
# Expand folders to .diff files
|
|
100
|
+
sources = []
|
|
101
|
+
for inp in args.inputs:
|
|
102
|
+
p = Path(inp)
|
|
103
|
+
if p.is_dir():
|
|
104
|
+
found = sorted(p.glob("**/*.diff"))
|
|
105
|
+
if not found:
|
|
106
|
+
print(_c(f" ✗ No .diff files in {inp}", RED))
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
sources.extend(str(f) for f in found)
|
|
109
|
+
else:
|
|
110
|
+
sources.append(inp)
|
|
111
|
+
|
|
112
|
+
# ── Banner ────────────────────────────────────────────────────────────────
|
|
113
|
+
print(_c("\n 🔬 git-to-doc compare", BOLD, CYAN))
|
|
114
|
+
print(_c(f" {len(sources)} source(s) × {len(args.models)} model(s)"
|
|
115
|
+
f" = {len(sources) * len(args.models)} run(s)", DIM))
|
|
116
|
+
print(_c(" " + "─" * 52, DIM))
|
|
117
|
+
|
|
118
|
+
# ── Run benchmarks ────────────────────────────────────────────────────────
|
|
119
|
+
results = []
|
|
120
|
+
for source in sources:
|
|
121
|
+
label = source.split("/")[-1][:27]
|
|
122
|
+
try:
|
|
123
|
+
print(_c(f"\n Loading: {source}", DIM))
|
|
124
|
+
diff_text = load_diff(source)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(_c(f" ✗ Failed to load {source}: {e}", RED))
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
for model in args.models:
|
|
130
|
+
print(_c(f" ⏳ [{model}] {label}…", DIM), end="", flush=True)
|
|
131
|
+
try:
|
|
132
|
+
r = benchmark(diff_text, label, model)
|
|
133
|
+
results.append(r)
|
|
134
|
+
print(_c(f" {r['elapsed']}s", GREEN))
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print(_c(f" ✗ Error: {e}", RED))
|
|
137
|
+
|
|
138
|
+
if not results:
|
|
139
|
+
print(_c("\n No results to display.", RED))
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
# ── Results table ─────────────────────────────────────────────────────────
|
|
143
|
+
print_table(results)
|
|
144
|
+
|
|
145
|
+
# ── Per-result detail ─────────────────────────────────────────────────────
|
|
146
|
+
for r in results:
|
|
147
|
+
doc = r["doc"]
|
|
148
|
+
print(_c(f" [{r['model']}] {r['label']}", BOLD))
|
|
149
|
+
print(f" Commit : {_c(doc.type + (f'({doc.scope})' if doc.scope else '') + ': ' + doc.subject, WHITE)}")
|
|
150
|
+
print(f" Summary: {doc.plain_english[:120]}")
|
|
151
|
+
print()
|
|
152
|
+
|
|
153
|
+
# ── Save JSON summary ─────────────────────────────────────────────────────
|
|
154
|
+
if args.output:
|
|
155
|
+
out = [
|
|
156
|
+
{k: v for k, v in r.items() if k != "doc"}
|
|
157
|
+
for r in results
|
|
158
|
+
]
|
|
159
|
+
Path("compare_results.json").write_text(json.dumps(out, indent=2))
|
|
160
|
+
print(_c(" ✓ Saved compare_results.json", GREEN))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import ollama
|
|
2
|
+
import json
|
|
3
|
+
from pydantic import BaseModel, ValidationError
|
|
4
|
+
from typing import Literal, Optional
|
|
5
|
+
|
|
6
|
+
class CommitDoc(BaseModel):
|
|
7
|
+
type: Literal["feat","fix","docs","refactor","perf","test","chore","ci","build","revert"]
|
|
8
|
+
scope: Optional[str]
|
|
9
|
+
subject: str
|
|
10
|
+
body: Optional[str]
|
|
11
|
+
breaking: bool
|
|
12
|
+
changelog_entry: str
|
|
13
|
+
plain_english: str
|
|
14
|
+
|
|
15
|
+
SYSTEM_PROMPT = """
|
|
16
|
+
|
|
17
|
+
You are an expert Git commit message generator and technical writer.
|
|
18
|
+
Given a raw git diff, output ONLY a valid JSON object with no explanation, no markdown fences, no preamble.
|
|
19
|
+
|
|
20
|
+
Rules for each field:
|
|
21
|
+
- type: one of feat|fix|docs|refactor|perf|test|chore|ci|build|revert
|
|
22
|
+
- scope: lowercase name of the module or folder most affected (null if unclear)
|
|
23
|
+
- subject: imperative mood, lowercase, no trailing period, max 72 chars — be specific
|
|
24
|
+
- body: 1-3 sentences of technical detail for non-trivial changes (null if simple)
|
|
25
|
+
- breaking: true ONLY if existing public API contracts are removed or changed incompatibly
|
|
26
|
+
- changelog_entry: a single markdown bullet like "- feat(scope): description" ready to paste into CHANGELOG.md
|
|
27
|
+
- plain_english: 1-2 sentences a non-technical person could understand explaining WHAT changed and WHY
|
|
28
|
+
|
|
29
|
+
Output format: raw JSON only. No ```json fences. No commentary.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def analyze_diff(diff_text: str, model: str = "gemma4", max_retries: int = 3) -> CommitDoc:
|
|
34
|
+
if len(diff_text) > 6000:
|
|
35
|
+
diff_text = diff_text[:6000] + "\n... [truncated]"
|
|
36
|
+
|
|
37
|
+
for attempt in range(max_retries):
|
|
38
|
+
try:
|
|
39
|
+
response = ollama.chat(
|
|
40
|
+
model=model if ":" in model else f"{model}:latest",
|
|
41
|
+
messages=[
|
|
42
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
43
|
+
{"role": "user", "content": f"Analyze this git diff:\n\n{diff_text}"}
|
|
44
|
+
],
|
|
45
|
+
format=CommitDoc.model_json_schema(),
|
|
46
|
+
options={"temperature": 0}
|
|
47
|
+
)
|
|
48
|
+
raw = response["message"]["content"]
|
|
49
|
+
return CommitDoc.model_validate_json(raw)
|
|
50
|
+
except ValidationError as e:
|
|
51
|
+
print(f"[retry {attempt+1}] validation failed: {e}")
|
|
52
|
+
|
|
53
|
+
# Fallback — never crash during demo
|
|
54
|
+
return CommitDoc(
|
|
55
|
+
type="chore",
|
|
56
|
+
scope=None,
|
|
57
|
+
subject="update codebase",
|
|
58
|
+
body=None,
|
|
59
|
+
breaking=False,
|
|
60
|
+
changelog_entry="- chore: miscellaneous updates",
|
|
61
|
+
plain_english="General codebase updates were made."
|
|
62
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from git_to_doc.model import CommitDoc
|
|
3
|
+
|
|
4
|
+
RESET = "\033[0m"
|
|
5
|
+
BOLD = "\033[1m"
|
|
6
|
+
GREEN = "\033[32m"
|
|
7
|
+
CYAN = "\033[36m"
|
|
8
|
+
YELLOW = "\033[33m"
|
|
9
|
+
DIM = "\033[2m"
|
|
10
|
+
WHITE = "\033[97m"
|
|
11
|
+
|
|
12
|
+
def _c(text: str, *codes: str) -> str:
|
|
13
|
+
return "".join(codes) + text + RESET
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_SECTION = {
|
|
17
|
+
"feat": "Added",
|
|
18
|
+
"fix": "Fixed",
|
|
19
|
+
"docs": "Documentation",
|
|
20
|
+
"refactor": "Changed",
|
|
21
|
+
"perf": "Performance",
|
|
22
|
+
"test": "Tests",
|
|
23
|
+
"chore": "Maintenance",
|
|
24
|
+
"ci": "CI/CD",
|
|
25
|
+
"build": "Build",
|
|
26
|
+
"revert": "Reverted",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_TYPE_EMOJI = {
|
|
30
|
+
"feat": "✨",
|
|
31
|
+
"fix": "🐛",
|
|
32
|
+
"docs": "📝",
|
|
33
|
+
"refactor": "♻️",
|
|
34
|
+
"perf": "⚡",
|
|
35
|
+
"test": "🧪",
|
|
36
|
+
"chore": "🔧",
|
|
37
|
+
"ci": "🤖",
|
|
38
|
+
"build": "📦",
|
|
39
|
+
"revert": "⏪",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def render_commit_message(doc: CommitDoc) -> str:
|
|
44
|
+
"""Returns a Conventional Commit string, e.g. feat(scope): subject"""
|
|
45
|
+
scope = f"({doc.scope})" if doc.scope else ""
|
|
46
|
+
breaking = "!" if doc.breaking else ""
|
|
47
|
+
header = f"{doc.type}{scope}{breaking}: {doc.subject}"
|
|
48
|
+
|
|
49
|
+
parts = [header]
|
|
50
|
+
if doc.body:
|
|
51
|
+
parts.append(f"\n{doc.body}")
|
|
52
|
+
if doc.breaking:
|
|
53
|
+
parts.append(f"\nBREAKING CHANGE: {doc.body or doc.subject}")
|
|
54
|
+
return "\n".join(parts)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render_changelog(doc: CommitDoc) -> str:
|
|
58
|
+
"""Returns a markdown changelog snippet ready to paste into CHANGELOG.md"""
|
|
59
|
+
today = date.today().isoformat()
|
|
60
|
+
section = "⚠️ Breaking Changes" if doc.breaking else _SECTION.get(doc.type, "Changed")
|
|
61
|
+
|
|
62
|
+
lines = [
|
|
63
|
+
f"## [Unreleased] — {today}",
|
|
64
|
+
"",
|
|
65
|
+
f"### {section}",
|
|
66
|
+
"",
|
|
67
|
+
doc.changelog_entry,
|
|
68
|
+
]
|
|
69
|
+
if doc.body:
|
|
70
|
+
lines += ["", f" {doc.body}"]
|
|
71
|
+
return "\n".join(lines)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def render_full_output(doc: CommitDoc) -> str:
|
|
75
|
+
"""Pretty-prints the full terminal output with colour and markdown blocks."""
|
|
76
|
+
emoji = _TYPE_EMOJI.get(doc.type, "📌")
|
|
77
|
+
bar = _c("─" * 58, DIM)
|
|
78
|
+
|
|
79
|
+
commit_msg = render_commit_message(doc)
|
|
80
|
+
changelog = render_changelog(doc)
|
|
81
|
+
|
|
82
|
+
output = f"""
|
|
83
|
+
{bar}
|
|
84
|
+
{_c(f" {emoji} CONVENTIONAL COMMIT MESSAGE", BOLD, CYAN)}
|
|
85
|
+
{bar}
|
|
86
|
+
|
|
87
|
+
{_c(commit_msg, BOLD, WHITE)}
|
|
88
|
+
|
|
89
|
+
{bar}
|
|
90
|
+
{_c(" 📋 CHANGELOG ENTRY (paste into CHANGELOG.md)", BOLD, CYAN)}
|
|
91
|
+
{bar}
|
|
92
|
+
|
|
93
|
+
{changelog}
|
|
94
|
+
|
|
95
|
+
{bar}
|
|
96
|
+
{_c(" 🗣️ PLAIN ENGLISH SUMMARY", BOLD, CYAN)}
|
|
97
|
+
{bar}
|
|
98
|
+
|
|
99
|
+
{_c(doc.plain_english, YELLOW)}
|
|
100
|
+
|
|
101
|
+
{bar}
|
|
102
|
+
"""
|
|
103
|
+
return output
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def render_markdown_file(doc: CommitDoc, model: str = "gemma4",
|
|
107
|
+
source: str = "", stats: dict = None) -> str:
|
|
108
|
+
"""Returns a rich markdown file with metadata, no heading collision."""
|
|
109
|
+
emoji = _TYPE_EMOJI.get(doc.type, "📌")
|
|
110
|
+
commit_msg = render_commit_message(doc)
|
|
111
|
+
today = date.today().isoformat()
|
|
112
|
+
section = "⚠️ Breaking Changes" if doc.breaking else _SECTION.get(doc.type, "Changed")
|
|
113
|
+
scope_str = f"({doc.scope})" if doc.scope else ""
|
|
114
|
+
|
|
115
|
+
# Files changed block
|
|
116
|
+
files_block = ""
|
|
117
|
+
if stats and stats.get("files"):
|
|
118
|
+
files_list = "\n".join(f"- `{f}`" for f in stats["files"])
|
|
119
|
+
files_block = f"""## 📂 Files Changed
|
|
120
|
+
|
|
121
|
+
{files_list}
|
|
122
|
+
|
|
123
|
+
**+{stats.get('additions', 0)} additions / -{stats.get('deletions', 0)} deletions**
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
source_block = f"> **Source:** {source}\n\n" if source else ""
|
|
130
|
+
|
|
131
|
+
body_block = f"\n> {doc.body}\n" if doc.body else ""
|
|
132
|
+
|
|
133
|
+
return f"""# {emoji} `{doc.type}{scope_str}`: {doc.subject}
|
|
134
|
+
|
|
135
|
+
> Generated by **git-to-doc** using `{model}` on {today}
|
|
136
|
+
{source_block}
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🗣️ Plain English Summary
|
|
140
|
+
|
|
141
|
+
{doc.plain_english}
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## ✅ Conventional Commit Message
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
{commit_msg}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 📋 Changelog Entry
|
|
154
|
+
|
|
155
|
+
### {section}
|
|
156
|
+
|
|
157
|
+
{doc.changelog_entry}
|
|
158
|
+
{body_block}
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
{files_block}## ℹ️ Metadata
|
|
162
|
+
|
|
163
|
+
| Field | Value |
|
|
164
|
+
|-------|-------|
|
|
165
|
+
| Type | `{doc.type}` |
|
|
166
|
+
| Scope | `{doc.scope or "—"}` |
|
|
167
|
+
| Breaking Change | {"⚠️ Yes" if doc.breaking else "No"} |
|
|
168
|
+
| Model | `{model}` |
|
|
169
|
+
| Generated | {today} |
|
|
170
|
+
"""
|
|
171
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-to-doc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate Conventional Commit messages & changelogs from git diffs using local Gemma
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: ollama>=0.4
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Requires-Dist: requests>=2.31
|
|
10
|
+
|
|
11
|
+
# git-to-doc ⚡
|
|
12
|
+
|
|
13
|
+
> Turn any git diff into a Conventional Commit message, changelog, and plain-English PR summary — powered by Gemma running locally via Ollama.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install git-to-doc
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Requires:** [Ollama](https://ollama.com) running locally with a Gemma model pulled:
|
|
22
|
+
```bash
|
|
23
|
+
ollama pull gemma4
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# From a GitHub PR URL
|
|
30
|
+
git-to-doc https://github.com/pallets/flask/pull/5000
|
|
31
|
+
|
|
32
|
+
# From a local .diff file
|
|
33
|
+
git-to-doc sample.diff
|
|
34
|
+
|
|
35
|
+
# Save output as markdown
|
|
36
|
+
git-to-doc https://github.com/pallets/flask/pull/5000 --output md
|
|
37
|
+
|
|
38
|
+
# Save both markdown and JSON
|
|
39
|
+
git-to-doc sample.diff --output both
|
|
40
|
+
|
|
41
|
+
# Use a different model
|
|
42
|
+
git-to-doc sample.diff --model gemma2:2b
|
|
43
|
+
|
|
44
|
+
# Benchmark multiple diffs
|
|
45
|
+
git-to-doc-compare sample.diff https://github.com/pallets/flask/pull/5000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Output
|
|
49
|
+
|
|
50
|
+
For each diff, `git-to-doc` produces:
|
|
51
|
+
|
|
52
|
+
1. **Conventional Commit message** — ready to paste into `git commit`
|
|
53
|
+
2. **Changelog entry** — ready to paste into `CHANGELOG.md`
|
|
54
|
+
3. **Plain-English summary** — for non-technical stakeholders
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
──────────────────────────────────────────────────────────
|
|
58
|
+
✨ CONVENTIONAL COMMIT MESSAGE
|
|
59
|
+
──────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
feat(parser): add null safety and safe_parse wrapper
|
|
62
|
+
|
|
63
|
+
──────────────────────────────────────────────────────────
|
|
64
|
+
📋 CHANGELOG ENTRY
|
|
65
|
+
──────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
## [Unreleased] — 2026-06-26
|
|
68
|
+
### Added
|
|
69
|
+
- feat(parser): add null safety checks and safe_parse fallback
|
|
70
|
+
|
|
71
|
+
──────────────────────────────────────────────────────────
|
|
72
|
+
🗣️ PLAIN ENGLISH SUMMARY
|
|
73
|
+
──────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
The parser now safely handles missing or null input instead of crashing.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Supported Inputs
|
|
79
|
+
|
|
80
|
+
| Input | Example |
|
|
81
|
+
|---|---|
|
|
82
|
+
| GitHub PR URL | `git-to-doc https://github.com/org/repo/pull/123` |
|
|
83
|
+
| Raw diff URL | `git-to-doc https://patch-diff.githubusercontent.com/raw/org/repo/pull/123.diff` |
|
|
84
|
+
| Local `.diff` file | `git-to-doc my_changes.diff` |
|
|
85
|
+
| Folder of diffs | `git-to-doc ./diffs/` |
|
|
86
|
+
|
|
87
|
+
## Flags
|
|
88
|
+
|
|
89
|
+
| Flag | Description |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `--output md` | Save markdown file (auto-named `PR-123-changelog-DATE.md`) |
|
|
92
|
+
| `--output json` | Save JSON file |
|
|
93
|
+
| `--output both` | Save both |
|
|
94
|
+
| `--model NAME` | Ollama model to use (default: `gemma4`) |
|
|
95
|
+
|
|
96
|
+
## Built With
|
|
97
|
+
|
|
98
|
+
- [Ollama](https://ollama.com) — local LLM runtime
|
|
99
|
+
- [Gemma](https://ai.google.dev/gemma) — Google's open model
|
|
100
|
+
- [Pydantic](https://docs.pydantic.dev) — structured output validation
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Built at AI-First Developer Efficiencies Hackathon · Track 1: The Automagic Documenter
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
git_to_doc/__init__.py
|
|
4
|
+
git_to_doc/cli.py
|
|
5
|
+
git_to_doc/compare.py
|
|
6
|
+
git_to_doc/model.py
|
|
7
|
+
git_to_doc/renderer.py
|
|
8
|
+
git_to_doc.egg-info/PKG-INFO
|
|
9
|
+
git_to_doc.egg-info/SOURCES.txt
|
|
10
|
+
git_to_doc.egg-info/dependency_links.txt
|
|
11
|
+
git_to_doc.egg-info/entry_points.txt
|
|
12
|
+
git_to_doc.egg-info/requires.txt
|
|
13
|
+
git_to_doc.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
git_to_doc
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "git-to-doc"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Generate Conventional Commit messages & changelogs from git diffs using local Gemma"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"ollama>=0.4",
|
|
13
|
+
"pydantic>=2.0",
|
|
14
|
+
"requests>=2.31",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
git-to-doc = "git_to_doc.cli:main"
|
|
19
|
+
git-to-doc-compare = "git_to_doc.compare:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["."]
|
|
23
|
+
include = ["git_to_doc*"]
|