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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-to-doc
3
- Version: 0.2.0
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 pr # preview
40
- git-to-doc pr --create # push + open via gh
41
- git-to-doc pr --draft --base develop
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 pr # preview
29
- git-to-doc pr --create # push + open via gh
30
- git-to-doc pr --draft --base develop
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
- #!/usr/bin/env python3
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
- print()
131
- print(_c(f" ⏳ Sending to {model}…", DIM))
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
- print(_c(f" ⚡ Inference done in {time.time() - t0:.1f}s", DIM))
136
- print(render_full_output(result))
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
- stem = f"{diff_file.stem}-changelog-{date.today().isoformat()}"
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
- # ── pr: branch diff → PR title + body, optionally opened via gh ────────────────
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 pr",
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 pr", BOLD, CYAN) + _c(f" {head} → {base} · {args.model}", DIM))
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
- print(_c(" ⏳ Generating pull request…", DIM))
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 <input> --commit-msg", BOLD)}
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 diff --cached | git-to-doc --commit-msg -
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] == "pr":
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
- ("format_compliance", 0.20,
18
- "Commit header matches Conventional Commits, valid type, imperative, "
19
- "no trailing period, <= 72 chars. 5 = perfect, 1 = not conventional."),
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
- ("conciseness", 0.15,
25
- "Terse, free of filler/preamble. 5 = clean, 1 = bloated."),
26
- ("changelog_quality", 0.20,
27
- "Valid markdown, user-facing, accurate. 5 = useful, 1 = missing/wrong."),
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
- raise last_err
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 — be specific
89
- - body: 1-3 sentences of technical detail for non-trivial changes (null if simple)
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 single markdown bullet like "- feat(scope): description" ready to paste into CHANGELOG.md
92
- - plain_english: 1-2 sentences a non-technical person could understand explaining WHAT changed and WHY
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 rich markdown file with metadata, no heading collision."""
142
- emoji = _TYPE_EMOJI.get(doc.type, "📌")
143
- commit_msg = render_commit_message(doc)
144
- today = date.today().isoformat()
145
- section = "⚠️ Breaking Changes" if doc.breaking else _SECTION.get(doc.type, "Changed")
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
- source_block = f"> **Source:** {source}\n\n" if source else ""
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
- body_block = f"\n> {doc.body}\n" if doc.body else ""
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
- return f"""# {emoji} `{doc.type}{scope_str}`: {doc.subject}
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
- ## 🗣️ Plain English Summary
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
- {commit_msg}
232
+ {commit_block}
182
233
  ```
183
234
 
184
- ---
185
-
186
- ## 📋 Changelog Entry
235
+ ## Changelog entry — paste into `CHANGELOG.md`
187
236
 
188
- ### {section}
189
-
190
- {doc.changelog_entry}
191
- {body_block}
192
- ---
237
+ ```markdown
238
+ {changelog_block}
239
+ ```
193
240
 
194
- {files_block}## ℹ️ Metadata
241
+ ## Files
195
242
 
196
- | Field | Value |
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.0
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 pr # preview
40
- git-to-doc pr --create # push + open via gh
41
- git-to-doc pr --draft --base develop
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.0"
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