git-to-doc 0.2.0__tar.gz → 0.2.2__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.2.0 → git_to_doc-0.2.2}/PKG-INFO +14 -7
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/README.md +13 -6
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc/cli.py +51 -43
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc/evaluate.py +9 -10
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc/model.py +19 -9
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc/renderer.py +87 -47
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc.egg-info/PKG-INFO +14 -7
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/pyproject.toml +1 -1
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc/__init__.py +0 -0
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc/compare.py +0 -0
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc/validate.py +0 -0
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc.egg-info/SOURCES.txt +0 -0
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc.egg-info/dependency_links.txt +0 -0
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc.egg-info/entry_points.txt +0 -0
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc.egg-info/requires.txt +0 -0
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/git_to_doc.egg-info/top_level.txt +0 -0
- {git_to_doc-0.2.0 → git_to_doc-0.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-to-doc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Conventional Commit messages, changelogs & PRs from git diffs using Gemma (local or cloud)
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -9,9 +9,11 @@ Requires-Dist: pydantic>=2.0
|
|
|
9
9
|
Requires-Dist: requests>=2.31
|
|
10
10
|
Requires-Dist: python-dotenv>=1.0
|
|
11
11
|
|
|
12
|
-
# git-to-doc
|
|
12
|
+
# git-to-doc ⚡
|
|
13
13
|
|
|
14
|
-
Turn a git diff into developer documentation with a local or cloud **Gemma** model — Conventional Commit messages, markdown changelogs, and pull requests — straight from the terminal.
|
|
14
|
+
> Turn a git diff into developer documentation with a local or cloud **Gemma** model — Conventional Commit messages, markdown changelogs, and pull requests — straight from the terminal.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
15
17
|
|
|
16
18
|
```bash
|
|
17
19
|
pip install git-to-doc
|
|
@@ -33,12 +35,11 @@ Developers write terrible commit messages and skip doc updates. `git-to-doc` is
|
|
|
33
35
|
git-to-doc sample.diff
|
|
34
36
|
git-to-doc https://github.com/pallets/flask/pull/5000 --output both
|
|
35
37
|
git-to-doc ./diffs/ --output md # batch a folder
|
|
36
|
-
cat x.diff | git-to-doc --commit-msg - # print ONLY the commit message
|
|
37
38
|
|
|
38
39
|
# generate (and open) a pull request from the current branch
|
|
39
|
-
git-to-doc
|
|
40
|
-
git-to-doc
|
|
41
|
-
git-to-doc
|
|
40
|
+
git-to-doc pull-request # preview
|
|
41
|
+
git-to-doc pull-request --create # push + open via gh
|
|
42
|
+
git-to-doc pull-request --draft --base develop
|
|
42
43
|
|
|
43
44
|
# install a git hook so `git commit` (no -m) auto-fills the message
|
|
44
45
|
git-to-doc install-hook
|
|
@@ -47,6 +48,12 @@ git-to-doc install-hook
|
|
|
47
48
|
git-to-doc-compare sample.diff --models gemma3:4b gemma3:12b --judge gpt-oss:120b
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
## Output Example
|
|
52
|
+
|
|
53
|
+
For each diff, `git-to-doc` produces a reviewer-first document:
|
|
54
|
+
|
|
55
|
+
📄 **See a real rendered example →** [examples/PR-474.md](examples/PR-474.md) *(GitHub renders the callouts, collapsible sections, and code blocks natively)*
|
|
56
|
+
|
|
50
57
|
## How it's built for trust
|
|
51
58
|
|
|
52
59
|
- **Self-repair loop** — every generated commit is checked against the Conventional Commits spec (`validate.py`); on failure the exact violations are fed back and the model regenerates. Output is guaranteed spec-valid, not hopeful.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# git-to-doc
|
|
1
|
+
# git-to-doc ⚡
|
|
2
2
|
|
|
3
|
-
Turn a git diff into developer documentation with a local or cloud **Gemma** model — Conventional Commit messages, markdown changelogs, and pull requests — straight from the terminal.
|
|
3
|
+
> Turn a git diff into developer documentation with a local or cloud **Gemma** model — Conventional Commit messages, markdown changelogs, and pull requests — straight from the terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
8
|
pip install git-to-doc
|
|
@@ -22,12 +24,11 @@ Developers write terrible commit messages and skip doc updates. `git-to-doc` is
|
|
|
22
24
|
git-to-doc sample.diff
|
|
23
25
|
git-to-doc https://github.com/pallets/flask/pull/5000 --output both
|
|
24
26
|
git-to-doc ./diffs/ --output md # batch a folder
|
|
25
|
-
cat x.diff | git-to-doc --commit-msg - # print ONLY the commit message
|
|
26
27
|
|
|
27
28
|
# generate (and open) a pull request from the current branch
|
|
28
|
-
git-to-doc
|
|
29
|
-
git-to-doc
|
|
30
|
-
git-to-doc
|
|
29
|
+
git-to-doc pull-request # preview
|
|
30
|
+
git-to-doc pull-request --create # push + open via gh
|
|
31
|
+
git-to-doc pull-request --draft --base develop
|
|
31
32
|
|
|
32
33
|
# install a git hook so `git commit` (no -m) auto-fills the message
|
|
33
34
|
git-to-doc install-hook
|
|
@@ -36,6 +37,12 @@ git-to-doc install-hook
|
|
|
36
37
|
git-to-doc-compare sample.diff --models gemma3:4b gemma3:12b --judge gpt-oss:120b
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
## Output Example
|
|
41
|
+
|
|
42
|
+
For each diff, `git-to-doc` produces a reviewer-first document:
|
|
43
|
+
|
|
44
|
+
📄 **See a real rendered example →** [examples/PR-474.md](examples/PR-474.md) *(GitHub renders the callouts, collapsible sections, and code blocks natively)*
|
|
45
|
+
|
|
39
46
|
## How it's built for trust
|
|
40
47
|
|
|
41
48
|
- **Self-repair loop** — every generated commit is checked against the Conventional Commits spec (`validate.py`); on failure the exact violations are fed back and the model regenerates. Output is guaranteed spec-valid, not hopeful.
|
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
"""
|
|
3
|
-
git-to-doc — Conventional Commit messages, changelogs & PRs from git diffs.
|
|
4
|
-
|
|
5
|
-
Commands:
|
|
6
|
-
git-to-doc <input> [--output md|json|both] [--model M] generate commit + changelog
|
|
7
|
-
git-to-doc <input> --commit-msg print ONLY the commit message
|
|
8
|
-
git-to-doc pr [--base B] [--create] [--draft] [--model M] generate / open a pull request
|
|
9
|
-
git-to-doc install-hook auto-fill commit messages via git hook
|
|
10
|
-
|
|
11
|
-
<input> is a GitHub PR URL, '-' for stdin, a local .diff/.txt file, or a folder of .diff files.
|
|
12
|
-
|
|
13
|
-
Examples:
|
|
14
|
-
git-to-doc https://github.com/pallets/flask/pull/5000 --output both
|
|
15
|
-
git-to-doc sample.diff
|
|
16
|
-
git diff --cached | git-to-doc --commit-msg -
|
|
17
|
-
git-to-doc pr --create
|
|
18
|
-
git-to-doc install-hook
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
import sys, re, argparse, time, os, subprocess, tempfile, shutil
|
|
1
|
+
import sys, re, json, argparse, time, os, subprocess, tempfile, shutil, threading, itertools
|
|
22
2
|
from pathlib import Path
|
|
23
3
|
from datetime import date
|
|
24
4
|
from typing import Optional
|
|
@@ -78,13 +58,14 @@ def diff_stats(text: str) -> dict:
|
|
|
78
58
|
return {"files": files, "additions": adds, "deletions": dels}
|
|
79
59
|
|
|
80
60
|
|
|
81
|
-
def auto_stem(input_str: str) -> str:
|
|
61
|
+
def auto_stem(input_str: str, model: str) -> str:
|
|
82
62
|
today = date.today().isoformat()
|
|
63
|
+
clean_model = model.replace(":", "-")
|
|
83
64
|
if is_url(input_str):
|
|
84
65
|
_, slug = normalise_url(input_str)
|
|
85
|
-
return f"{slug}-changelog-{today}"
|
|
66
|
+
return f"{slug}-changelog-{clean_model}-{today}"
|
|
86
67
|
p = Path(input_str)
|
|
87
|
-
return f"{(p.name if p.is_dir() else p.stem)}-changelog-{today}"
|
|
68
|
+
return f"{(p.name if p.is_dir() else p.stem)}-changelog-{clean_model}-{today}"
|
|
88
69
|
|
|
89
70
|
|
|
90
71
|
def _read_source(input_str: str) -> str:
|
|
@@ -127,13 +108,30 @@ def process_diff(diff_text: str, stem: str, model: str,
|
|
|
127
108
|
for f in stats["files"]:
|
|
128
109
|
print(_c(f" • {f}", DIM))
|
|
129
110
|
print(_c(f" +{stats['additions']} additions -{stats['deletions']} deletions", DIM))
|
|
130
|
-
|
|
131
|
-
|
|
111
|
+
done = False
|
|
112
|
+
def spinner():
|
|
113
|
+
for char in itertools.cycle(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']):
|
|
114
|
+
if done:
|
|
115
|
+
break
|
|
116
|
+
sys.stdout.write(f"\r{_c(' ' + char + f' Sending to {model}…', DIM)}")
|
|
117
|
+
sys.stdout.flush()
|
|
118
|
+
time.sleep(0.1)
|
|
132
119
|
|
|
133
120
|
t0 = time.time()
|
|
121
|
+
t = threading.Thread(target=spinner)
|
|
122
|
+
t.start()
|
|
123
|
+
|
|
134
124
|
result = analyze_diff(diff_text, model=model)
|
|
135
|
-
|
|
136
|
-
|
|
125
|
+
|
|
126
|
+
done = True
|
|
127
|
+
t.join()
|
|
128
|
+
sys.stdout.write("\r" + " " * 50 + "\r") # clear spinner line
|
|
129
|
+
|
|
130
|
+
elapsed = time.time() - t0
|
|
131
|
+
print(_c(f" ⚡ Inference done in {elapsed:.1f}s", DIM))
|
|
132
|
+
|
|
133
|
+
if fmt is None:
|
|
134
|
+
print(render_full_output(result))
|
|
137
135
|
|
|
138
136
|
if fmt in ("md", "both"):
|
|
139
137
|
md_path = Path(f"{stem}.md")
|
|
@@ -159,8 +157,7 @@ def cmd_doc(argv):
|
|
|
159
157
|
metavar="{md,json,both}",
|
|
160
158
|
help="Save output: 'md' (default when flag used), 'json', or 'both'.")
|
|
161
159
|
parser.add_argument("--model", default="gemma4", help="Ollama model name (default: gemma4)")
|
|
162
|
-
parser.add_argument("--commit-msg", action="store_true",
|
|
163
|
-
help="Print ONLY the Conventional Commit message (for git hooks / piping)")
|
|
160
|
+
parser.add_argument("--commit-msg", action="store_true", help=argparse.SUPPRESS)
|
|
164
161
|
args = parser.parse_args(argv)
|
|
165
162
|
|
|
166
163
|
if args.commit_msg:
|
|
@@ -184,12 +181,13 @@ def cmd_doc(argv):
|
|
|
184
181
|
print(_c(f" ⚡ Source: folder ({len(diffs)} diff files)", BOLD))
|
|
185
182
|
for diff_file in diffs:
|
|
186
183
|
print(_c(f"\n ── {diff_file.name} ──", BOLD, CYAN))
|
|
187
|
-
|
|
184
|
+
clean_model = args.model.replace(":", "-")
|
|
185
|
+
stem = f"{diff_file.stem}-changelog-{clean_model}-{date.today().isoformat()}"
|
|
188
186
|
process_diff(diff_file.read_text(encoding="utf-8"), stem, args.model, fmt, source=str(diff_file))
|
|
189
187
|
else:
|
|
190
188
|
label = "URL" if is_url(args.input) else ("stdin" if args.input == "-" else Path(args.input).name)
|
|
191
189
|
print(_c(f" ⚡ Source: {label}", BOLD))
|
|
192
|
-
process_diff(_read_source(args.input), auto_stem(args.input), args.model, fmt, source=args.input)
|
|
190
|
+
process_diff(_read_source(args.input), auto_stem(args.input, args.model), args.model, fmt, source=args.input)
|
|
193
191
|
except requests.HTTPError as e:
|
|
194
192
|
code = getattr(e.response, "status_code", "?")
|
|
195
193
|
print(_c(f"\n ✗ Could not fetch diff (HTTP {code}). Check the PR URL is public.", RED)); sys.exit(1)
|
|
@@ -198,10 +196,10 @@ def cmd_doc(argv):
|
|
|
198
196
|
print()
|
|
199
197
|
|
|
200
198
|
|
|
201
|
-
# ──
|
|
199
|
+
# ── pull-request: branch diff → PR title + body, optionally opened via gh ──────
|
|
202
200
|
def cmd_pr(argv):
|
|
203
201
|
parser = argparse.ArgumentParser(
|
|
204
|
-
prog="git-to-doc
|
|
202
|
+
prog="git-to-doc pull-request",
|
|
205
203
|
description="Generate (and optionally open) a pull request from the current branch.")
|
|
206
204
|
parser.add_argument("--base", help="base branch (default: auto-detect main/master)")
|
|
207
205
|
parser.add_argument("--model", default="gemma4", help="Ollama model name (default: gemma4)")
|
|
@@ -226,10 +224,24 @@ def cmd_pr(argv):
|
|
|
226
224
|
if not diff_text.strip():
|
|
227
225
|
print(_c(f" ✗ No changes between {base} and {head}.", RED)); sys.exit(1)
|
|
228
226
|
|
|
229
|
-
print(_c("\n 🔀 git-to-doc
|
|
227
|
+
print(_c("\n 🔀 git-to-doc pull-request", BOLD, CYAN) + _c(f" {head} → {base} · {args.model}", DIM))
|
|
230
228
|
print(_c(" " + "─" * 52, DIM))
|
|
231
|
-
|
|
229
|
+
done = False
|
|
230
|
+
def spinner():
|
|
231
|
+
for char in itertools.cycle(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']):
|
|
232
|
+
if done:
|
|
233
|
+
break
|
|
234
|
+
sys.stdout.write(f"\r{_c(' ' + char + f' Generating pull request…', DIM)}")
|
|
235
|
+
sys.stdout.flush()
|
|
236
|
+
time.sleep(0.1)
|
|
237
|
+
|
|
238
|
+
t = threading.Thread(target=spinner)
|
|
239
|
+
t.start()
|
|
232
240
|
pr = analyze_pr(diff_text, model=args.model, verbose=True)
|
|
241
|
+
done = True
|
|
242
|
+
t.join()
|
|
243
|
+
sys.stdout.write("\r" + " " * 50 + "\r")
|
|
244
|
+
|
|
233
245
|
print(render_pr_full_output(pr))
|
|
234
246
|
|
|
235
247
|
if not (args.create or args.draft):
|
|
@@ -301,10 +313,7 @@ def _top_help():
|
|
|
301
313
|
Generate a Conventional Commit message + changelog + plain-English summary.
|
|
302
314
|
<input> = a GitHub PR URL, '-' for stdin, a .diff/.txt file, or a folder of .diff files.
|
|
303
315
|
|
|
304
|
-
{_c("git-to-doc
|
|
305
|
-
Print ONLY the commit message (for piping / git hooks).
|
|
306
|
-
|
|
307
|
-
{_c("git-to-doc pr", BOLD)} [--base B] [--create] [--draft] [--model M]
|
|
316
|
+
{_c("git-to-doc pull-request", BOLD)} [--base B] [--create] [--draft] [--model M]
|
|
308
317
|
Generate a pull request from the current branch. Preview by default;
|
|
309
318
|
--create pushes the branch and opens the PR via gh.
|
|
310
319
|
|
|
@@ -317,8 +326,7 @@ def _top_help():
|
|
|
317
326
|
{_c(" EXAMPLES", BOLD)}
|
|
318
327
|
git-to-doc sample.diff
|
|
319
328
|
git-to-doc https://github.com/pallets/flask/pull/5000 --output both
|
|
320
|
-
git
|
|
321
|
-
git-to-doc pr --create
|
|
329
|
+
git-to-doc pull-request --create
|
|
322
330
|
|
|
323
331
|
{_c(" Run", DIM)} {_c("git-to-doc <command> --help", BOLD)} {_c("for command-specific options.", DIM)}
|
|
324
332
|
""")
|
|
@@ -330,7 +338,7 @@ def main():
|
|
|
330
338
|
if not argv or argv[0] in ("-h", "--help", "help"):
|
|
331
339
|
_top_help()
|
|
332
340
|
return
|
|
333
|
-
if argv[0]
|
|
341
|
+
if argv[0] in ("pull-request", "pr"):
|
|
334
342
|
return cmd_pr(argv[1:])
|
|
335
343
|
if argv[0] == "install-hook":
|
|
336
344
|
return cmd_install_hook(argv[1:])
|
|
@@ -14,17 +14,16 @@ from git_to_doc.model import _client, _resolve_model
|
|
|
14
14
|
DEFAULT_JUDGE = "gpt-oss:120b"
|
|
15
15
|
|
|
16
16
|
RUBRIC = [
|
|
17
|
-
("
|
|
18
|
-
"Commit header matches Conventional Commits, valid type, imperative, "
|
|
19
|
-
|
|
20
|
-
("type_accuracy", 0.15,
|
|
21
|
-
"type/scope correctly reflect the change. 5 = right, 1 = wrong."),
|
|
22
|
-
("semantic_accuracy", 0.30,
|
|
17
|
+
("conventional_commit", 0.15,
|
|
18
|
+
"Commit header matches Conventional Commits, valid type/scope, imperative mood. 5 = perfect, 1 = invalid."),
|
|
19
|
+
("semantic_accuracy", 0.25,
|
|
23
20
|
"Truthfully describes the diff, no hallucination/omission. 5 = faithful, 1 = fabricated."),
|
|
24
|
-
("
|
|
25
|
-
"
|
|
26
|
-
("
|
|
27
|
-
"
|
|
21
|
+
("changelog_quality", 0.15,
|
|
22
|
+
"Valid markdown, user-facing, accurate changelog entry. 5 = useful, 1 = missing/wrong."),
|
|
23
|
+
("plain_english_quality", 0.25,
|
|
24
|
+
"The 'What changed' section explains the change clearly to a human reader, and file notes accurately summarize file changes. 5 = excellent, 1 = confusing/absent."),
|
|
25
|
+
("reviewer_utility", 0.20,
|
|
26
|
+
"The review notes highlight tricky parts, design decisions, and are highly actionable for reviewers. 5 = highly useful, 1 = generic/empty."),
|
|
28
27
|
]
|
|
29
28
|
|
|
30
29
|
|
|
@@ -63,7 +63,10 @@ def _parse(raw: str, Model):
|
|
|
63
63
|
return Model.model_validate_json(c)
|
|
64
64
|
except ValidationError as e:
|
|
65
65
|
last_err = e
|
|
66
|
-
|
|
66
|
+
|
|
67
|
+
if last_err is not None:
|
|
68
|
+
raise last_err
|
|
69
|
+
raise ValueError("No valid candidates found for parsing.")
|
|
67
70
|
|
|
68
71
|
|
|
69
72
|
# ── Commit document ───────────────────────────────────────────────────────────
|
|
@@ -75,24 +78,28 @@ class CommitDoc(BaseModel):
|
|
|
75
78
|
breaking: bool
|
|
76
79
|
changelog_entry: str
|
|
77
80
|
plain_english: str
|
|
81
|
+
human_title: str
|
|
82
|
+
review_notes: str
|
|
83
|
+
file_notes: dict[str, str]
|
|
78
84
|
|
|
79
85
|
|
|
80
86
|
SYSTEM_PROMPT = """
|
|
81
|
-
|
|
82
|
-
You are an expert Git commit message generator and technical writer.
|
|
87
|
+
You are a senior developer and expert technical writer.
|
|
83
88
|
Given a raw git diff, output ONLY a valid JSON object with no explanation, no markdown fences, no preamble.
|
|
84
89
|
|
|
85
90
|
Rules for each field:
|
|
86
91
|
- type: one of feat|fix|docs|refactor|perf|test|chore|ci|build|revert
|
|
87
92
|
- scope: lowercase name of the module or folder most affected (null if unclear)
|
|
88
|
-
- subject: imperative mood, lowercase, no trailing period, max 72 chars —
|
|
89
|
-
- body:
|
|
93
|
+
- subject: imperative mood, lowercase, no trailing period, max 72 chars. Write it like a human developer would — natural and specific, not robotic.
|
|
94
|
+
- body: 2-4 sentences of precise technical detail explaining WHAT was changed and WHY. Reference specific function names, file paths, or patterns you see in the diff. Never be vague. null if truly trivial.
|
|
90
95
|
- breaking: true ONLY if existing public API contracts are removed or changed incompatibly
|
|
91
|
-
- changelog_entry: a
|
|
92
|
-
- plain_english:
|
|
96
|
+
- changelog_entry: a user-facing markdown bullet starting with "- " that summarizes the change for a CHANGELOG.md. Be specific and helpful — mention the feature, fix, or improvement by name. For large changes, use multiple sub-bullets with " - " to break down the key items.
|
|
97
|
+
- plain_english: 3-5 sentences explaining this change to a developer who hasn't seen the code. Cover: (1) what the code did before, (2) what it does now, and (3) why this matters. Use concrete language, not vague summaries. Reference module names and behaviors.
|
|
98
|
+
- human_title: A clear, specific title for the document. Bad: "Code Update". Good: "Refactor: Simplified checkpoint loading to return checkpointer objects directly". The title should tell someone exactly what happened without opening the doc.
|
|
99
|
+
- review_notes: 2-3 paragraphs for code reviewers. Paragraph 1: the overall approach and key design decisions. Paragraph 2: specific areas that need careful review (e.g., edge cases, error handling, concurrency). Paragraph 3 (optional): suggestions for follow-up work or things that were intentionally left out.
|
|
100
|
+
- file_notes: A dictionary mapping up to 10 of the MOST IMPORTANT changed file paths to a 1-sentence summary of what changed in each. Focus on files where the logic actually changed, not config or boilerplate. If the diff has more than 10 files, pick the ones with the most substantive changes.
|
|
93
101
|
|
|
94
102
|
Output format: raw JSON only. No ```json fences. No commentary.
|
|
95
|
-
|
|
96
103
|
"""
|
|
97
104
|
|
|
98
105
|
|
|
@@ -138,8 +145,11 @@ def analyze_diff(diff_text: str, model: str = "gemma4", max_retries: int = 3,
|
|
|
138
145
|
# Fallback — never crash during a demo.
|
|
139
146
|
return CommitDoc(
|
|
140
147
|
type="chore", scope=None, subject="update codebase", body=None,
|
|
141
|
-
breaking=False, changelog_entry="- chore: miscellaneous updates",
|
|
148
|
+
breaking=False, changelog_entry="- chore: miscellaneous codebase updates",
|
|
142
149
|
plain_english="General codebase updates were made.",
|
|
150
|
+
human_title="Maintenance: Codebase Update",
|
|
151
|
+
review_notes="This is a general maintenance update. No specific review areas identified.",
|
|
152
|
+
file_notes={}
|
|
143
153
|
)
|
|
144
154
|
|
|
145
155
|
|
|
@@ -138,67 +138,107 @@ def render_pr_full_output(doc: PRDoc) -> str:
|
|
|
138
138
|
|
|
139
139
|
def render_markdown_file(doc: CommitDoc, model: str = "gemma4",
|
|
140
140
|
source: str = "", stats: dict = None) -> str:
|
|
141
|
-
"""Returns a
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
today
|
|
145
|
-
|
|
146
|
-
scope_str = f"({doc.scope})" if doc.scope else ""
|
|
147
|
-
|
|
148
|
-
# Files changed block
|
|
149
|
-
files_block = ""
|
|
150
|
-
if stats and stats.get("files"):
|
|
151
|
-
files_list = "\n".join(f"- `{f}`" for f in stats["files"])
|
|
152
|
-
files_block = f"""## 📂 Files Changed
|
|
153
|
-
|
|
154
|
-
{files_list}
|
|
155
|
-
|
|
156
|
-
**+{stats.get('additions', 0)} additions / -{stats.get('deletions', 0)} deletions**
|
|
157
|
-
|
|
158
|
-
---
|
|
141
|
+
"""Returns a reviewer-first markdown doc: merge-safety callout, clean
|
|
142
|
+
copy-paste blocks, and collapsible files/metadata sections."""
|
|
143
|
+
emoji = _TYPE_EMOJI.get(doc.type, "📌")
|
|
144
|
+
today = date.today().isoformat()
|
|
145
|
+
header = doc.human_title
|
|
159
146
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
147
|
+
# 1. Merge-safety callout — the first thing a reviewer needs
|
|
148
|
+
if doc.breaking:
|
|
149
|
+
callout = ("> [!WARNING]\n"
|
|
150
|
+
"> **Breaking change** — review before merging. "
|
|
151
|
+
"Existing API behavior changes.")
|
|
152
|
+
else:
|
|
153
|
+
callout = "> [!NOTE]\n> **Non-breaking change** — safe to merge."
|
|
154
|
+
|
|
155
|
+
# 2. One-line at-a-glance strip
|
|
156
|
+
n_files = len(stats["files"]) if stats and stats.get("files") else 0
|
|
157
|
+
diffstat = f" · +{stats.get('additions', 0)} / -{stats.get('deletions', 0)}" if stats else ""
|
|
158
|
+
stat_line = (f"`{doc.type}` · scope `{doc.scope or '—'}` · "
|
|
159
|
+
f"{n_files} file(s) changed{diffstat} · via git-to-doc + `{model}`")
|
|
160
|
+
|
|
161
|
+
# 3. CLEAN copy-paste commit block (nothing decorative inside)
|
|
162
|
+
scope = f"({doc.scope})" if doc.scope else ""
|
|
163
|
+
commit_block = f"{doc.type}{scope}{'!' if doc.breaking else ''}: {doc.subject}"
|
|
164
|
+
if doc.body:
|
|
165
|
+
commit_block += f"\n\n{doc.body}"
|
|
166
|
+
if doc.breaking:
|
|
167
|
+
commit_block += f"\n\nBREAKING CHANGE: {doc.body or doc.subject}"
|
|
163
168
|
|
|
164
|
-
|
|
169
|
+
# 4. Changelog snippet = section bucket + bullet (paste under Unreleased)
|
|
170
|
+
section = _SECTION.get(doc.type, "Changed")
|
|
171
|
+
changelog_block = f"### {section}\n{doc.changelog_entry}"
|
|
165
172
|
|
|
166
|
-
|
|
173
|
+
# 5. Files section — key files with notes shown prominently, rest collapsed
|
|
174
|
+
files_section = ""
|
|
175
|
+
if stats and stats.get("files"):
|
|
176
|
+
noted_lines = []
|
|
177
|
+
other_lines = []
|
|
178
|
+
for f in stats["files"]:
|
|
179
|
+
note = doc.file_notes.get(f)
|
|
180
|
+
if note:
|
|
181
|
+
noted_lines.append(f"| `{f}` | {note} |")
|
|
182
|
+
else:
|
|
183
|
+
other_lines.append(f"- `{f}`")
|
|
184
|
+
|
|
185
|
+
parts = []
|
|
186
|
+
if noted_lines:
|
|
187
|
+
parts.append("### Key files\n")
|
|
188
|
+
parts.append("| File | Change |")
|
|
189
|
+
parts.append("|------|--------|")
|
|
190
|
+
parts.extend(noted_lines)
|
|
191
|
+
parts.append("")
|
|
192
|
+
|
|
193
|
+
if other_lines:
|
|
194
|
+
other_text = "\n".join(other_lines)
|
|
195
|
+
parts.append(f"<details>\n<summary>Other files changed ({len(other_lines)})</summary>\n")
|
|
196
|
+
parts.append(other_text)
|
|
197
|
+
parts.append("\n</details>\n")
|
|
198
|
+
|
|
199
|
+
if parts:
|
|
200
|
+
files_section = "\n".join(parts) + "\n"
|
|
201
|
+
|
|
202
|
+
# 6. Review notes
|
|
203
|
+
review_section = ""
|
|
204
|
+
if doc.review_notes:
|
|
205
|
+
review_section = f"## Review notes\n\n{doc.review_notes}\n\n"
|
|
206
|
+
|
|
207
|
+
# 7. Metadata (always collapsed)
|
|
208
|
+
src_row = f"| Source | {source} |\n" if source else ""
|
|
209
|
+
meta_section = (f"<details>\n<summary>Metadata</summary>\n\n"
|
|
210
|
+
f"| Field | Value |\n|-------|-------|\n"
|
|
211
|
+
f"| Type | `{doc.type}` |\n"
|
|
212
|
+
f"| Scope | `{doc.scope or '—'}` |\n"
|
|
213
|
+
f"| Breaking | {'⚠️ Yes' if doc.breaking else 'No'} |\n"
|
|
214
|
+
f"| Model | `{model}` |\n{src_row}"
|
|
215
|
+
f"| Generated | {today} |\n\n</details>\n")
|
|
216
|
+
|
|
217
|
+
return f"""# {emoji} {header}
|
|
218
|
+
|
|
219
|
+
{callout}
|
|
220
|
+
|
|
221
|
+
{stat_line}
|
|
167
222
|
|
|
168
|
-
> Generated by **git-to-doc** using `{model}` on {today}
|
|
169
|
-
{source_block}
|
|
170
223
|
---
|
|
171
224
|
|
|
172
|
-
##
|
|
225
|
+
## What changed
|
|
173
226
|
|
|
174
227
|
{doc.plain_english}
|
|
175
228
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
## ✅ Conventional Commit Message
|
|
229
|
+
{review_section}## Commit message
|
|
179
230
|
|
|
180
231
|
```
|
|
181
|
-
{
|
|
232
|
+
{commit_block}
|
|
182
233
|
```
|
|
183
234
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
## 📋 Changelog Entry
|
|
235
|
+
## Changelog entry — paste into `CHANGELOG.md`
|
|
187
236
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
{body_block}
|
|
192
|
-
---
|
|
237
|
+
```markdown
|
|
238
|
+
{changelog_block}
|
|
239
|
+
```
|
|
193
240
|
|
|
194
|
-
|
|
241
|
+
## Files
|
|
195
242
|
|
|
196
|
-
|
|
197
|
-
|-------|-------|
|
|
198
|
-
| Type | `{doc.type}` |
|
|
199
|
-
| Scope | `{doc.scope or "—"}` |
|
|
200
|
-
| Breaking Change | {"⚠️ Yes" if doc.breaking else "No"} |
|
|
201
|
-
| Model | `{model}` |
|
|
202
|
-
| Generated | {today} |
|
|
203
|
-
"""
|
|
243
|
+
{files_section}{meta_section}"""
|
|
204
244
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-to-doc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Conventional Commit messages, changelogs & PRs from git diffs using Gemma (local or cloud)
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -9,9 +9,11 @@ Requires-Dist: pydantic>=2.0
|
|
|
9
9
|
Requires-Dist: requests>=2.31
|
|
10
10
|
Requires-Dist: python-dotenv>=1.0
|
|
11
11
|
|
|
12
|
-
# git-to-doc
|
|
12
|
+
# git-to-doc ⚡
|
|
13
13
|
|
|
14
|
-
Turn a git diff into developer documentation with a local or cloud **Gemma** model — Conventional Commit messages, markdown changelogs, and pull requests — straight from the terminal.
|
|
14
|
+
> Turn a git diff into developer documentation with a local or cloud **Gemma** model — Conventional Commit messages, markdown changelogs, and pull requests — straight from the terminal.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
15
17
|
|
|
16
18
|
```bash
|
|
17
19
|
pip install git-to-doc
|
|
@@ -33,12 +35,11 @@ Developers write terrible commit messages and skip doc updates. `git-to-doc` is
|
|
|
33
35
|
git-to-doc sample.diff
|
|
34
36
|
git-to-doc https://github.com/pallets/flask/pull/5000 --output both
|
|
35
37
|
git-to-doc ./diffs/ --output md # batch a folder
|
|
36
|
-
cat x.diff | git-to-doc --commit-msg - # print ONLY the commit message
|
|
37
38
|
|
|
38
39
|
# generate (and open) a pull request from the current branch
|
|
39
|
-
git-to-doc
|
|
40
|
-
git-to-doc
|
|
41
|
-
git-to-doc
|
|
40
|
+
git-to-doc pull-request # preview
|
|
41
|
+
git-to-doc pull-request --create # push + open via gh
|
|
42
|
+
git-to-doc pull-request --draft --base develop
|
|
42
43
|
|
|
43
44
|
# install a git hook so `git commit` (no -m) auto-fills the message
|
|
44
45
|
git-to-doc install-hook
|
|
@@ -47,6 +48,12 @@ git-to-doc install-hook
|
|
|
47
48
|
git-to-doc-compare sample.diff --models gemma3:4b gemma3:12b --judge gpt-oss:120b
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
## Output Example
|
|
52
|
+
|
|
53
|
+
For each diff, `git-to-doc` produces a reviewer-first document:
|
|
54
|
+
|
|
55
|
+
📄 **See a real rendered example →** [examples/PR-474.md](examples/PR-474.md) *(GitHub renders the callouts, collapsible sections, and code blocks natively)*
|
|
56
|
+
|
|
50
57
|
## How it's built for trust
|
|
51
58
|
|
|
52
59
|
- **Self-repair loop** — every generated commit is checked against the Conventional Commits spec (`validate.py`); on failure the exact violations are fed back and the model regenerates. Output is guaranteed spec-valid, not hopeful.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "git-to-doc"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Conventional Commit messages, changelogs & PRs from git diffs using Gemma (local or cloud)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|