cmt-cli 0.1.1__tar.gz → 1.0.0__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.
- cmt_cli-1.0.0/.github/workflows/test.yml +30 -0
- cmt_cli-1.0.0/PKG-INFO +94 -0
- cmt_cli-1.0.0/README.md +70 -0
- cmt_cli-1.0.0/art/cmt-cli.svg +1 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/pyproject.toml +9 -13
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/ai/cache.py +2 -2
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/ai/openai.py +29 -17
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/ai/prompt.py +7 -1
- cmt_cli-1.0.0/src/cmt/cli.py +171 -0
- cmt_cli-1.0.0/src/cmt/core/__init__.py +4 -0
- cmt_cli-1.0.0/src/cmt/core/logging.py +18 -0
- {cmt_cli-0.1.1/src/cmt/config → cmt_cli-1.0.0/src/cmt/core}/settings.py +18 -17
- cmt_cli-1.0.0/src/cmt/enums/config.py +6 -0
- cmt_cli-1.0.0/src/cmt/schemas/config.py +6 -0
- cmt_cli-1.0.0/src/cmt/schemas/general.py +7 -0
- cmt_cli-1.0.0/src/cmt/services/__init__.py +3 -0
- cmt_cli-1.0.0/src/cmt/services/pypi.py +34 -0
- cmt_cli-1.0.0/tests/__init__.py +0 -0
- cmt_cli-1.0.0/tests/unit/test_repository.py +24 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/uv.lock +40 -2
- cmt_cli-0.1.1/PKG-INFO +0 -31
- cmt_cli-0.1.1/README.md +0 -7
- cmt_cli-0.1.1/src/cmt/cli.py +0 -85
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/.gitignore +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/.python-version +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/LICENSE +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/__init__.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/ai/__init__.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/ai/provider.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/analysis/__init__.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/analysis/analyzer.py +0 -0
- {cmt_cli-0.1.1/src/cmt/config → cmt_cli-1.0.0/src/cmt/enums}/__init__.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/exceptions.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/git/__init__.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/git/repository.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/models/__init__.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/models/changes.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/models/suggestion.py +0 -0
- {cmt_cli-0.1.1/tests → cmt_cli-1.0.0/src/cmt/schemas}/__init__.py +0 -0
- {cmt_cli-0.1.1 → cmt_cli-1.0.0}/src/cmt/utils.py +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- 'src/**'
|
|
8
|
+
- 'tests/**'
|
|
9
|
+
- 'pyproject.toml'
|
|
10
|
+
pull_request:
|
|
11
|
+
branches: [main]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v3
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --all-groups
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: uv run pytest
|
cmt_cli-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cmt-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AI-powered Git commit message generator
|
|
5
|
+
Project-URL: Homepage, https://github.com/bijaydas/cmt
|
|
6
|
+
Project-URL: Documentation, https://github.com/bijaydas/cmt#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/bijaydas/cmt
|
|
8
|
+
Project-URL: Issues, https://github.com/bijaydas/cmt/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/bijaydas/cmt/releases
|
|
10
|
+
Author-email: Bijay Das <me@bijaydas.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: langchain-openai>=1.6.0
|
|
20
|
+
Requires-Dist: langchain>=1.3.18
|
|
21
|
+
Requires-Dist: pydantic>=2.13.4
|
|
22
|
+
Requires-Dist: typer>=0.27.1
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="art/cmt-cli.svg" alt="cmt-cli logo" width="400">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
# cmt-cli
|
|
30
|
+
|
|
31
|
+
<p>
|
|
32
|
+
<a href="https://pypi.org/project/cmt-cli/"><img src="https://img.shields.io/pypi/v/cmt-cli.svg" alt="PyPI version"></a>
|
|
33
|
+
<a href="https://pypi.org/project/cmt-cli/"><img src="https://img.shields.io/pypi/pyversions/cmt-cli.svg" alt="Python versions"></a>
|
|
34
|
+
<a href="https://github.com/bijaydas/cmt/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
35
|
+
<img src="https://img.shields.io/badge/status-alpha-orange.svg" alt="Alpha status">
|
|
36
|
+
<img src="https://img.shields.io/badge/tests-passing-green.svg" alt="Tests">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p>
|
|
40
|
+
AI-powered CLI tool that analyzes your staged Git changes and suggests a professional commit message.
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
> **Note:** `cmt-cli` is in **alpha**. Expect breaking changes, rough edges, and incomplete features until a stable
|
|
44
|
+
> release.
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- Analyzes staged Git changes (added, modified, deleted, and renamed files, plus diffs)
|
|
49
|
+
- Generates a commit message and description using OpenAI (via LangChain)
|
|
50
|
+
- Review, edit (in `vim`), or reject the suggested message before committing
|
|
51
|
+
- Caches suggestions per diff/model to avoid redundant API calls
|
|
52
|
+
- Simple configuration for your OpenAI API key and model
|
|
53
|
+
``
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv tool install cmt-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Requires Python 3.12+.
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
Stage your changes as usual, then run:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git add .
|
|
69
|
+
cmt suggest
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
You'll be shown a suggested commit message and can choose to:
|
|
73
|
+
|
|
74
|
+
- `y` — commit with the suggested message
|
|
75
|
+
- `e` — edit the message in `vim` before committing
|
|
76
|
+
- `n` — abort
|
|
77
|
+
|
|
78
|
+
### Configuration
|
|
79
|
+
|
|
80
|
+
Before first use, configure your OpenAI API key and model:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cmt config set
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To view your current configuration:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cmt config get
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
This project is licensed under the terms of the MIT license.
|
cmt_cli-1.0.0/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="art/cmt-cli.svg" alt="cmt-cli logo" width="400">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# cmt-cli
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<a href="https://pypi.org/project/cmt-cli/"><img src="https://img.shields.io/pypi/v/cmt-cli.svg" alt="PyPI version"></a>
|
|
9
|
+
<a href="https://pypi.org/project/cmt-cli/"><img src="https://img.shields.io/pypi/pyversions/cmt-cli.svg" alt="Python versions"></a>
|
|
10
|
+
<a href="https://github.com/bijaydas/cmt/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/status-alpha-orange.svg" alt="Alpha status">
|
|
12
|
+
<img src="https://img.shields.io/badge/tests-passing-green.svg" alt="Tests">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p>
|
|
16
|
+
AI-powered CLI tool that analyzes your staged Git changes and suggests a professional commit message.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
> **Note:** `cmt-cli` is in **alpha**. Expect breaking changes, rough edges, and incomplete features until a stable
|
|
20
|
+
> release.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Analyzes staged Git changes (added, modified, deleted, and renamed files, plus diffs)
|
|
25
|
+
- Generates a commit message and description using OpenAI (via LangChain)
|
|
26
|
+
- Review, edit (in `vim`), or reject the suggested message before committing
|
|
27
|
+
- Caches suggestions per diff/model to avoid redundant API calls
|
|
28
|
+
- Simple configuration for your OpenAI API key and model
|
|
29
|
+
``
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install cmt-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires Python 3.12+.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Stage your changes as usual, then run:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git add .
|
|
45
|
+
cmt suggest
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You'll be shown a suggested commit message and can choose to:
|
|
49
|
+
|
|
50
|
+
- `y` — commit with the suggested message
|
|
51
|
+
- `e` — edit the message in `vim` before committing
|
|
52
|
+
- `n` — abort
|
|
53
|
+
|
|
54
|
+
### Configuration
|
|
55
|
+
|
|
56
|
+
Before first use, configure your OpenAI API key and model:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cmt config set
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
To view your current configuration:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cmt config get
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
This project is licensed under the terms of the MIT license.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1584" zoomAndPan="magnify" viewBox="0 0 1188 296.999993" height="396" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><g/><clipPath id="8fdfe6cc69"><rect x="0" width="344" y="0" height="139"/></clipPath></defs><g transform="matrix(1, 0, 0, 1, 420, 79)"><g clip-path="url(#8fdfe6cc69)"><g fill="#3d3b3a" fill-opacity="1"><g transform="translate(0.871423, 99.480686)"><g><path d="M 36.5625 -42.0625 C 32.90625 -42.0625 29.757812 -40.707031 27.125 -38 C 24.488281 -35.289062 23.171875 -32.03125 23.171875 -28.21875 C 23.171875 -24.34375 24.488281 -21.050781 27.125 -18.34375 C 29.757812 -15.632812 32.90625 -14.28125 36.5625 -14.28125 C 38.320312 -14.28125 39.988281 -14.5 41.5625 -14.9375 C 43.132812 -15.375 44.285156 -15.8125 45.015625 -16.25 L 46.015625 -16.90625 L 52.59375 -3.40625 C 52.082031 -3.039062 51.367188 -2.582031 50.453125 -2.03125 C 49.546875 -1.476562 47.460938 -0.726562 44.203125 0.21875 C 40.941406 1.164062 37.445312 1.640625 33.71875 1.640625 C 25.4375 1.640625 18.164062 -1.265625 11.90625 -7.078125 C 5.65625 -12.898438 2.53125 -19.875 2.53125 -28 C 2.53125 -36.195312 5.65625 -43.242188 11.90625 -49.140625 C 18.164062 -55.035156 25.4375 -57.984375 33.71875 -57.984375 C 37.445312 -57.984375 40.882812 -57.539062 44.03125 -56.65625 C 47.175781 -55.78125 49.410156 -54.90625 50.734375 -54.03125 L 52.59375 -52.703125 L 46.015625 -39.421875 C 43.597656 -41.179688 40.445312 -42.0625 36.5625 -42.0625 Z M 36.5625 -42.0625 "/></g></g></g><g fill="#3d3b3a" fill-opacity="1"><g transform="translate(50.17971, 99.480686)"><g><path d="M 6.59375 0 L 6.59375 -56.328125 L 26.03125 -56.328125 L 26.03125 -50.40625 L 26.25 -50.40625 C 31.300781 -55.457031 36.679688 -57.984375 42.390625 -57.984375 C 46.484375 -57.984375 50.320312 -57.140625 53.90625 -55.453125 C 57.5 -53.765625 60.210938 -51.164062 62.046875 -47.65625 C 67.753906 -54.539062 73.976562 -57.984375 80.71875 -57.984375 C 86.71875 -57.984375 91.875 -56.296875 96.1875 -52.921875 C 100.507812 -49.554688 102.671875 -44.285156 102.671875 -37.109375 L 102.671875 0 L 83.234375 0 L 83.234375 -31.84375 C 83.234375 -34.84375 82.410156 -37.347656 80.765625 -39.359375 C 79.117188 -41.378906 76.648438 -42.390625 73.359375 -42.390625 C 70.503906 -42.390625 68.320312 -41.523438 66.8125 -39.796875 C 65.3125 -38.078125 64.488281 -36.015625 64.34375 -33.609375 L 64.34375 0 L 44.90625 0 L 44.90625 -31.84375 C 44.90625 -34.84375 44.082031 -37.347656 42.4375 -39.359375 C 40.789062 -41.378906 38.320312 -42.390625 35.03125 -42.390625 C 32.03125 -42.390625 29.78125 -41.453125 28.28125 -39.578125 C 26.78125 -37.710938 26.03125 -35.5 26.03125 -32.9375 L 26.03125 0 Z M 6.59375 0 "/></g></g></g><g fill="#3d3b3a" fill-opacity="1"><g transform="translate(153.298845, 99.480686)"><g><path d="M 9.890625 -56.328125 L 9.890625 -77.3125 L 29.3125 -77.3125 L 29.3125 -56.328125 L 41.171875 -56.328125 L 41.171875 -41.515625 L 29.3125 -41.515625 L 29.3125 -23.609375 C 29.3125 -18.410156 30.78125 -15.8125 33.71875 -15.8125 C 34.445312 -15.8125 35.210938 -15.957031 36.015625 -16.25 C 36.816406 -16.539062 37.441406 -16.832031 37.890625 -17.125 L 38.546875 -17.5625 L 43.375 -1.859375 C 39.195312 0.472656 34.4375 1.640625 29.09375 1.640625 C 25.4375 1.640625 22.289062 0.996094 19.65625 -0.28125 C 17.019531 -1.5625 15.023438 -3.28125 13.671875 -5.4375 C 12.316406 -7.59375 11.347656 -9.859375 10.765625 -12.234375 C 10.179688 -14.617188 9.890625 -17.164062 9.890625 -19.875 L 9.890625 -41.515625 L 1.75 -41.515625 L 1.75 -56.328125 Z M 9.890625 -56.328125 "/></g></g></g><g fill="#3d3b3a" fill-opacity="1"><g transform="translate(192.284217, 99.480686)"><g><path d="M 6.59375 -40.1875 L 41.953125 -40.1875 L 41.953125 -23.390625 L 6.59375 -23.390625 Z M 6.59375 -40.1875 "/></g></g></g><g fill="#3d3b3a" fill-opacity="1"><g transform="translate(236.431027, 99.480686)"><g><path d="M 36.5625 -42.0625 C 32.90625 -42.0625 29.757812 -40.707031 27.125 -38 C 24.488281 -35.289062 23.171875 -32.03125 23.171875 -28.21875 C 23.171875 -24.34375 24.488281 -21.050781 27.125 -18.34375 C 29.757812 -15.632812 32.90625 -14.28125 36.5625 -14.28125 C 38.320312 -14.28125 39.988281 -14.5 41.5625 -14.9375 C 43.132812 -15.375 44.285156 -15.8125 45.015625 -16.25 L 46.015625 -16.90625 L 52.59375 -3.40625 C 52.082031 -3.039062 51.367188 -2.582031 50.453125 -2.03125 C 49.546875 -1.476562 47.460938 -0.726562 44.203125 0.21875 C 40.941406 1.164062 37.445312 1.640625 33.71875 1.640625 C 25.4375 1.640625 18.164062 -1.265625 11.90625 -7.078125 C 5.65625 -12.898438 2.53125 -19.875 2.53125 -28 C 2.53125 -36.195312 5.65625 -43.242188 11.90625 -49.140625 C 18.164062 -55.035156 25.4375 -57.984375 33.71875 -57.984375 C 37.445312 -57.984375 40.882812 -57.539062 44.03125 -56.65625 C 47.175781 -55.78125 49.410156 -54.90625 50.734375 -54.03125 L 52.59375 -52.703125 L 46.015625 -39.421875 C 43.597656 -41.179688 40.445312 -42.0625 36.5625 -42.0625 Z M 36.5625 -42.0625 "/></g></g></g><g fill="#3d3b3a" fill-opacity="1"><g transform="translate(285.739315, 99.480686)"><g><path d="M 6.59375 0 L 6.59375 -94.4375 L 26.03125 -94.4375 L 26.03125 0 Z M 6.59375 0 "/></g></g></g><g fill="#3d3b3a" fill-opacity="1"><g transform="translate(313.962521, 99.480686)"><g><path d="M 8.453125 -87.734375 C 10.722656 -90.003906 13.429688 -91.140625 16.578125 -91.140625 C 19.722656 -91.140625 22.429688 -90.003906 24.703125 -87.734375 C 26.972656 -85.472656 28.109375 -82.765625 28.109375 -79.609375 C 28.109375 -76.460938 26.972656 -73.753906 24.703125 -71.484375 C 22.429688 -69.210938 19.722656 -68.078125 16.578125 -68.078125 C 13.429688 -68.078125 10.722656 -69.210938 8.453125 -71.484375 C 6.179688 -73.753906 5.046875 -76.460938 5.046875 -79.609375 C 5.046875 -82.765625 6.179688 -85.472656 8.453125 -87.734375 Z M 6.59375 -56.328125 L 6.59375 0 L 26.03125 0 L 26.03125 -56.328125 Z M 6.59375 -56.328125 "/></g></g></g></g></g></svg>
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cmt-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "AI-powered Git commit message generator"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
license = { text = "MIT" }
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
|
-
authors = [
|
|
10
|
-
{ name = "Bijay Das", email = "me@bijaydas.com" },
|
|
11
|
-
]
|
|
9
|
+
authors = [{ name = "Bijay Das", email = "me@bijaydas.com" }]
|
|
12
10
|
dependencies = [
|
|
13
11
|
"langchain>=1.3.18",
|
|
14
12
|
"langchain-openai>=1.6.0",
|
|
@@ -39,9 +37,7 @@ build-backend = "hatchling.build"
|
|
|
39
37
|
keywords = ["ai", "git", "commit", "message", "generator"]
|
|
40
38
|
|
|
41
39
|
[dependency-groups]
|
|
42
|
-
dev = [
|
|
43
|
-
"ruff>=0.16.5",
|
|
44
|
-
]
|
|
40
|
+
dev = ["pytest>=9.1.1", "ruff>=0.16.5"]
|
|
45
41
|
[tool.ruff]
|
|
46
42
|
line-length = 100
|
|
47
43
|
target-version = "py312"
|
|
@@ -49,11 +45,11 @@ src = ["src"]
|
|
|
49
45
|
|
|
50
46
|
[tool.ruff.lint]
|
|
51
47
|
select = [
|
|
52
|
-
"E",
|
|
53
|
-
"F",
|
|
54
|
-
"I",
|
|
55
|
-
"UP",
|
|
56
|
-
"B",
|
|
48
|
+
"E", # pycodestyle errors
|
|
49
|
+
"F", # pyflakes
|
|
50
|
+
"I", # isort (import sorting)
|
|
51
|
+
"UP", # pyupgrade (modern syntax)
|
|
52
|
+
"B", # flake8-bugbear (common bugs)
|
|
57
53
|
"SIM", # flake8-simplify
|
|
58
54
|
]
|
|
59
55
|
ignore = []
|
|
@@ -62,4 +58,4 @@ ignore = []
|
|
|
62
58
|
quote-style = "double"
|
|
63
59
|
|
|
64
60
|
[tool.hatch.build.targets.wheel]
|
|
65
|
-
packages = ["src/cmt"]
|
|
61
|
+
packages = ["src/cmt"]
|
|
@@ -2,13 +2,13 @@ import hashlib
|
|
|
2
2
|
import json
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from cmt.
|
|
5
|
+
from cmt.core.settings import settings
|
|
6
6
|
from cmt.models.suggestion import CommitSuggestion
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class CommitMessageCache:
|
|
10
10
|
def __init__(self):
|
|
11
|
-
self.cache_dir = Path(
|
|
11
|
+
self.cache_dir = Path(settings.CACHE_DIR)
|
|
12
12
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
13
13
|
|
|
14
14
|
@staticmethod
|
|
@@ -1,25 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
1
4
|
from langchain.agents import create_agent
|
|
5
|
+
from langchain.agents.middleware.types import InputAgentState
|
|
6
|
+
from langchain_core.messages import BaseMessage, get_buffer_string
|
|
2
7
|
from langchain_openai import ChatOpenAI
|
|
3
8
|
from pydantic import SecretStr
|
|
4
9
|
|
|
5
10
|
from cmt.ai.cache import CommitMessageCache
|
|
6
11
|
from cmt.ai.prompt import COMMIT_PROMPT, COMMIT_SYSTEM_PROMPT
|
|
7
12
|
from cmt.ai.provider import AIProvider
|
|
8
|
-
from cmt.
|
|
13
|
+
from cmt.core.settings import settings
|
|
9
14
|
from cmt.models.changes import AnalysisResult, StagedChangeSet, StagedFile
|
|
10
15
|
from cmt.models.suggestion import CommitSuggestion
|
|
11
16
|
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
12
19
|
|
|
13
20
|
class OpenAIProvider(AIProvider):
|
|
14
21
|
def __init__(self) -> None:
|
|
15
|
-
settings = Settings()
|
|
16
22
|
self.config = settings.get()
|
|
17
23
|
|
|
18
24
|
def _build_prompt(
|
|
19
|
-
self,
|
|
20
|
-
|
|
21
|
-
analysis: AnalysisResult
|
|
22
|
-
) -> str:
|
|
25
|
+
self, changes: StagedChangeSet, analysis: AnalysisResult
|
|
26
|
+
) -> list[BaseMessage]:
|
|
23
27
|
staged_files = self._process_files(changes.files)
|
|
24
28
|
staged_diffs = changes.diff
|
|
25
29
|
|
|
@@ -30,7 +34,7 @@ class OpenAIProvider(AIProvider):
|
|
|
30
34
|
deleted_files=analysis.deleted_files,
|
|
31
35
|
renamed_files=analysis.renamed_files,
|
|
32
36
|
staged_files=staged_files,
|
|
33
|
-
staged_diffs=staged_diffs
|
|
37
|
+
staged_diffs=staged_diffs,
|
|
34
38
|
)
|
|
35
39
|
|
|
36
40
|
@staticmethod
|
|
@@ -41,12 +45,14 @@ class OpenAIProvider(AIProvider):
|
|
|
41
45
|
|
|
42
46
|
return output.strip()
|
|
43
47
|
|
|
44
|
-
def _invoke(self, prompt:
|
|
48
|
+
def _invoke(self, prompt: list[BaseMessage]) -> CommitSuggestion:
|
|
45
49
|
model = ChatOpenAI(
|
|
46
50
|
model=self.config.model,
|
|
47
51
|
api_key=SecretStr(self.config.api_key),
|
|
48
52
|
timeout=30,
|
|
49
|
-
|
|
53
|
+
)
|
|
54
|
+
logger.info(
|
|
55
|
+
"Invoking %s model with prompt:\n%s", self.config.model, get_buffer_string(prompt)
|
|
50
56
|
)
|
|
51
57
|
|
|
52
58
|
agent = create_agent(
|
|
@@ -55,14 +61,12 @@ class OpenAIProvider(AIProvider):
|
|
|
55
61
|
response_format=CommitSuggestion,
|
|
56
62
|
)
|
|
57
63
|
|
|
58
|
-
result = agent.invoke(
|
|
64
|
+
result = agent.invoke(cast(InputAgentState, prompt))
|
|
59
65
|
|
|
60
66
|
return result["structured_response"]
|
|
61
67
|
|
|
62
68
|
def generate_commit_message(
|
|
63
|
-
self,
|
|
64
|
-
change_set: StagedChangeSet,
|
|
65
|
-
analysis: AnalysisResult
|
|
69
|
+
self, change_set: StagedChangeSet, analysis: AnalysisResult
|
|
66
70
|
) -> CommitSuggestion:
|
|
67
71
|
prompt = self._build_prompt(change_set, analysis)
|
|
68
72
|
|
|
@@ -70,17 +74,25 @@ class OpenAIProvider(AIProvider):
|
|
|
70
74
|
cached_message = cache.get(change_set.diff, self.config.model)
|
|
71
75
|
|
|
72
76
|
if cached_message:
|
|
73
|
-
|
|
74
|
-
message=cached_message.message,
|
|
75
|
-
|
|
77
|
+
suggestion = CommitSuggestion(
|
|
78
|
+
message=cached_message.message, description=cached_message.description
|
|
79
|
+
)
|
|
80
|
+
logger.info(
|
|
81
|
+
"Returning cached commit suggestion for the following changes:\n\n%s\n\n"
|
|
82
|
+
"Commit message: %s",
|
|
83
|
+
change_set.diff,
|
|
84
|
+
suggestion.message,
|
|
76
85
|
)
|
|
86
|
+
return suggestion
|
|
77
87
|
|
|
78
88
|
suggestion = self._invoke(prompt)
|
|
89
|
+
logger.info("Generated new commit suggestion.\n%s", suggestion)
|
|
79
90
|
|
|
80
91
|
cache.set(change_set.diff, self.config.model, suggestion)
|
|
92
|
+
logger.info("Cached new commit suggestion.")
|
|
81
93
|
|
|
82
94
|
return suggestion
|
|
83
95
|
|
|
84
96
|
@staticmethod
|
|
85
97
|
def commit_command(commit_suggestion: CommitSuggestion) -> str:
|
|
86
|
-
return f"{commit_suggestion.message} \n\n{commit_suggestion.description}"
|
|
98
|
+
return f"{commit_suggestion.message} \n\n{commit_suggestion.description}"
|
|
@@ -59,7 +59,13 @@ Renamed: {renamed_files}
|
|
|
59
59
|
Generate the commit message now.
|
|
60
60
|
"""
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
COMMIT_PROMPT1 = ChatPromptTemplate.from_messages(
|
|
63
|
+
[
|
|
64
|
+
("human", COMMIT_DATA_PROMPT),
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
COMMIT_PROMPT = ChatPromptTemplate(
|
|
63
69
|
[
|
|
64
70
|
("human", COMMIT_DATA_PROMPT),
|
|
65
71
|
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from cmt.ai.openai import OpenAIProvider
|
|
8
|
+
from cmt.analysis.analyzer import Analyzer
|
|
9
|
+
from cmt.core import settings, setup_logging
|
|
10
|
+
from cmt.enums.config import ConfigTask
|
|
11
|
+
from cmt.exceptions import CmtError
|
|
12
|
+
from cmt.git.repository import Repository
|
|
13
|
+
from cmt.schemas.config import AIConfig
|
|
14
|
+
from cmt.services import PyPIService
|
|
15
|
+
from cmt.utils import edit_with_vim
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
add_completion=False,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def version_callback(value: bool) -> None:
|
|
25
|
+
if value:
|
|
26
|
+
typer.echo(f"cmt-cli version: {version(settings.APP_NAME)}")
|
|
27
|
+
raise typer.Exit()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.callback(invoke_without_command=True)
|
|
31
|
+
def main(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
flag: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True),
|
|
34
|
+
) -> None:
|
|
35
|
+
setup_logging()
|
|
36
|
+
if ctx.invoked_subcommand is None:
|
|
37
|
+
logger.info("No subcommand invoked, displaying help.")
|
|
38
|
+
typer.echo(ctx.get_help())
|
|
39
|
+
raise typer.Exit(code=0)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def suggest() -> None:
|
|
44
|
+
"""Suggest a commit message based on staged changes."""
|
|
45
|
+
try:
|
|
46
|
+
repository = Repository()
|
|
47
|
+
analyzer = Analyzer()
|
|
48
|
+
|
|
49
|
+
if not repository.is_git_repository():
|
|
50
|
+
logger.error("Not a git repository.")
|
|
51
|
+
raise CmtError("Not a git repository.")
|
|
52
|
+
|
|
53
|
+
staged_files = repository.get_staged_changes()
|
|
54
|
+
|
|
55
|
+
if not staged_files.files:
|
|
56
|
+
logger.warning(
|
|
57
|
+
"No staged files found. Please stage your changes before running this command."
|
|
58
|
+
)
|
|
59
|
+
typer.echo(
|
|
60
|
+
"No staged files found. Please stage your changes before running this command."
|
|
61
|
+
)
|
|
62
|
+
raise typer.Exit(code=0)
|
|
63
|
+
|
|
64
|
+
analysis_result = analyzer.analyze(staged_files)
|
|
65
|
+
|
|
66
|
+
open_ai = OpenAIProvider()
|
|
67
|
+
|
|
68
|
+
commit = open_ai.generate_commit_message(staged_files, analysis_result)
|
|
69
|
+
sys.exit()
|
|
70
|
+
commit_command = OpenAIProvider.commit_command(commit)
|
|
71
|
+
|
|
72
|
+
logger.info("Suggested commit:\n\n%s", commit_command)
|
|
73
|
+
typer.echo(f"Suggested commit:\n\n{commit_command}")
|
|
74
|
+
|
|
75
|
+
while True:
|
|
76
|
+
action = (
|
|
77
|
+
typer.prompt("Use this message? [y]es / [e]dit / [n]o", default="y")
|
|
78
|
+
.strip()
|
|
79
|
+
.lower()[0]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if action == "n":
|
|
83
|
+
typer.echo("Aborted.")
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
if action == "e":
|
|
87
|
+
commit_command = edit_with_vim(commit_command)
|
|
88
|
+
typer.echo(f"Edited commit:\n\n{commit_command}")
|
|
89
|
+
|
|
90
|
+
if action == "y":
|
|
91
|
+
logger.info("Committing with message:\n\n%s", commit_command)
|
|
92
|
+
result = repository.commit(commit_command)
|
|
93
|
+
|
|
94
|
+
logger.info("Commit result:\n\n%s", result.stdout)
|
|
95
|
+
typer.echo(f"Commit result:\n\n{result.stdout}")
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
except CmtError as e:
|
|
99
|
+
typer.echo(f"Error: {e}", err=True)
|
|
100
|
+
raise typer.Exit(code=1) from None
|
|
101
|
+
except typer.Exit:
|
|
102
|
+
raise
|
|
103
|
+
except KeyboardInterrupt:
|
|
104
|
+
logger.info("Operation cancelled by user.")
|
|
105
|
+
typer.echo("\nOperation cancelled by user.", err=True)
|
|
106
|
+
raise typer.Exit(code=1) from None
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error("Unexpected error: %s", e)
|
|
109
|
+
typer.echo(f"Unexpected error: {e}", err=True)
|
|
110
|
+
raise typer.Exit(code=1) from None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command()
|
|
114
|
+
def config(task: ConfigTask) -> None:
|
|
115
|
+
"""Configure cmt-cli configuration for OpenAI API key and model."""
|
|
116
|
+
if task == ConfigTask.set:
|
|
117
|
+
open_ai_key = typer.prompt("Enter your OpenAI API key", default=None)
|
|
118
|
+
open_ai_model = typer.prompt(
|
|
119
|
+
"Enter your OpenAI model", default=settings.OPEN_AI_DEFAULT_MODEL
|
|
120
|
+
)
|
|
121
|
+
logger.info("Configuration updated with OpenAI API key and model.")
|
|
122
|
+
settings.set(AIConfig(api_key=open_ai_key, model=open_ai_model))
|
|
123
|
+
|
|
124
|
+
if task == ConfigTask.get:
|
|
125
|
+
setting_values = settings.get()
|
|
126
|
+
|
|
127
|
+
if setting_values.api_key:
|
|
128
|
+
typer.echo(f"OpenAI API key: {setting_values.api_key[:10]} ****")
|
|
129
|
+
|
|
130
|
+
if setting_values.model:
|
|
131
|
+
typer.echo(f"OpenAI model: {setting_values.model}")
|
|
132
|
+
|
|
133
|
+
logger.info("Retrieved current configuration.")
|
|
134
|
+
|
|
135
|
+
raise typer.Exit(code=0)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command()
|
|
139
|
+
def update() -> None:
|
|
140
|
+
"""Update cmt-cli to the latest version."""
|
|
141
|
+
logger.info("Starting update process for %s.", settings.APP_NAME)
|
|
142
|
+
|
|
143
|
+
current_version = version(settings.APP_NAME)
|
|
144
|
+
logger.info("Current %s version: %s", settings.APP_NAME, current_version)
|
|
145
|
+
|
|
146
|
+
pypi_service = PyPIService()
|
|
147
|
+
latest_info_response = pypi_service.any_update(current_version=current_version)
|
|
148
|
+
|
|
149
|
+
if latest_info_response.updated_required:
|
|
150
|
+
logger.info(
|
|
151
|
+
"Update required: current version %s, latest version %s",
|
|
152
|
+
latest_info_response.current_version,
|
|
153
|
+
latest_info_response.latest_version,
|
|
154
|
+
)
|
|
155
|
+
typer.echo(
|
|
156
|
+
f"A new version of {settings.APP_NAME} is available: "
|
|
157
|
+
f"{latest_info_response.latest_version}.\n\n"
|
|
158
|
+
f"upgrade {settings.APP_NAME} using your package manager.",
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
logger.info(
|
|
162
|
+
"No update required. You are using the latest version: %s",
|
|
163
|
+
latest_info_response.current_version,
|
|
164
|
+
)
|
|
165
|
+
typer.echo(f"You are using the latest version of {settings.APP_NAME}")
|
|
166
|
+
|
|
167
|
+
raise typer.Exit(code=0)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
app()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from logging.handlers import RotatingFileHandler
|
|
3
|
+
|
|
4
|
+
from cmt.core.settings import settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def setup_logging():
|
|
8
|
+
settings.LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
9
|
+
|
|
10
|
+
handler = RotatingFileHandler(
|
|
11
|
+
settings.LOGS_DIR / f"{settings.APP_NAME}.log", maxBytes=1024 * 1024, backupCount=9
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.DEBUG,
|
|
16
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
17
|
+
handlers=[handler],
|
|
18
|
+
)
|
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
from configparser import ConfigParser
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel
|
|
5
|
-
|
|
6
4
|
from cmt.exceptions import ConfigurationError
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class OpenAIConfig(BaseModel):
|
|
10
|
-
api_key: str
|
|
11
|
-
model: str
|
|
5
|
+
from cmt.schemas.config import AIConfig
|
|
12
6
|
|
|
13
7
|
|
|
14
8
|
class Settings:
|
|
9
|
+
APP_NAME: str = "cmt-cli"
|
|
15
10
|
CONFIG_DIR: Path = Path.home() / ".config" / "cmt"
|
|
16
11
|
CACHE_DIR: Path = CONFIG_DIR / "cache"
|
|
12
|
+
LOGS_DIR: Path = CONFIG_DIR / "logs"
|
|
17
13
|
|
|
18
14
|
CONFIG_FILE: Path = CONFIG_DIR / "config.ini"
|
|
19
15
|
OPEN_AI_DEFAULT_MODEL: str = "gpt-4o-mini"
|
|
20
16
|
|
|
21
|
-
def set(self, openai_config:
|
|
17
|
+
def set(self, openai_config: AIConfig):
|
|
22
18
|
self.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
23
19
|
|
|
24
20
|
config = ConfigParser()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
21
|
+
|
|
22
|
+
if openai_config.api_key:
|
|
23
|
+
config["openai"] = {}
|
|
24
|
+
|
|
25
|
+
if openai_config.api_key:
|
|
26
|
+
config["openai"]["api_key"] = openai_config.api_key
|
|
27
|
+
|
|
28
|
+
if openai_config.model:
|
|
29
|
+
config["openai"]["model"] = openai_config.model
|
|
29
30
|
|
|
30
31
|
with open(self.CONFIG_FILE, "w") as f:
|
|
31
32
|
config.write(f)
|
|
32
33
|
|
|
33
|
-
def get(self) ->
|
|
34
|
+
def get(self) -> AIConfig:
|
|
34
35
|
if not self.CONFIG_FILE.exists():
|
|
35
36
|
raise FileNotFoundError(
|
|
36
37
|
"Configuration file not found. Run `cmt config set` to set it up."
|
|
@@ -52,7 +53,7 @@ class Settings:
|
|
|
52
53
|
"Model not found in the configuration. Run `cmt config set` to set it up."
|
|
53
54
|
)
|
|
54
55
|
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
return AIConfig(api_key=api_key, model=model)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
settings = Settings()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from cmt.core import settings
|
|
6
|
+
from cmt.schemas.general import VersionInfo
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PyPIService:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._base_url = "https://pypi.org/pypi"
|
|
14
|
+
|
|
15
|
+
def _get_package_info(self) -> dict:
|
|
16
|
+
url = f"{self._base_url}/{settings.APP_NAME}/json"
|
|
17
|
+
response = httpx.get(url)
|
|
18
|
+
|
|
19
|
+
logger.debug("Received response with status code: %s", response.status_code)
|
|
20
|
+
response.raise_for_status()
|
|
21
|
+
|
|
22
|
+
return response.json()
|
|
23
|
+
|
|
24
|
+
def any_update(self, current_version: str) -> VersionInfo:
|
|
25
|
+
latest_info = self._get_package_info()
|
|
26
|
+
latest_version = latest_info["info"]["version"]
|
|
27
|
+
|
|
28
|
+
logger.debug("Current version: %s, Latest version: %s", current_version, latest_version)
|
|
29
|
+
|
|
30
|
+
return VersionInfo(
|
|
31
|
+
current_version=current_version,
|
|
32
|
+
latest_version=latest_version,
|
|
33
|
+
updated_required=latest_version != current_version,
|
|
34
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from cmt.git.repository import Repository
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_invalid_git_repository(tmp_path):
|
|
5
|
+
repo_path = tmp_path / "test_repo"
|
|
6
|
+
repo_path.mkdir()
|
|
7
|
+
|
|
8
|
+
repo = Repository(tmp_path)
|
|
9
|
+
|
|
10
|
+
assert repo.is_git_repository() is False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_valid_git_repository(tmp_path):
|
|
14
|
+
repo_path = tmp_path / "test_repo"
|
|
15
|
+
repo_path.mkdir()
|
|
16
|
+
|
|
17
|
+
# Initialize a git repository
|
|
18
|
+
import subprocess
|
|
19
|
+
|
|
20
|
+
subprocess.run(["git", "init"], cwd=repo_path)
|
|
21
|
+
|
|
22
|
+
repo = Repository(repo_path)
|
|
23
|
+
|
|
24
|
+
assert repo.is_git_repository() is True
|
|
@@ -175,7 +175,7 @@ wheels = [
|
|
|
175
175
|
|
|
176
176
|
[[package]]
|
|
177
177
|
name = "cmt-cli"
|
|
178
|
-
version = "0.
|
|
178
|
+
version = "0.2.0"
|
|
179
179
|
source = { editable = "." }
|
|
180
180
|
dependencies = [
|
|
181
181
|
{ name = "langchain" },
|
|
@@ -186,6 +186,7 @@ dependencies = [
|
|
|
186
186
|
|
|
187
187
|
[package.dev-dependencies]
|
|
188
188
|
dev = [
|
|
189
|
+
{ name = "pytest" },
|
|
189
190
|
{ name = "ruff" },
|
|
190
191
|
]
|
|
191
192
|
|
|
@@ -198,7 +199,10 @@ requires-dist = [
|
|
|
198
199
|
]
|
|
199
200
|
|
|
200
201
|
[package.metadata.requires-dev]
|
|
201
|
-
dev = [
|
|
202
|
+
dev = [
|
|
203
|
+
{ name = "pytest", specifier = ">=9.1.1" },
|
|
204
|
+
{ name = "ruff", specifier = ">=0.16.5" },
|
|
205
|
+
]
|
|
202
206
|
|
|
203
207
|
[[package]]
|
|
204
208
|
name = "colorama"
|
|
@@ -303,6 +307,15 @@ wheels = [
|
|
|
303
307
|
{ url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
|
304
308
|
]
|
|
305
309
|
|
|
310
|
+
[[package]]
|
|
311
|
+
name = "iniconfig"
|
|
312
|
+
version = "2.3.0"
|
|
313
|
+
source = { registry = "https://pypi.org/simple" }
|
|
314
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
315
|
+
wheels = [
|
|
316
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
317
|
+
]
|
|
318
|
+
|
|
306
319
|
[[package]]
|
|
307
320
|
name = "jiter"
|
|
308
321
|
version = "0.16.0"
|
|
@@ -676,6 +689,15 @@ wheels = [
|
|
|
676
689
|
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
|
677
690
|
]
|
|
678
691
|
|
|
692
|
+
[[package]]
|
|
693
|
+
name = "pluggy"
|
|
694
|
+
version = "1.6.0"
|
|
695
|
+
source = { registry = "https://pypi.org/simple" }
|
|
696
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
697
|
+
wheels = [
|
|
698
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
699
|
+
]
|
|
700
|
+
|
|
679
701
|
[[package]]
|
|
680
702
|
name = "pydantic"
|
|
681
703
|
version = "2.13.4"
|
|
@@ -775,6 +797,22 @@ wheels = [
|
|
|
775
797
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
|
776
798
|
]
|
|
777
799
|
|
|
800
|
+
[[package]]
|
|
801
|
+
name = "pytest"
|
|
802
|
+
version = "9.1.1"
|
|
803
|
+
source = { registry = "https://pypi.org/simple" }
|
|
804
|
+
dependencies = [
|
|
805
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
806
|
+
{ name = "iniconfig" },
|
|
807
|
+
{ name = "packaging" },
|
|
808
|
+
{ name = "pluggy" },
|
|
809
|
+
{ name = "pygments" },
|
|
810
|
+
]
|
|
811
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
|
812
|
+
wheels = [
|
|
813
|
+
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
|
814
|
+
]
|
|
815
|
+
|
|
778
816
|
[[package]]
|
|
779
817
|
name = "pyyaml"
|
|
780
818
|
version = "6.0.3"
|
cmt_cli-0.1.1/PKG-INFO
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: cmt-cli
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: AI-powered Git commit message generator
|
|
5
|
-
Project-URL: Homepage, https://github.com/bijaydas/cmt
|
|
6
|
-
Project-URL: Documentation, https://github.com/bijaydas/cmt#readme
|
|
7
|
-
Project-URL: Repository, https://github.com/bijaydas/cmt
|
|
8
|
-
Project-URL: Issues, https://github.com/bijaydas/cmt/issues
|
|
9
|
-
Project-URL: Changelog, https://github.com/bijaydas/cmt/releases
|
|
10
|
-
Author-email: Bijay Das <me@bijaydas.com>
|
|
11
|
-
License: MIT
|
|
12
|
-
License-File: LICENSE
|
|
13
|
-
Classifier: Development Status :: 3 - Alpha
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Topic :: Software Development :: Version Control
|
|
18
|
-
Requires-Python: >=3.12
|
|
19
|
-
Requires-Dist: langchain-openai>=1.6.0
|
|
20
|
-
Requires-Dist: langchain>=1.3.18
|
|
21
|
-
Requires-Dist: pydantic>=2.13.4
|
|
22
|
-
Requires-Dist: typer>=0.27.1
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
|
|
25
|
-
# cmt
|
|
26
|
-
|
|
27
|
-
`cmt` is a CLI tool that checks the currently staged Git changes and suggests a professional commit message.
|
|
28
|
-
|
|
29
|
-
## License
|
|
30
|
-
|
|
31
|
-
MIT
|
cmt_cli-0.1.1/README.md
DELETED
cmt_cli-0.1.1/src/cmt/cli.py
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
|
|
3
|
-
from cmt.ai.openai import OpenAIProvider
|
|
4
|
-
from cmt.analysis.analyzer import Analyzer
|
|
5
|
-
from cmt.config.settings import OpenAIConfig, Settings
|
|
6
|
-
from cmt.exceptions import CmtError
|
|
7
|
-
from cmt.git.repository import Repository
|
|
8
|
-
from cmt.utils import edit_with_vim
|
|
9
|
-
|
|
10
|
-
app = typer.Typer()
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@app.command()
|
|
14
|
-
def suggest() -> None:
|
|
15
|
-
try:
|
|
16
|
-
repository = Repository()
|
|
17
|
-
analyzer = Analyzer()
|
|
18
|
-
|
|
19
|
-
if not repository.is_git_repository():
|
|
20
|
-
raise CmtError("Not a git repository.")
|
|
21
|
-
|
|
22
|
-
staged_files = repository.get_staged_changes()
|
|
23
|
-
|
|
24
|
-
if not staged_files.files:
|
|
25
|
-
typer.echo(
|
|
26
|
-
"No staged files found. Please stage your changes before running this command."
|
|
27
|
-
)
|
|
28
|
-
raise typer.Exit(code=0)
|
|
29
|
-
|
|
30
|
-
analysis_result = analyzer.analyze(staged_files)
|
|
31
|
-
|
|
32
|
-
open_ai = OpenAIProvider()
|
|
33
|
-
|
|
34
|
-
commit = open_ai.generate_commit_message(staged_files, analysis_result)
|
|
35
|
-
commit_command = OpenAIProvider.commit_command(commit)
|
|
36
|
-
|
|
37
|
-
typer.echo(f"Suggested commit:\n\n{commit_command}")
|
|
38
|
-
|
|
39
|
-
while True:
|
|
40
|
-
action = (
|
|
41
|
-
typer.prompt("\nUse this message? [y]es / [e]dit / [n]o", default="y")
|
|
42
|
-
.strip()
|
|
43
|
-
.lower()[0]
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
if action == "n":
|
|
47
|
-
typer.echo("Aborted.")
|
|
48
|
-
break
|
|
49
|
-
|
|
50
|
-
if action == "e":
|
|
51
|
-
commit_command = edit_with_vim(commit_command)
|
|
52
|
-
typer.echo(f"Edited commit:\n\n{commit_command}")
|
|
53
|
-
|
|
54
|
-
if action == "y":
|
|
55
|
-
result = repository.commit(commit_command)
|
|
56
|
-
typer.echo(f"Commit result:\n\n{result.stdout}")
|
|
57
|
-
break
|
|
58
|
-
|
|
59
|
-
except CmtError as e:
|
|
60
|
-
typer.echo(f"Error: {e}", err=True)
|
|
61
|
-
raise typer.Exit(code=1) from None
|
|
62
|
-
except typer.Exit:
|
|
63
|
-
raise
|
|
64
|
-
except KeyboardInterrupt:
|
|
65
|
-
typer.echo("\nOperation cancelled by user.", err=True)
|
|
66
|
-
raise typer.Exit(code=1) from None
|
|
67
|
-
except Exception as e:
|
|
68
|
-
typer.echo(f"Unexpected error: {e}", err=True)
|
|
69
|
-
raise typer.Exit(code=1) from None
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@app.command()
|
|
73
|
-
def config(task: str) -> None:
|
|
74
|
-
if task == "set":
|
|
75
|
-
settings = Settings()
|
|
76
|
-
|
|
77
|
-
open_ai_key = typer.prompt("Enter your OpenAI API key")
|
|
78
|
-
open_ai_model = typer.prompt(
|
|
79
|
-
"Enter your OpenAI model", default=settings.OPEN_AI_DEFAULT_MODEL
|
|
80
|
-
)
|
|
81
|
-
settings.set(OpenAIConfig(api_key=open_ai_key, model=open_ai_model))
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if __name__ == "__main__":
|
|
85
|
-
app()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|