enhanced-git 1.0.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,349 @@
1
+ Metadata-Version: 2.4
2
+ Name: enhanced-git
3
+ Version: 1.0.0
4
+ Summary: Generate Conventional Commit messages and changelog sections using AI
5
+ Project-URL: Homepage, https://github.com/mxzahid/git-ai
6
+ Project-URL: Repository, https://github.com/mxzahid/git-ai
7
+ Project-URL: Issues, https://github.com/mxzahid/git-ai/issues
8
+ Project-URL: Documentation, https://github.com/mxzahid/git-ai#readme
9
+ Author-email: Abdullah Zahid <abdullahzahid229@gmail.com>
10
+ Maintainer-email: Abdullah Zahid <abdullahzahid229@gmail.com>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: ai,changelog,commit,conventional-commits,git,llm
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Version Control :: Git
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: gitpython>=3.1.0
25
+ Requires-Dist: openai>=1.0.0
26
+ Requires-Dist: requests>=2.31.0
27
+ Requires-Dist: rich>=13.0.0
28
+ Requires-Dist: tomli>=2.0.0; python_version < '3.11'
29
+ Requires-Dist: typer>=0.9.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: black>=23.0.0; extra == 'dev'
32
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
33
+ Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
35
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
36
+ Requires-Dist: types-requests>=2.31.0; extra == 'dev'
37
+ Provides-Extra: docs
38
+ Requires-Dist: mkdocs-material>=9.0.0; extra == 'docs'
39
+ Requires-Dist: mkdocs>=1.5.0; extra == 'docs'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # Git-AI
43
+
44
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
45
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
46
+
47
+ Generate Conventional Commit messages and changelog sections using AI. Simple to install, works with
48
+ Use Local models with Ollama or OpenAI, gracefully degrades when no AI is available.
49
+
50
+ ## Features
51
+
52
+ - **AI-Powered**: Generate commit messages using OpenAI or local Ollama models
53
+ - **Conventional Commits**: Follows strict Conventional Commit formatting rules
54
+ - **Changelog Generation**: Create Keep a Changelog formatted sections
55
+ - **Git Hook Integration**: Automatic commit message generation on `git commit`
56
+ - **Fallback Mode**: Works without AI using intelligent heuristics
57
+ - **Cross-Platform**: Linux, macOS support
58
+ - **Zero Dependencies**: Single Python package, minimal runtime deps
59
+
60
+ ## Quick Start (60 seconds)
61
+
62
+ ### Install
63
+
64
+ ```bash
65
+ # using pipx (recommended)
66
+ pipx install git-ai
67
+
68
+ # or using pip
69
+ pip install git-ai
70
+ ```
71
+
72
+ ### Setup (Optional AI Integration)
73
+
74
+ **With OpenAI:**
75
+ ```bash
76
+ export OPENAI_API_KEY="your-api-key-here"
77
+ ```
78
+
79
+ **With Ollama (Local AI):**
80
+ ```bash
81
+ # install Ollama and pull a model
82
+ ollama pull qwen2.5-coder:3b
83
+
84
+ # set environment variables
85
+ export OLLAMA_BASE_URL="http://localhost:11434"
86
+ export OLLAMA_MODEL="qwen2.5-coder:3b"
87
+ ```
88
+
89
+ ### Basic Usage
90
+
91
+ ```bash
92
+ # stage some changes
93
+ git add .
94
+
95
+ # Install Git hook for automatic generation
96
+ git-ai hook install
97
+
98
+ #if using git-ai hook, then just do git commit -m "some sample message"
99
+ #if not using git-ai hook use this:
100
+ git commit -m "$(git-ai commit)"
101
+
102
+
103
+ # generate changelog
104
+ git-ai changelog --since v1.0.0 --version v1.1.0
105
+ ```
106
+
107
+ ## Usage
108
+
109
+ ### Commands
110
+
111
+ #### `git-ai commit`
112
+
113
+ Generate a commit message from staged changes.
114
+
115
+ ```bash
116
+ # Basic usage
117
+ git-ai commit
118
+
119
+ # Preview without committing
120
+ git-ai commit --dry-run
121
+
122
+ # Subject line only
123
+ git-ai commit --no-body
124
+
125
+ # Force plain style (no conventional format)
126
+ git-ai commit --style plain
127
+
128
+ # Used by Git hook
129
+ git-ai commit --hook /path/to/.git/COMMIT_EDITMSG
130
+ ```
131
+
132
+ #### `git-ai hook install`
133
+
134
+ Install Git hook for automatic commit message generation.
135
+
136
+ ```bash
137
+ # Install hook
138
+ git-ai hook install
139
+
140
+ # Force overwrite existing hook
141
+ git-ai hook install --force
142
+
143
+ # Remove hook
144
+ git-ai hook uninstall
145
+ ```
146
+
147
+ #### `git-ai changelog`
148
+
149
+ Generate changelog section from commit history.
150
+
151
+ ```bash
152
+ # generate changelog from commits since v1.0.0
153
+ git-ai changelog --since v1.0.0
154
+
155
+ # with version header
156
+ git-ai changelog --since v1.0.0 --version v1.1.0
157
+
158
+ # custom output file
159
+ git-ai changelog --since v1.0.0 --output HISTORY.md
160
+
161
+ # different end reference
162
+ git-ai changelog --since v1.0.0 --to main
163
+ ```
164
+
165
+ ### Environment Variables
166
+
167
+ - `OPENAI_API_KEY`: Your OpenAI API key
168
+ - `OLLAMA_BASE_URL`: Ollama server URL (default: http://localhost:11434)
169
+ - `OLLAMA_MODEL`: Ollama model name (default: qwen2.5-coder:3b)
170
+
171
+ ## How It Works
172
+
173
+ ### Commit Message Generation
174
+
175
+ 1. **Diff Analysis**: Parses `git diff --staged` to understand changes
176
+ 2. **Type Inference**: Detects commit type from file paths and content:
177
+ - `tests/` → `test`
178
+ - `docs/` → `docs`
179
+ - `fix`/`bug` in content → `fix`
180
+ - New files → `feat`
181
+ 3. **AI Enhancement**: Uses LLM to polish the message while preserving accuracy
182
+ 4. **Formatting**: Ensures Conventional Commit compliance:
183
+ - Subject < 70 chars
184
+ - `type(scope): description` format
185
+ - Proper body wrapping at 72 columns
186
+
187
+ ### Changelog Generation
188
+
189
+ 1. **Commit Parsing**: Extracts commits between references
190
+ 2. **Grouping**: Groups by Conventional Commit types
191
+ 3. **AI Polish**: Improves clarity while preserving facts
192
+ 4. **Insertion**: Adds new section to top of CHANGELOG.md
193
+
194
+ ### Fallback Mode
195
+
196
+ When no AI is configured, GitAI uses intelligent heuristics:
197
+
198
+ - Path-based type detection
199
+ - Content analysis for keywords
200
+ - Statistical analysis of changes
201
+ - Scope inference from directory structure
202
+
203
+ ## Development
204
+
205
+ ### Setup
206
+
207
+ ```bash
208
+ # clone repository
209
+ git clone https://github.com/yourusername/git-ai.git
210
+ cd gitai
211
+
212
+ # install with dev dependencies
213
+ pip install -e ".[dev]"
214
+
215
+ # run tests
216
+ pytest
217
+
218
+ # run linting
219
+ ruff check .
220
+ mypy gitai
221
+ ```
222
+
223
+ ### Project Structure
224
+
225
+ ```
226
+ gitai/
227
+ ├── cli.py # Main CLI entry point
228
+ ├── commit.py # Commit message generation
229
+ ├── changelog.py # Changelog generation
230
+ ├── config.py # Configuration management
231
+ ├── constants.py # Prompts and constants
232
+ ├── diff.py # Git diff parsing and chunking
233
+ ├── hook.py # Git hook management
234
+ ├── providers/ # LLM providers
235
+ │ ├── base.py
236
+ │ ├── openai_provider.py
237
+ │ └── ollama_provider.py
238
+ ├── util.py # Utility functions
239
+ └── __init__.py
240
+
241
+ tests/ # Test suite
242
+ ├── test_commit.py
243
+ ├── test_changelog.py
244
+ ├── test_diff.py
245
+ └── test_hook_integration.py
246
+ ```
247
+
248
+ ## Contributing
249
+
250
+ I welcome contributions! Be kind
251
+
252
+ ### Development Requirements
253
+
254
+ - Python 3.11+
255
+ - Poetry for dependency management
256
+ - Pre-commit for code quality
257
+
258
+ ### Testing
259
+
260
+ ```bash
261
+ # run test suite
262
+ pytest
263
+
264
+ # with coverage
265
+ pytest --cov=gitai --cov-report=html
266
+
267
+ # run specific tests
268
+ pytest tests/test_commit.py -v
269
+ ```
270
+
271
+ ## License
272
+
273
+ MIT License - see [LICENSE](LICENSE) file for details.
274
+
275
+ ## Privacy & Security
276
+
277
+ - **No Code Storage**: Your code never leaves your machine
278
+ - **Local AI Option**: Use Ollama for complete local processing
279
+ - **API Usage**: Only sends commit diffs and prompts to configured LLM
280
+ - **Graceful Degradation**: Works without any network access
281
+
282
+ ## Troubleshooting
283
+
284
+ ### No staged changes
285
+ ```
286
+ Error: No staged changes found. Did you forget to run 'git add'?
287
+ ```
288
+ **Solution**: Stage your changes with `git add` before running `git-ai commit`
289
+
290
+ ### Missing API key
291
+ ```
292
+ Warning: OPENAI_API_KEY environment variable is required
293
+ ```
294
+ **Solution**: Set your API key or use Ollama for local AI
295
+
296
+ ### Hook conflicts
297
+ ```
298
+ Warning: Existing commit-msg hook found
299
+ ```
300
+ **Solution**: Use `git-ai hook install --force` to overwrite, or manually merge
301
+
302
+ ### Network timeouts
303
+ ```
304
+ Error: Ollama API error: Connection timeout
305
+ ```
306
+ **Solution**: Check Ollama is running: `ollama serve`
307
+
308
+ ### Large diffs
309
+ GitAI automatically chunks large diffs to stay within LLM token limits
310
+
311
+ ## Examples
312
+
313
+ ### Example Commit Messages
314
+
315
+ **AI-Generated:**
316
+ ```
317
+ feat(auth): add user registration and login system
318
+
319
+ - Implement user registration with email validation
320
+ - Add login endpoint with JWT token generation
321
+ - Create password hashing utilities
322
+ - Add input validation and error handling
323
+ ```
324
+
325
+ **Fallback Mode:**
326
+ ```
327
+ feat(src): add user authentication module
328
+
329
+ - add src/auth.py (45 additions)
330
+ - update src/models.py (12 additions, 3 deletions)
331
+ ```
332
+
333
+ ### Example Changelog
334
+
335
+ ```markdown
336
+ ## [v1.1.0] - 2024-01-15
337
+
338
+ ### Features
339
+ - **auth**: Add user registration and login system (#123)
340
+ - **api**: Implement RESTful user management endpoints
341
+
342
+ ### Fixes
343
+ - **core**: Fix null pointer exception in user validation (#456)
344
+ - **db**: Resolve connection timeout issues
345
+
346
+ ### Documentation
347
+ - Update API documentation with authentication examples
348
+ - Add installation instructions for local development
349
+ ```
@@ -0,0 +1,18 @@
1
+ gitai/__init__.py,sha256=X_3SlMT2EeGvZ9bdsXdjzwd1FFta8HHakgPv6yRq7kU,108
2
+ gitai/changelog.py,sha256=F2atDczLs-HgoafHOngKC6m2BhlfVYmknHyCIPjjFL4,8724
3
+ gitai/cli.py,sha256=l0i6UKqdeHB3TDAgaY8Gq1N-GFM4QrI2h72m9EWzY-I,4453
4
+ gitai/commit.py,sha256=vVfMcXDCOSc90ILMm8wMg0PjLiuUarzteyI2I_IT76c,12257
5
+ gitai/config.py,sha256=uOE1AyMadaPVsGJoFDLR2TJoZsYT28Q0SmQHLK-_Wyk,3543
6
+ gitai/constants.py,sha256=smipnjD7Y8h11Io1bpnimue-bz21opM74MHxczOq3rQ,3201
7
+ gitai/diff.py,sha256=Ae3aslHoeVrYDYo1UVnZe5x-z5poFUCM8K9bXRfVyug,5107
8
+ gitai/hook.py,sha256=U4KF1_uJZuw6AKtsyCcnntJcBOIsHNxZF149DYjgQDk,2527
9
+ gitai/util.py,sha256=8FS8jS7dJqedrjYsXwgXX3QHWBPxRigK22OAjcXCSqI,3718
10
+ gitai/providers/__init__.py,sha256=6IFc912-oepXeDGJyE4Ksm3KJLn6CGdYZb8HkUMfvlA,31
11
+ gitai/providers/base.py,sha256=a5b1ZulBnQvVmTlxeUQhixMyFWhwiZKMX1sIeQHHkms,1851
12
+ gitai/providers/ollama_provider.py,sha256=crRCfQZxJY1S4LaSFdiNT19u2T9WjbhpU8TCxbuo92w,2540
13
+ gitai/providers/openai_provider.py,sha256=i1lwyCtWoN5APt3UsB4MBS-jOLifDZcUCGj1Ko1CKcs,2444
14
+ enhanced_git-1.0.0.dist-info/METADATA,sha256=3_-WQqGFOr8KheBVmvNj3wazlV7bALYCzJfjmFnfXD0,8950
15
+ enhanced_git-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ enhanced_git-1.0.0.dist-info/entry_points.txt,sha256=y59MLN9OtRqEzDRPiC0tThQ9DKLAO_hkTWOBCMTqKXM,47
17
+ enhanced_git-1.0.0.dist-info/licenses/LICENSE,sha256=d11_Oc9IT-MUTvztUzbHPs_CSr9drf-6d1vnIvPiMJc,1075
18
+ enhanced_git-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ enhanced-git = gitai.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 GitAI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
gitai/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """GitAI - Generate Conventional Commit messages and changelog sections using AI."""
2
+
3
+ __version__ = "1.0.0"
gitai/changelog.py ADDED
@@ -0,0 +1,251 @@
1
+ """Changelog generation functionality."""
2
+
3
+ import re
4
+ from collections import defaultdict
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from .config import Config
9
+ from .constants import (
10
+ CHANGELOG_SECTIONS,
11
+ CHANGELOG_SYSTEM_PROMPT,
12
+ CHANGELOG_USER_PROMPT,
13
+ CHANGELOG_WRAP_WIDTH,
14
+ TYPE_TO_SECTION,
15
+ )
16
+ from .diff import get_commit_history
17
+ from .providers.base import create_provider
18
+ from .util import wrap_text
19
+
20
+
21
+ class ChangelogGenerator:
22
+ """Generates changelog sections from commit history."""
23
+
24
+ def __init__(self, config: Config):
25
+ self.config = config
26
+ self.provider = None
27
+
28
+ # Initialize LLM provider if available
29
+ if config.is_llm_available():
30
+ try:
31
+ if config.llm.provider == "openai":
32
+ self.provider = create_provider(
33
+ "openai",
34
+ api_key=config.llm.api_key,
35
+ base_url=config.llm.base_url,
36
+ model=config.llm.model,
37
+ timeout=config.llm.timeout_seconds,
38
+ )
39
+ elif config.llm.provider == "ollama":
40
+ self.provider = create_provider(
41
+ "ollama",
42
+ base_url=config.llm.base_url,
43
+ model=config.llm.model,
44
+ timeout=config.llm.timeout_seconds,
45
+ )
46
+ except Exception:
47
+ self.provider = None
48
+
49
+ def generate_changelog(
50
+ self,
51
+ since_ref: str,
52
+ to_ref: str = "HEAD",
53
+ version: str | None = None,
54
+ output_path: Path | None = None,
55
+ ) -> str:
56
+ """Generate changelog section from commits between refs."""
57
+ commits = get_commit_history(since_ref, to_ref)
58
+
59
+ if not commits:
60
+ return "No commits found in the specified range."
61
+
62
+ # Parse and group commits
63
+ grouped_commits = self._group_commits(commits)
64
+
65
+ # Generate raw changelog content
66
+ changelog_content = self._generate_raw_changelog(grouped_commits, version)
67
+
68
+ # Polish with LLM if available
69
+ if self.provider and grouped_commits:
70
+ try:
71
+ changelog_content = self._polish_with_llm(changelog_content)
72
+ except Exception:
73
+ # Fall back to raw content on LLM failure
74
+ pass
75
+
76
+ # Insert into existing CHANGELOG.md if it exists
77
+ if output_path:
78
+ return self._insert_into_changelog(changelog_content, output_path)
79
+ else:
80
+ # Default to CHANGELOG.md in git root
81
+ default_path = self.config.git_root / "CHANGELOG.md"
82
+ return self._insert_into_changelog(changelog_content, default_path)
83
+
84
+ def _group_commits(self, commits: list[dict[str, str]]) -> dict[str, list[str]]:
85
+ """Group commits by type for changelog sections."""
86
+ grouped = defaultdict(list)
87
+
88
+ for commit in commits:
89
+ subject = commit["subject"]
90
+ body = commit.get("body", "")
91
+
92
+ # Parse conventional commit format: type(scope): description
93
+ match = re.match(r"^(\w+)(?:\(([^)]+)\))?:\s*(.+)$", subject)
94
+ if match:
95
+ commit_type = match.group(1)
96
+ scope = match.group(2)
97
+ description = match.group(3)
98
+
99
+ # Map to changelog section
100
+ section = TYPE_TO_SECTION.get(commit_type, "Other")
101
+
102
+ # Include scope if present
103
+ if scope:
104
+ description = f"**{scope}:** {description}"
105
+
106
+ # Add PR/issue references from body
107
+ pr_refs = self._extract_pr_references(body)
108
+ if pr_refs:
109
+ description += f" ({pr_refs})"
110
+
111
+ grouped[section].append(description)
112
+ else:
113
+ # Non-conventional commit
114
+ grouped["Other"].append(subject)
115
+
116
+ return dict(grouped)
117
+
118
+ def _generate_raw_changelog(
119
+ self, grouped_commits: dict[str, list[str]], version: str | None
120
+ ) -> str:
121
+ """Generate raw changelog content from grouped commits."""
122
+ lines = []
123
+
124
+ # Add header
125
+ if version:
126
+ lines.append(f"## [{version}] - {datetime.now().strftime('%Y-%m-%d')}")
127
+ else:
128
+ lines.append(f"## {datetime.now().strftime('%Y-%m-%d')}")
129
+
130
+ lines.append("")
131
+
132
+ # Add sections in order
133
+ for section in CHANGELOG_SECTIONS:
134
+ if section in grouped_commits:
135
+ lines.append(f"### {section}")
136
+ lines.append("")
137
+
138
+ for item in grouped_commits[section]:
139
+ # Wrap long lines
140
+ wrapped_item = wrap_text(item, CHANGELOG_WRAP_WIDTH)
141
+ lines.append(f"- {wrapped_item}")
142
+
143
+ lines.append("")
144
+
145
+ return "\n".join(lines)
146
+
147
+ def _polish_with_llm(self, raw_changelog: str) -> str:
148
+ """Polish changelog content using LLM."""
149
+ if not self.provider:
150
+ return raw_changelog
151
+ lines = raw_changelog.split("\n")
152
+ header_lines = []
153
+ content_lines = []
154
+
155
+ in_content = False
156
+ for line in lines:
157
+ if line.startswith("### "):
158
+ in_content = True
159
+ if in_content:
160
+ content_lines.append(line)
161
+ else:
162
+ header_lines.append(line)
163
+
164
+ if not content_lines:
165
+ return raw_changelog
166
+
167
+ grouped_bullets = "\n".join(content_lines)
168
+
169
+ prompt = CHANGELOG_USER_PROMPT.format(grouped_bullets=grouped_bullets)
170
+ if self.config.debug_settings.debug_mode:
171
+ print("Sending changelog to LLM for polishing...")
172
+ print(f"System: {CHANGELOG_SYSTEM_PROMPT}")
173
+ print(f"User: {prompt}")
174
+ print("-" * 50)
175
+
176
+ polished_content = self.provider.generate(
177
+ system=CHANGELOG_SYSTEM_PROMPT,
178
+ user=prompt,
179
+ max_tokens=self.config.llm.max_tokens,
180
+ temperature=self.config.llm.temperature,
181
+ timeout=self.config.llm.timeout_seconds,
182
+ )
183
+ if self.config.debug_settings.debug_mode:
184
+ print(f"LLM polished response: {polished_content}")
185
+ print("-" * 50)
186
+
187
+ polished_lines = []
188
+ for line in polished_content.split("\n"):
189
+ line = line.strip()
190
+ if line and not line.startswith("```"):
191
+ if not line.startswith("- "):
192
+ line = f"- {line}"
193
+ polished_lines.append(line)
194
+
195
+ return "\n".join(header_lines) + "\n" + "\n".join(polished_lines) + "\n"
196
+
197
+ def _insert_into_changelog(self, new_content: str, changelog_path: Path) -> str:
198
+ """Insert new changelog content at the top of existing file."""
199
+ if not changelog_path.exists():
200
+ # create new changelog file
201
+ changelog_path.write_text(new_content + "\n")
202
+ return f"Created new changelog at {changelog_path}"
203
+
204
+ # read existing content
205
+ existing_content = changelog_path.read_text()
206
+
207
+ # find the first header to insert before it
208
+ lines = existing_content.split("\n")
209
+ insert_index = 0
210
+
211
+ for i, line in enumerate(lines):
212
+ if line.startswith("# "):
213
+ insert_index = i
214
+ break
215
+
216
+ # insert new content
217
+ new_lines = new_content.split("\n")
218
+ updated_lines = lines[:insert_index] + new_lines + [""] + lines[insert_index:]
219
+
220
+ # write back to file
221
+ changelog_path.write_text("\n".join(updated_lines))
222
+
223
+ return f"Updated changelog at {changelog_path}"
224
+
225
+ def _extract_pr_references(self, body: str) -> str:
226
+ """Extract PR and issue references from commit body."""
227
+ references = []
228
+
229
+ # common patterns for PR/issue references
230
+ patterns = [
231
+ r"#(\d+)",
232
+ r"\b(PR|Pull Request|Issue|Fixes|Closes)\s*#?(\d+)\b",
233
+ r"\bGH-(\d+)\b",
234
+ ]
235
+
236
+ for pattern in patterns:
237
+ matches = re.findall(pattern, body, re.IGNORECASE)
238
+ for match in matches:
239
+ if isinstance(match, tuple):
240
+ # for patterns with capture groups
241
+ ref_num = match[1] if len(match) > 1 else match[0]
242
+ references.append(f"#{ref_num}")
243
+ else:
244
+ references.append(f"#{match}")
245
+
246
+ # remove duplicates and return
247
+ def extract_number(ref: str) -> int:
248
+ match = re.match(r"#(\d+)", ref)
249
+ return int(match.group(1)) if match else 0
250
+
251
+ return ", ".join(sorted(set(references), key=extract_number))