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.
- enhanced_git-1.0.0.dist-info/METADATA +349 -0
- enhanced_git-1.0.0.dist-info/RECORD +18 -0
- enhanced_git-1.0.0.dist-info/WHEEL +4 -0
- enhanced_git-1.0.0.dist-info/entry_points.txt +2 -0
- enhanced_git-1.0.0.dist-info/licenses/LICENSE +21 -0
- gitai/__init__.py +3 -0
- gitai/changelog.py +251 -0
- gitai/cli.py +166 -0
- gitai/commit.py +338 -0
- gitai/config.py +120 -0
- gitai/constants.py +134 -0
- gitai/diff.py +167 -0
- gitai/hook.py +81 -0
- gitai/providers/__init__.py +1 -0
- gitai/providers/base.py +71 -0
- gitai/providers/ollama_provider.py +86 -0
- gitai/providers/openai_provider.py +78 -0
- gitai/util.py +137 -0
@@ -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
|
+
[](https://opensource.org/licenses/MIT)
|
45
|
+
[](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,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
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))
|