git-history-extraction 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,189 @@
1
+ import subprocess
2
+ import re
3
+ from pathlib import Path
4
+ import click
5
+
6
+
7
+ def get_commit_files(sha: str, repo_path: Path | None = None) -> list[str]:
8
+ cmd = ["git", "diff-tree", "--no-commit-id", "--name-only", "-r", sha]
9
+ result = subprocess.run(
10
+ cmd,
11
+ capture_output=True,
12
+ text=True,
13
+ check=True,
14
+ cwd=str(repo_path) if repo_path else None,
15
+ )
16
+ return [l.strip() for l in result.stdout.splitlines() if l.strip()]
17
+
18
+
19
+ def get_git_commits(since, since_commit=None, repo_path: Path | None = None):
20
+ rec_sep = "\x1e"
21
+ fld_sep = "\x1f"
22
+ end_hdr = "\x1d"
23
+
24
+ pretty = f"{rec_sep}%H{fld_sep}%cI{fld_sep}%B{end_hdr}"
25
+ if since_commit:
26
+ cmd = [
27
+ "git",
28
+ "log",
29
+ f"{since_commit}..HEAD",
30
+ f"--pretty=format:{pretty}",
31
+ "--name-only",
32
+ ]
33
+ else:
34
+ cmd = [
35
+ "git",
36
+ "log",
37
+ f"--since={since}",
38
+ f"--pretty=format:{pretty}",
39
+ "--name-only",
40
+ ]
41
+
42
+ result = subprocess.run(
43
+ cmd,
44
+ capture_output=True,
45
+ text=True,
46
+ check=True,
47
+ cwd=str(repo_path) if repo_path else None,
48
+ )
49
+
50
+ commits: list[dict] = []
51
+ stream = result.stdout
52
+ for block in stream.split(rec_sep):
53
+ if not block.strip():
54
+ continue
55
+ if end_hdr not in block:
56
+ continue
57
+ header, files_blob = block.split(end_hdr, 1)
58
+ parts = header.split(fld_sep, 2)
59
+ if len(parts) != 3:
60
+ continue
61
+ sha, date_iso, body = parts
62
+ files = [l.strip() for l in files_blob.splitlines() if l.strip()]
63
+ commits.append(
64
+ {
65
+ "sha": sha.strip(),
66
+ "date": date_iso.strip(),
67
+ "body": body.strip(),
68
+ "files": files,
69
+ }
70
+ )
71
+
72
+ return commits
73
+
74
+
75
+ def remove_git_trailers(commit_body: str) -> str:
76
+ lines = commit_body.splitlines()
77
+ trailer_regex = re.compile(r"^\s*[-*]?\s*[^:]+:\s*.*$")
78
+
79
+ while lines and not lines[-1].strip():
80
+ lines.pop()
81
+
82
+ while lines and trailer_regex.match(lines[-1]):
83
+ lines.pop()
84
+
85
+ while lines and not lines[-1].strip():
86
+ lines.pop()
87
+
88
+ return "\n".join(lines)
89
+
90
+
91
+ def extract_git_trailers(commit_body: str) -> list[tuple[str, str]]:
92
+ lines = commit_body.splitlines()
93
+ trailer_regex = re.compile(r"^\s*[-*]?\s*([^:]+):\s*(.*)$")
94
+
95
+ idx = len(lines) - 1
96
+ while idx >= 0 and not lines[idx].strip():
97
+ idx -= 1
98
+
99
+ collected: list[tuple[str, str]] = []
100
+ seen_keys: set[tuple[str, str]] = set()
101
+ j = idx
102
+ while j >= 0:
103
+ m = trailer_regex.match(lines[j])
104
+ if not m:
105
+ break
106
+ pair = (m.group(1), m.group(2))
107
+ collected.append(pair)
108
+ seen_keys.add((m.group(1).strip().lower(), m.group(2).strip()))
109
+ j -= 1
110
+ collected.reverse()
111
+
112
+ for line in lines:
113
+ m = trailer_regex.match(line)
114
+ if not m:
115
+ continue
116
+ key_norm = (m.group(1).strip().lower(), m.group(2).strip())
117
+ if key_norm in seen_keys:
118
+ continue
119
+ collected.append((m.group(1), m.group(2)))
120
+ seen_keys.add(key_norm)
121
+
122
+ return collected
123
+
124
+
125
+ @click.command()
126
+ @click.option(
127
+ "--since",
128
+ type=str,
129
+ default="24 hours ago",
130
+ help="ISO date/time or relative time (default: '24 hours ago')",
131
+ )
132
+ @click.option(
133
+ "--since-commit",
134
+ type=str,
135
+ default=None,
136
+ help="Specific commit sha to start from (e.g. abc123). Overrides --since if provided.",
137
+ )
138
+ @click.option(
139
+ "--repo",
140
+ type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
141
+ default=Path("."),
142
+ help="Path to the git repository to summarize.",
143
+ )
144
+ @click.option(
145
+ "--trailers",
146
+ type=str,
147
+ default=None,
148
+ help="Comma-separated trailer key(s) to output (case-insensitive).",
149
+ )
150
+ @click.option(
151
+ "--format",
152
+ type=click.Choice(["simple", "json"]),
153
+ default="simple",
154
+ help="Output format (default: simple)",
155
+ )
156
+ def main(since: str, since_commit: str | None, repo: Path, trailers: str | None, format: str):
157
+ commits = get_git_commits(since, since_commit, repo_path=repo)
158
+ if not commits:
159
+ click.echo("No commits found using the specified parameters.")
160
+ return
161
+
162
+ if trailers is not None:
163
+ selectors = {part.strip().lower() for part in trailers.split(",") if part.strip()}
164
+ out_lines: list[str] = []
165
+ for c in commits:
166
+ trailer_items = extract_git_trailers(c["body"]) or []
167
+ if selectors:
168
+ trailer_items = [t for t in trailer_items if t[0].lower() in selectors]
169
+ if not trailer_items:
170
+ continue
171
+ files = ", ".join(c.get("files", []))
172
+ out_lines.append(f"Commit: {c['sha']}\nDate: {c['date']}\nFiles: {files}")
173
+ out_lines.extend([f"{k}: {v}" for k, v in trailer_items])
174
+ out_lines.append("")
175
+ click.echo("\n".join(out_lines).rstrip())
176
+ return
177
+
178
+ if format == "json":
179
+ import json
180
+ click.echo(json.dumps(commits, indent=2))
181
+ else:
182
+ for c in commits:
183
+ body = remove_git_trailers(c["body"]) or "(no message)"
184
+ files = ", ".join(c.get("files", []))
185
+ click.echo(f"Commit: {c['sha']}")
186
+ click.echo(f"Date: {c['date']}")
187
+ click.echo(f"Files: {files}")
188
+ click.echo(f"\n{body}\n")
189
+ click.echo("-" * 80)
@@ -0,0 +1,259 @@
1
+ Metadata-Version: 2.3
2
+ Name: git-history-extraction
3
+ Version: 0.1.0
4
+ Summary: Extract and analyze git commit history with support for filtering by trailers, date ranges, and AI-powered summarization for changelogs
5
+ Keywords: git,changelog,history,commit,trailers,ai,summarization,openai,gemini
6
+ Author: Michael Bianco
7
+ Author-email: Michael Bianco <mike@mikebian.co>
8
+ Requires-Dist: click>=8.0
9
+ Requires-Python: >=3.10
10
+ Project-URL: Repository, https://github.com/iloveitaly/git-history-extraction
11
+ Description-Content-Type: text/markdown
12
+
13
+ # git-history-extraction
14
+
15
+ A tool to extract and filter git commit history, making it easy to pipe to AI tools for changelog generation and summaries.
16
+
17
+ ## Features
18
+
19
+ - Extract git commits with metadata (SHA, date, files, message)
20
+ - Filter commits by time range or starting commit
21
+ - Extract and filter git trailers (e.g., `Co-authored-by`, `User-Facing`)
22
+ - Output in simple text or JSON format
23
+ - Pipe output to AI tools (OpenAI, Gemini, Claude) for automated summarization
24
+
25
+ ## Installation
26
+
27
+ ### Using uv (Recommended)
28
+
29
+ No installation needed! The tool can be run directly:
30
+
31
+ ```bash
32
+ uv run git-history-extraction --help
33
+ ```
34
+
35
+ ### Install as Package
36
+
37
+ ```bash
38
+ pip install git-history-extraction
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ### Basic Examples
44
+
45
+ Extract commits from the last 24 hours (default):
46
+ ```bash
47
+ git-history-extraction
48
+ ```
49
+
50
+ Extract commits from the last 7 days:
51
+ ```bash
52
+ git-history-extraction --since "7 days ago"
53
+ ```
54
+
55
+ Extract commits from a specific repository:
56
+ ```bash
57
+ git-history-extraction --repo /path/to/repo --since "1 week ago"
58
+ ```
59
+
60
+ ### Output Formats
61
+
62
+ Simple text format (default):
63
+ ```bash
64
+ git-history-extraction --since "1 day ago"
65
+ ```
66
+
67
+ JSON format for piping to other tools:
68
+ ```bash
69
+ git-history-extraction --since "1 day ago" --format json
70
+ ```
71
+
72
+ ### Commit Range Selection
73
+
74
+ By time range:
75
+ ```bash
76
+ git-history-extraction --since "2024-01-01"
77
+ ```
78
+
79
+ From a specific commit to HEAD:
80
+ ```bash
81
+ git-history-extraction --since-commit abc1234
82
+ ```
83
+
84
+ ### Git Trailers
85
+
86
+ Extract specific trailers only (case-insensitive):
87
+ ```bash
88
+ git-history-extraction --since "1 week ago" --trailers "co-authored-by,reviewed-by"
89
+ ```
90
+
91
+ ## Piping to AI Tools for Summarization
92
+
93
+ This tool extracts and formats git history, making it easy to pipe to AI tools for summarization. The tool itself **does not perform AI summarization**—it prepares the data so you can use your preferred AI tool.
94
+
95
+ The tool enables you to extract targeted slices of git history for different audiences. For example, use git trailers like `User-Facing:` to mark end-user changes, then extract and pipe them to AI for changelogs or internal notifications.
96
+
97
+ ### Creating Structured Git Trailers with AI
98
+
99
+ Combine with [aiautocommit](https://github.com/iloveitaly/aiautocommit) to automatically generate git trailers during commits. This creates a structured history that can be easily filtered and summarized for different audiences.
100
+
101
+ **Example custom commit prompt for aiautocommit:**
102
+
103
+ ```markdown
104
+ # IMPORTANT: Your Instructions
105
+
106
+ You are an expert software developer. Generate a commit message from the `git diff` output below using these rules:
107
+
108
+ ## 1. Subject Line
109
+
110
+ - Use a conventional commit prefix:
111
+ - `feat`: New features
112
+ - `fix`: Bug fixes, including user-visible design or style fixes.
113
+ - `docs`: Changes only to internal documentation (e.g., `.md`, `.rst`) or code comments.
114
+ - `style`: Formatting, linting, or code style changes in code files.
115
+ - `refactor`: Code structure improvements (no behavior changes).
116
+ - `build`: Updates to build scripts or configs (e.g., `Makefile`, `Justfile`, `Dockerfile`, `package.json`).
117
+ - `deploy`: Deployment script or IAC updates.
118
+ - `test`: Changes to tests
119
+ - Add optional scope in parentheses when changes affect a specific module (e.g., `feat(auth): add login`)
120
+ - Limit to 50 characters after the prefix and scope.
121
+ - Use imperative mood (e.g., "improve layout").
122
+ - Describe the intent or outcome (e.g., "prevent text overflow" not "add break-all").
123
+ - Be specific about the change ("validate email format" not "improve validation").
124
+
125
+ ## 2. Extended Commit Message
126
+
127
+ - Include only if changes have non-obvious implications, fix complex bugs, or introduce breaking changes.
128
+ - Separate from subject with one blank line.
129
+ - Use markdown bullets focusing on **why** the change was needed and **what impact** it has.
130
+ - Mention side effects, user impact, or important trade-offs.
131
+
132
+ ## 3. User-facing Changes
133
+
134
+ If the change is something that a end-user (not internal admin!) would see, include a `User-facing:` git trailer with a sentence
135
+ or two explaining, to the user, what they would see differently because of this change.
136
+
137
+ ## 4. General Guidelines
138
+
139
+ - Prioritize the purpose of changes over listing tools or properties used.
140
+ - Keep concise; avoid obvious or verbose details.
141
+ - Always generate a message based on the diff, even with limited context.
142
+
143
+ ## 5. Scopes
144
+
145
+ Optional scopes (e.g., `feat(api):`):
146
+
147
+ - `match`: frontend or backend changes tied
148
+ - `site`: content, additional pages, etc for the static site content
149
+ - `internal-admin`: internal admin changes (including CMS)
150
+ ```
151
+
152
+ With this setup, commits automatically get `User-facing:` trailers. You can then extract and summarize them:
153
+
154
+ ```bash
155
+ # Extract only user-facing changes from the last sprint
156
+ git-history-extraction --since "last monday" --trailers "User-facing" | \
157
+ gemini -i "Create a user-friendly changelog from these changes"
158
+ ```
159
+
160
+ ### Using with Gemini CLI
161
+
162
+ Extract user-facing changes and generate a non-technical summary:
163
+ ```bash
164
+ git-history-extraction --repo . --since "last monday" \
165
+ --trailers "User-Facing" | \
166
+ gemini -i "This is a compressed git history identifying user-facing changes. \
167
+ Can you write a 1-2 sentence overview of the changes, with a list of bullets \
168
+ identifying changes. This is for a non-technical internal audience, letting \
169
+ them know what the development team has done. Separate into 'new' and 'fixed' \
170
+ sections. Include a 'Updates Since' with the date of the first commit in the \
171
+ history. Remove fluff, keep it concise and information dense."
172
+ ```
173
+
174
+ ### Using the OpenAI Playground Script (Optional)
175
+
176
+ An optional playground script is included that demonstrates OpenAI integration:
177
+
178
+ ```bash
179
+ # Generate summary with OpenAI
180
+ git-history-extraction --since "1 week ago" --format json | \
181
+ uv run playground/summarize_commits.py
182
+
183
+ # Preview the prompt without calling OpenAI
184
+ git-history-extraction --since "1 week ago" --format json | \
185
+ uv run playground/summarize_commits.py --dump-prompt
186
+ ```
187
+
188
+ **Requirements:**
189
+ - `OPENAI_API_KEY` environment variable
190
+ - The script uses GPT-4o-mini by default
191
+
192
+ This is just an example—you can pipe to any AI tool you prefer. See [playground/README.md](playground/README.md) for more details.
193
+
194
+ ## Output Format
195
+
196
+ ### Simple Format
197
+
198
+ Each commit is displayed with:
199
+ - **Commit:** SHA hash
200
+ - **Date:** ISO 8601 timestamp
201
+ - **Files:** Comma-separated list of changed files
202
+ - **Message:** Commit body with trailers removed
203
+
204
+ ### JSON Format
205
+
206
+ Array of commit objects:
207
+ ```json
208
+ [
209
+ {
210
+ "sha": "abc123...",
211
+ "date": "2024-10-31T08:00:00-06:00",
212
+ "body": "commit message with trailers",
213
+ "files": ["file1.py", "file2.md"]
214
+ }
215
+ ]
216
+ ```
217
+
218
+ ## Options
219
+
220
+ | Option | Description | Default |
221
+ |--------|-------------|---------|
222
+ | `--since TEXT` | ISO date/time or relative time | `"24 hours ago"` |
223
+ | `--since-commit TEXT` | Start from specific commit (overrides `--since`) | None |
224
+ | `--repo DIRECTORY` | Path to git repository | `.` (current directory) |
225
+ | `--trailers TEXT` | Comma-separated trailer keys to extract | None (show all) |
226
+ | `--format [simple\|json]` | Output format | `simple` |
227
+
228
+ ## How It Works
229
+
230
+ - Uses `git log` with custom formatting for efficient single-pass extraction
231
+ - Parses commit metadata, body, and file changes in one command
232
+ - Intelligently extracts git trailers from commit messages
233
+ - No per-commit subprocess calls for optimal performance
234
+
235
+ ## Development
236
+
237
+ The main logic lives in `git_history_extraction/__init__.py`. The tool is structured as a Python package with:
238
+
239
+ - **main.py:** Entry point script
240
+ - **git_history_extraction/:** Core module with extraction logic
241
+ - **playground/:** Optional AI summarization scripts
242
+
243
+ ### Running Tests
244
+
245
+ ```bash
246
+ pytest
247
+ ```
248
+
249
+ ## Limitations
250
+
251
+ - Large commit ranges may generate significant output; consider narrowing the time range
252
+ - This tool extracts and formats data only—AI summarization requires external tools
253
+ - Git must be available in PATH
254
+
255
+ ## Requirements
256
+
257
+ - Python >= 3.9
258
+ - git
259
+ - [uv](https://docs.astral.sh/uv/) (recommended) or pip
@@ -0,0 +1,5 @@
1
+ git_history_extraction/__init__.py,sha256=iLgnikniMeicx5Ogh7bQ_dPGNT8t07kpLSTW2vyhhxY,5469
2
+ git_history_extraction-0.1.0.dist-info/WHEEL,sha256=Pi5uDq5Fdo_Rr-HD5h9BiPn9Et29Y9Sh8NhcJNnFU1c,79
3
+ git_history_extraction-0.1.0.dist-info/entry_points.txt,sha256=PWlXY0SGL3v6LsZiCNTv02Lnl6_OM0l5bqL-TQjbD_Q,72
4
+ git_history_extraction-0.1.0.dist-info/METADATA,sha256=5gkb3sAPoPHzXW-AeFFDuJmGO8nh3fE40LrWx20-bxw,8544
5
+ git_history_extraction-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.17
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ git-history-extraction = git_history_extraction:main
3
+