aiautocommit 0.6.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.
- aiautocommit-0.6.0/LICENSE +21 -0
- aiautocommit-0.6.0/PKG-INFO +111 -0
- aiautocommit-0.6.0/README.md +98 -0
- aiautocommit-0.6.0/aiautocommit/__init__.py +368 -0
- aiautocommit-0.6.0/aiautocommit.egg-info/PKG-INFO +111 -0
- aiautocommit-0.6.0/aiautocommit.egg-info/SOURCES.txt +10 -0
- aiautocommit-0.6.0/aiautocommit.egg-info/dependency_links.txt +1 -0
- aiautocommit-0.6.0/aiautocommit.egg-info/entry_points.txt +2 -0
- aiautocommit-0.6.0/aiautocommit.egg-info/requires.txt +2 -0
- aiautocommit-0.6.0/aiautocommit.egg-info/top_level.txt +1 -0
- aiautocommit-0.6.0/pyproject.toml +17 -0
- aiautocommit-0.6.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Michael Bianco
|
|
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.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: aiautocommit
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: AI generated commit messages
|
|
5
|
+
Author-email: Michael Bianco <mike@mikebian.co>
|
|
6
|
+
Project-URL: Repository, https://github.com/iloveitaly/aiautocommit
|
|
7
|
+
Keywords: git,commit,automation,ai,openai
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: click>=8.1.7
|
|
12
|
+
Requires-Dist: openai>=1.54.3
|
|
13
|
+
|
|
14
|
+
[](https://github.com/iloveitaly/aiautocommit/releases) [](https://pepy.tech/project/aiautocommit) [](https://pypi.org/project/aiautocommit)  [](https://opensource.org/licenses/MIT)
|
|
15
|
+
|
|
16
|
+
# aiautocommit
|
|
17
|
+
|
|
18
|
+
Generate intelligent commit messages using AI. aiautocommit analyzes your staged changes and creates conventional commit messages, handling both small tweaks and large changesets effectively.
|
|
19
|
+
|
|
20
|
+
Yes, there are a lot of these. Main ways this is different:
|
|
21
|
+
|
|
22
|
+
* Simple codebase
|
|
23
|
+
* Ability to easily customize prompts on a per-repo basis
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
pip install aiautocommit
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
* Generates conventional commit messages
|
|
34
|
+
* Customizable prompts and exclusions
|
|
35
|
+
* Pre-commit hook integration
|
|
36
|
+
* Supports custom config directories
|
|
37
|
+
|
|
38
|
+
## Getting Started
|
|
39
|
+
|
|
40
|
+
Set your OpenAI API key:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
export OPENAI_API_KEY=<YOUR API KEY>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Stage your changes and run aiautocommit:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
git add .
|
|
50
|
+
aiautocommit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Customization
|
|
54
|
+
|
|
55
|
+
### Using Config Directory
|
|
56
|
+
|
|
57
|
+
aiautocommit looks for configuration files in these locations (in priority order):
|
|
58
|
+
|
|
59
|
+
* .aiautocommit/ in current directory
|
|
60
|
+
* $XDG_CONFIG_HOME/aiautocommit/ (defaults to ~/.config/aiautocommit/)
|
|
61
|
+
* Custom path via aiautocommit_CONFIG environment variable
|
|
62
|
+
|
|
63
|
+
To get started with customization:
|
|
64
|
+
|
|
65
|
+
```shell
|
|
66
|
+
aiautocommit dump-prompts
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This creates a `.aiautocommit/` directory with:
|
|
70
|
+
|
|
71
|
+
* `diff_prompt.txt`: Template for generating diff summaries
|
|
72
|
+
* `commit_prompt.txt`: Template for generating commit messages
|
|
73
|
+
* `exclusions.txt`: List of files to exclude from processing
|
|
74
|
+
|
|
75
|
+
### Installing Pre-commit Hook
|
|
76
|
+
|
|
77
|
+
To automatically generate commit messages during git commits:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
aiautocommit install-pre-commit
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Environment Variables
|
|
84
|
+
|
|
85
|
+
* `OPENAI_API_KEY`: Your OpenAI API key
|
|
86
|
+
* `aiautocommit_MODEL`: AI model to use (default: gpt-4-mini)
|
|
87
|
+
* `aiautocommit_CONFIG`: Custom config directory path
|
|
88
|
+
* `LOG_LEVEL`: Logging verbosity
|
|
89
|
+
* `aiautocommit_LOG_PATH`: Custom log file path
|
|
90
|
+
|
|
91
|
+
## Privacy Disclaimer
|
|
92
|
+
|
|
93
|
+
`gpt-commit` uses the [OpenAI API](https://platform.openai.com/docs) to generate commit messages. Both file names and contents from files that contain staged changes will be shared with OpenAI when using `gpt-commit`. OpenAI will process this data according to their [terms of use](https://openai.com/policies/terms-of-use) and [API data usage policies](https://openai.com/policies/api-data-usage-policies). On March 1st 2023 OpenAI pledged that by default, they would not use data submitted by customers via their API to train or improve their models, and that this data will be retained for a maximum of 30 days, after which it will be deleted.
|
|
94
|
+
|
|
95
|
+
## Special Thanks
|
|
96
|
+
|
|
97
|
+
[This project](https://github.com/markuswt/gpt-commit) inspired this project. It had a very simple codebase. I've taken the idea and expanded it to include a lot more features, specifically per-project prompt customization.
|
|
98
|
+
|
|
99
|
+
## Related Projects
|
|
100
|
+
|
|
101
|
+
I looked at a bunch of projects before building this one.
|
|
102
|
+
|
|
103
|
+
- https://github.com/abi/aiautocommit - python, inactive. Many files, not simple.
|
|
104
|
+
- https://github.com/Sett17/turboCommit - rust, inactive.
|
|
105
|
+
- https://github.com/Nutlope/aicommits - typescript. Node is so slow and I hate working with it.
|
|
106
|
+
- https://github.com/Elhameed/aicommits - python
|
|
107
|
+
- https://github.com/zurawiki/gptcommit - active, rust. Too complicated and no prompt customization.
|
|
108
|
+
- https://github.com/ywkim/gpt-commit - lisp, inactive.
|
|
109
|
+
- https://github.com/markuswt/gpt-commit - single file, python based. No commits > 1yr. Very simple codebase.
|
|
110
|
+
- https://github.com/josenerydev/gpt-commit - also python
|
|
111
|
+
- https://github.com/di-sukharev/opencommit - has conventional commit structure
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[](https://github.com/iloveitaly/aiautocommit/releases) [](https://pepy.tech/project/aiautocommit) [](https://pypi.org/project/aiautocommit)  [](https://opensource.org/licenses/MIT)
|
|
2
|
+
|
|
3
|
+
# aiautocommit
|
|
4
|
+
|
|
5
|
+
Generate intelligent commit messages using AI. aiautocommit analyzes your staged changes and creates conventional commit messages, handling both small tweaks and large changesets effectively.
|
|
6
|
+
|
|
7
|
+
Yes, there are a lot of these. Main ways this is different:
|
|
8
|
+
|
|
9
|
+
* Simple codebase
|
|
10
|
+
* Ability to easily customize prompts on a per-repo basis
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```shell
|
|
15
|
+
pip install aiautocommit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
* Generates conventional commit messages
|
|
21
|
+
* Customizable prompts and exclusions
|
|
22
|
+
* Pre-commit hook integration
|
|
23
|
+
* Supports custom config directories
|
|
24
|
+
|
|
25
|
+
## Getting Started
|
|
26
|
+
|
|
27
|
+
Set your OpenAI API key:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
export OPENAI_API_KEY=<YOUR API KEY>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Stage your changes and run aiautocommit:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
git add .
|
|
37
|
+
aiautocommit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Customization
|
|
41
|
+
|
|
42
|
+
### Using Config Directory
|
|
43
|
+
|
|
44
|
+
aiautocommit looks for configuration files in these locations (in priority order):
|
|
45
|
+
|
|
46
|
+
* .aiautocommit/ in current directory
|
|
47
|
+
* $XDG_CONFIG_HOME/aiautocommit/ (defaults to ~/.config/aiautocommit/)
|
|
48
|
+
* Custom path via aiautocommit_CONFIG environment variable
|
|
49
|
+
|
|
50
|
+
To get started with customization:
|
|
51
|
+
|
|
52
|
+
```shell
|
|
53
|
+
aiautocommit dump-prompts
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This creates a `.aiautocommit/` directory with:
|
|
57
|
+
|
|
58
|
+
* `diff_prompt.txt`: Template for generating diff summaries
|
|
59
|
+
* `commit_prompt.txt`: Template for generating commit messages
|
|
60
|
+
* `exclusions.txt`: List of files to exclude from processing
|
|
61
|
+
|
|
62
|
+
### Installing Pre-commit Hook
|
|
63
|
+
|
|
64
|
+
To automatically generate commit messages during git commits:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
aiautocommit install-pre-commit
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Environment Variables
|
|
71
|
+
|
|
72
|
+
* `OPENAI_API_KEY`: Your OpenAI API key
|
|
73
|
+
* `aiautocommit_MODEL`: AI model to use (default: gpt-4-mini)
|
|
74
|
+
* `aiautocommit_CONFIG`: Custom config directory path
|
|
75
|
+
* `LOG_LEVEL`: Logging verbosity
|
|
76
|
+
* `aiautocommit_LOG_PATH`: Custom log file path
|
|
77
|
+
|
|
78
|
+
## Privacy Disclaimer
|
|
79
|
+
|
|
80
|
+
`gpt-commit` uses the [OpenAI API](https://platform.openai.com/docs) to generate commit messages. Both file names and contents from files that contain staged changes will be shared with OpenAI when using `gpt-commit`. OpenAI will process this data according to their [terms of use](https://openai.com/policies/terms-of-use) and [API data usage policies](https://openai.com/policies/api-data-usage-policies). On March 1st 2023 OpenAI pledged that by default, they would not use data submitted by customers via their API to train or improve their models, and that this data will be retained for a maximum of 30 days, after which it will be deleted.
|
|
81
|
+
|
|
82
|
+
## Special Thanks
|
|
83
|
+
|
|
84
|
+
[This project](https://github.com/markuswt/gpt-commit) inspired this project. It had a very simple codebase. I've taken the idea and expanded it to include a lot more features, specifically per-project prompt customization.
|
|
85
|
+
|
|
86
|
+
## Related Projects
|
|
87
|
+
|
|
88
|
+
I looked at a bunch of projects before building this one.
|
|
89
|
+
|
|
90
|
+
- https://github.com/abi/aiautocommit - python, inactive. Many files, not simple.
|
|
91
|
+
- https://github.com/Sett17/turboCommit - rust, inactive.
|
|
92
|
+
- https://github.com/Nutlope/aicommits - typescript. Node is so slow and I hate working with it.
|
|
93
|
+
- https://github.com/Elhameed/aicommits - python
|
|
94
|
+
- https://github.com/zurawiki/gptcommit - active, rust. Too complicated and no prompt customization.
|
|
95
|
+
- https://github.com/ywkim/gpt-commit - lisp, inactive.
|
|
96
|
+
- https://github.com/markuswt/gpt-commit - single file, python based. No commits > 1yr. Very simple codebase.
|
|
97
|
+
- https://github.com/josenerydev/gpt-commit - also python
|
|
98
|
+
- https://github.com/di-sukharev/opencommit - has conventional commit structure
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
# Config file locations in priority order
|
|
10
|
+
CONFIG_PATHS = [
|
|
11
|
+
Path(".aiautocommit"), # $PWD/.aiautocommit
|
|
12
|
+
Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser()
|
|
13
|
+
/ "aiautocommit", # XDG config dir
|
|
14
|
+
Path(os.environ.get("AIAUTOCOMMIT_CONFIG", "")), # Custom config path
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
DIFF_PROMPT_FILE = "diff_prompt.txt"
|
|
18
|
+
COMMIT_PROMPT_FILE = "commit_prompt.txt"
|
|
19
|
+
EXCLUSIONS_FILE = "exclusions.txt"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
MODEL_NAME = os.environ.get("AIAUTOCOMMIT_MODEL", "gpt-4o-mini")
|
|
23
|
+
DIFF_PROMPT = """
|
|
24
|
+
Generate a short summary of the git diffs included using these rules:
|
|
25
|
+
|
|
26
|
+
* Indicate (ex: "Size: large" on the first line) if the change is large (800+ lines changes), medium (300-800 lines changed), or small (less than 300 lines changed)
|
|
27
|
+
* Exclude comments or whitespace changes from the summary
|
|
28
|
+
* Do not include information about modified comments
|
|
29
|
+
* Omit information about renamed variables or functions
|
|
30
|
+
|
|
31
|
+
Only respond with summary content.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
"""
|
|
35
|
+
# https://cbea.ms/git-commit/
|
|
36
|
+
COMMIT_MSG_PROMPT = """
|
|
37
|
+
Generate a commit message from the code change summaries using these rules:
|
|
38
|
+
|
|
39
|
+
* No more than 50 character summary
|
|
40
|
+
* Imperative mood in the subject line
|
|
41
|
+
* Conventional commit format
|
|
42
|
+
* When change summaries are indicated as large, include extended commit message with markdown bullets.
|
|
43
|
+
* Use the extended commit (body) to explain what and why vs. how
|
|
44
|
+
* Do not wrap in a codeblock
|
|
45
|
+
* Do not include generic messages without specifics such as:
|
|
46
|
+
* "Improved comments and structured logic for clarity..."
|
|
47
|
+
* "Separated logic from the original function..."
|
|
48
|
+
* "Refactored X into Y..."
|
|
49
|
+
* "Introduced new function..."
|
|
50
|
+
* "Enhances clarity and ease of use..."
|
|
51
|
+
* Don't mention details which feat: update prompt text in DIFF_PROMPT variable
|
|
52
|
+
* If there is not enough information to generate a summary, return an empty string
|
|
53
|
+
|
|
54
|
+
Below are the change summaries:
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
"""
|
|
58
|
+
EXCLUDED_FILES = [
|
|
59
|
+
"Gemfile.lock",
|
|
60
|
+
"uv.lock",
|
|
61
|
+
"poetry.lock",
|
|
62
|
+
"package-lock.json",
|
|
63
|
+
"yarn.lock",
|
|
64
|
+
"pnpm-lock.yaml",
|
|
65
|
+
"package-lock.json",
|
|
66
|
+
"yarn.lock",
|
|
67
|
+
"pnpm-lock.yaml",
|
|
68
|
+
"pnpm-lock.yaml",
|
|
69
|
+
"composer.lock",
|
|
70
|
+
"cargo.lock",
|
|
71
|
+
"mix.lock",
|
|
72
|
+
"Pipfile.lock",
|
|
73
|
+
"pdm.lock",
|
|
74
|
+
"flake.lock",
|
|
75
|
+
"bun.lockb",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
PROMPT_CUTOFF = 10_000
|
|
79
|
+
|
|
80
|
+
logging.basicConfig(
|
|
81
|
+
level=os.environ.get("LOG_LEVEL", "INFO").upper(),
|
|
82
|
+
**(
|
|
83
|
+
{"filename": os.environ.get("AIAUTOCOMMIT_LOG_PATH")}
|
|
84
|
+
if os.environ.get("AIAUTOCOMMIT_LOG_PATH")
|
|
85
|
+
else {"stream": sys.stderr}
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def configure_prompts(config_dir=None):
|
|
91
|
+
global DIFF_PROMPT, COMMIT_MSG_PROMPT, EXCLUDED_FILES, CONFIG_PATHS
|
|
92
|
+
|
|
93
|
+
if config_dir:
|
|
94
|
+
CONFIG_PATHS.insert(0, Path(config_dir))
|
|
95
|
+
|
|
96
|
+
# Find first existing config dir
|
|
97
|
+
config_dir = next((path for path in CONFIG_PATHS if path and path.exists()), None)
|
|
98
|
+
|
|
99
|
+
if not config_dir:
|
|
100
|
+
logging.debug("No config directory found")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
logging.debug(f"Found config directory at {config_dir}")
|
|
104
|
+
|
|
105
|
+
# Load diff prompt
|
|
106
|
+
diff_file = config_dir / DIFF_PROMPT_FILE
|
|
107
|
+
if diff_file.exists():
|
|
108
|
+
logging.debug("Loading custom diff prompt from diff.txt")
|
|
109
|
+
DIFF_PROMPT = diff_file.read_text().strip()
|
|
110
|
+
|
|
111
|
+
# Load commit prompt
|
|
112
|
+
commit_file = config_dir / COMMIT_PROMPT_FILE
|
|
113
|
+
if commit_file.exists():
|
|
114
|
+
logging.debug("Loading custom commit prompt from commit.txt")
|
|
115
|
+
COMMIT_MSG_PROMPT = commit_file.read_text().strip()
|
|
116
|
+
|
|
117
|
+
# Load exclusions
|
|
118
|
+
exclusions_file = config_dir / EXCLUSIONS_FILE
|
|
119
|
+
if exclusions_file.exists():
|
|
120
|
+
logging.debug("Loading custom exclusions from exclusions.txt")
|
|
121
|
+
EXCLUDED_FILES = [
|
|
122
|
+
line.strip()
|
|
123
|
+
for line in exclusions_file.read_text().splitlines()
|
|
124
|
+
if line.strip()
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_diff(ignore_whitespace=True):
|
|
129
|
+
arguments = [
|
|
130
|
+
"git",
|
|
131
|
+
"--no-pager",
|
|
132
|
+
"diff",
|
|
133
|
+
"--staged",
|
|
134
|
+
]
|
|
135
|
+
if ignore_whitespace:
|
|
136
|
+
arguments += [
|
|
137
|
+
"--ignore-space-change",
|
|
138
|
+
"--ignore-blank-lines",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
for file in EXCLUDED_FILES:
|
|
142
|
+
arguments += [f":(exclude){file}"]
|
|
143
|
+
|
|
144
|
+
diff_process = subprocess.run(arguments, capture_output=True, text=True)
|
|
145
|
+
diff_process.check_returncode()
|
|
146
|
+
normalized_diff = diff_process.stdout.strip()
|
|
147
|
+
|
|
148
|
+
logging.debug(f"Discovered Diff:\n{normalized_diff}")
|
|
149
|
+
|
|
150
|
+
return normalized_diff
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def parse_diff(diff):
|
|
154
|
+
file_diffs = diff.split("\ndiff")
|
|
155
|
+
file_diffs = [file_diffs[0]] + [
|
|
156
|
+
"\ndiff" + file_diff for file_diff in file_diffs[1:]
|
|
157
|
+
]
|
|
158
|
+
chunked_file_diffs = []
|
|
159
|
+
for file_diff in file_diffs:
|
|
160
|
+
[head, *chunks] = file_diff.split("\n@@")
|
|
161
|
+
chunks = ["\n@@" + chunk for chunk in reversed(chunks)]
|
|
162
|
+
chunked_file_diffs.append((head, chunks))
|
|
163
|
+
return chunked_file_diffs
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def assemble_diffs(parsed_diffs, cutoff):
|
|
167
|
+
"""
|
|
168
|
+
Create multiple well-formatted diff strings, each being shorter than cutoff
|
|
169
|
+
"""
|
|
170
|
+
assembled_diffs = [""]
|
|
171
|
+
|
|
172
|
+
def add_chunk(chunk):
|
|
173
|
+
if len(assembled_diffs[-1]) + len(chunk) <= cutoff:
|
|
174
|
+
assembled_diffs[-1] += "\n" + chunk
|
|
175
|
+
return True
|
|
176
|
+
else:
|
|
177
|
+
assembled_diffs.append(chunk)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
for head, chunks in parsed_diffs:
|
|
181
|
+
if not chunks:
|
|
182
|
+
add_chunk(head)
|
|
183
|
+
else:
|
|
184
|
+
add_chunk(head + chunks.pop())
|
|
185
|
+
while chunks:
|
|
186
|
+
if not add_chunk(chunks.pop()):
|
|
187
|
+
assembled_diffs[-1] = head + assembled_diffs[-1]
|
|
188
|
+
return assembled_diffs
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def asyncopenai() -> "AsyncOpenAI":
|
|
192
|
+
from openai import AsyncOpenAI
|
|
193
|
+
|
|
194
|
+
if not hasattr(asyncopenai, "_instance"):
|
|
195
|
+
# Using a class-level attribute to store the singleton instance
|
|
196
|
+
setattr(asyncopenai, "_instance", AsyncOpenAI())
|
|
197
|
+
return getattr(asyncopenai, "_instance")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def complete(prompt):
|
|
201
|
+
aclient = asyncopenai()
|
|
202
|
+
completion_resp = await aclient.chat.completions.create(
|
|
203
|
+
model=MODEL_NAME,
|
|
204
|
+
messages=[{"role": "user", "content": prompt[: PROMPT_CUTOFF + 100]}],
|
|
205
|
+
# TODO this seems awfully small?
|
|
206
|
+
max_tokens=128,
|
|
207
|
+
)
|
|
208
|
+
completion = completion_resp.choices[0].message.content.strip()
|
|
209
|
+
return completion
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def summarize_diff(diff):
|
|
213
|
+
assert diff
|
|
214
|
+
|
|
215
|
+
summarized_diff = await complete(DIFF_PROMPT + "\n\n" + diff + "\n\n")
|
|
216
|
+
logging.debug(f"Summarized Diff:\n{summarized_diff}")
|
|
217
|
+
return summarized_diff
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def summarize_summaries(summaries):
|
|
221
|
+
assert summaries
|
|
222
|
+
return await complete(COMMIT_MSG_PROMPT + "\n\n" + summaries + "\n\n")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def generate_commit_message(diff):
|
|
226
|
+
if not diff:
|
|
227
|
+
logging.debug("No commit message generated")
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
assembled_diffs = assemble_diffs(parse_diff(diff), PROMPT_CUTOFF)
|
|
231
|
+
|
|
232
|
+
logging.debug(f"Summarizing {len(assembled_diffs)} diffs")
|
|
233
|
+
|
|
234
|
+
summaries = await asyncio.gather(
|
|
235
|
+
*[summarize_diff(diff) for diff in assembled_diffs]
|
|
236
|
+
)
|
|
237
|
+
return await summarize_summaries("\n".join(summaries))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def git_commit(message):
|
|
241
|
+
# will ignore message if diff is empty
|
|
242
|
+
return subprocess.run(["git", "commit", "--message", message, "--edit"]).returncode
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@click.group(invoke_without_command=True)
|
|
246
|
+
def main():
|
|
247
|
+
"""
|
|
248
|
+
Generate a commit message for staged files and commit them.
|
|
249
|
+
Git will prompt you to edit the generated commit message.
|
|
250
|
+
"""
|
|
251
|
+
ctx = click.get_current_context()
|
|
252
|
+
if ctx.invoked_subcommand is None:
|
|
253
|
+
ctx.invoke(commit)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@main.command()
|
|
257
|
+
@click.option(
|
|
258
|
+
"-p",
|
|
259
|
+
"--print-message",
|
|
260
|
+
is_flag=True,
|
|
261
|
+
default=False,
|
|
262
|
+
help="print commit msg to stdout instead of performing commit",
|
|
263
|
+
)
|
|
264
|
+
@click.option(
|
|
265
|
+
"-o",
|
|
266
|
+
"--output-file",
|
|
267
|
+
type=click.Path(writable=True),
|
|
268
|
+
help="write commit message to specified file",
|
|
269
|
+
)
|
|
270
|
+
@click.option(
|
|
271
|
+
"--config-dir",
|
|
272
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
273
|
+
help="specify custom config directory",
|
|
274
|
+
)
|
|
275
|
+
def commit(print_message, output_file, config_dir):
|
|
276
|
+
"""
|
|
277
|
+
Generate commit message from git diff.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
configure_prompts(config_dir)
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
if not get_diff(ignore_whitespace=False):
|
|
284
|
+
click.echo(
|
|
285
|
+
"No changes staged. Use `git add` to stage files before invoking gpt-commit.",
|
|
286
|
+
err=True,
|
|
287
|
+
)
|
|
288
|
+
return 1
|
|
289
|
+
|
|
290
|
+
# run async so when multiple chunks exist we can get summaries concurrently
|
|
291
|
+
commit_message = asyncio.run(generate_commit_message(get_diff()))
|
|
292
|
+
except UnicodeDecodeError:
|
|
293
|
+
click.echo("gpt-commit does not support binary files", err=True)
|
|
294
|
+
commit_message = (
|
|
295
|
+
# TODO use heredoc
|
|
296
|
+
"# gpt-commit does not support binary files. "
|
|
297
|
+
"Please enter a commit message manually or unstage any binary files."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if output_file:
|
|
301
|
+
if commit_message:
|
|
302
|
+
Path(output_file).write_text(commit_message)
|
|
303
|
+
return 0
|
|
304
|
+
return 1
|
|
305
|
+
elif print_message:
|
|
306
|
+
click.echo(commit_message)
|
|
307
|
+
return 0
|
|
308
|
+
else:
|
|
309
|
+
return git_commit(commit_message)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@main.command()
|
|
313
|
+
@click.option(
|
|
314
|
+
"--overwrite",
|
|
315
|
+
is_flag=True,
|
|
316
|
+
help="Overwrite existing pre-commit hook if it exists",
|
|
317
|
+
)
|
|
318
|
+
def install_pre_commit(overwrite):
|
|
319
|
+
"""Install pre-commit script into git hooks directory"""
|
|
320
|
+
git_result = subprocess.run(
|
|
321
|
+
["git", "rev-parse", "--git-dir"],
|
|
322
|
+
capture_output=True,
|
|
323
|
+
text=True,
|
|
324
|
+
)
|
|
325
|
+
git_result.check_returncode()
|
|
326
|
+
|
|
327
|
+
git_dir = git_result.stdout.strip()
|
|
328
|
+
hooks_dir = Path(git_dir) / "hooks"
|
|
329
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
330
|
+
|
|
331
|
+
pre_commit = hooks_dir / "prepare-commit-msg"
|
|
332
|
+
|
|
333
|
+
if not pre_commit.exists() or overwrite:
|
|
334
|
+
pre_commit_script = Path(__file__).parent / "prepare-commit-msg"
|
|
335
|
+
pre_commit.write_text(pre_commit_script.read_text())
|
|
336
|
+
pre_commit.chmod(0o755)
|
|
337
|
+
click.echo("Installed pre-commit hook")
|
|
338
|
+
else:
|
|
339
|
+
click.echo(
|
|
340
|
+
"pre-commit hook already exists. Here's the contents we would have written:"
|
|
341
|
+
)
|
|
342
|
+
click.echo(pre_commit.read_text())
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@main.command()
|
|
346
|
+
def dump_prompts():
|
|
347
|
+
"""Dump default prompts into .aiautocommit directory for easy customization"""
|
|
348
|
+
config_dir = Path(".aiautocommit")
|
|
349
|
+
config_dir.mkdir(exist_ok=True)
|
|
350
|
+
|
|
351
|
+
diff_prompt = config_dir / DIFF_PROMPT_FILE
|
|
352
|
+
commit_prompt = config_dir / COMMIT_PROMPT_FILE
|
|
353
|
+
exclusions = config_dir / EXCLUSIONS_FILE
|
|
354
|
+
|
|
355
|
+
if not diff_prompt.exists():
|
|
356
|
+
diff_prompt.write_text(DIFF_PROMPT)
|
|
357
|
+
if not commit_prompt.exists():
|
|
358
|
+
commit_prompt.write_text(COMMIT_MSG_PROMPT)
|
|
359
|
+
if not exclusions.exists():
|
|
360
|
+
exclusions.write_text("\n".join(EXCLUDED_FILES))
|
|
361
|
+
|
|
362
|
+
click.echo(
|
|
363
|
+
"""Dumped default prompts to .aiautocommit directory:
|
|
364
|
+
|
|
365
|
+
- diff_prompt.txt: Template for generating diff summaries, this is passed to commit_prompt.txt
|
|
366
|
+
- commit_prompt.txt: Template for generating commit messages
|
|
367
|
+
- exclusions.txt: List of file patterns to exclude from processing"""
|
|
368
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: aiautocommit
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: AI generated commit messages
|
|
5
|
+
Author-email: Michael Bianco <mike@mikebian.co>
|
|
6
|
+
Project-URL: Repository, https://github.com/iloveitaly/aiautocommit
|
|
7
|
+
Keywords: git,commit,automation,ai,openai
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: click>=8.1.7
|
|
12
|
+
Requires-Dist: openai>=1.54.3
|
|
13
|
+
|
|
14
|
+
[](https://github.com/iloveitaly/aiautocommit/releases) [](https://pepy.tech/project/aiautocommit) [](https://pypi.org/project/aiautocommit)  [](https://opensource.org/licenses/MIT)
|
|
15
|
+
|
|
16
|
+
# aiautocommit
|
|
17
|
+
|
|
18
|
+
Generate intelligent commit messages using AI. aiautocommit analyzes your staged changes and creates conventional commit messages, handling both small tweaks and large changesets effectively.
|
|
19
|
+
|
|
20
|
+
Yes, there are a lot of these. Main ways this is different:
|
|
21
|
+
|
|
22
|
+
* Simple codebase
|
|
23
|
+
* Ability to easily customize prompts on a per-repo basis
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
pip install aiautocommit
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
* Generates conventional commit messages
|
|
34
|
+
* Customizable prompts and exclusions
|
|
35
|
+
* Pre-commit hook integration
|
|
36
|
+
* Supports custom config directories
|
|
37
|
+
|
|
38
|
+
## Getting Started
|
|
39
|
+
|
|
40
|
+
Set your OpenAI API key:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
export OPENAI_API_KEY=<YOUR API KEY>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Stage your changes and run aiautocommit:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
git add .
|
|
50
|
+
aiautocommit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Customization
|
|
54
|
+
|
|
55
|
+
### Using Config Directory
|
|
56
|
+
|
|
57
|
+
aiautocommit looks for configuration files in these locations (in priority order):
|
|
58
|
+
|
|
59
|
+
* .aiautocommit/ in current directory
|
|
60
|
+
* $XDG_CONFIG_HOME/aiautocommit/ (defaults to ~/.config/aiautocommit/)
|
|
61
|
+
* Custom path via aiautocommit_CONFIG environment variable
|
|
62
|
+
|
|
63
|
+
To get started with customization:
|
|
64
|
+
|
|
65
|
+
```shell
|
|
66
|
+
aiautocommit dump-prompts
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This creates a `.aiautocommit/` directory with:
|
|
70
|
+
|
|
71
|
+
* `diff_prompt.txt`: Template for generating diff summaries
|
|
72
|
+
* `commit_prompt.txt`: Template for generating commit messages
|
|
73
|
+
* `exclusions.txt`: List of files to exclude from processing
|
|
74
|
+
|
|
75
|
+
### Installing Pre-commit Hook
|
|
76
|
+
|
|
77
|
+
To automatically generate commit messages during git commits:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
aiautocommit install-pre-commit
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Environment Variables
|
|
84
|
+
|
|
85
|
+
* `OPENAI_API_KEY`: Your OpenAI API key
|
|
86
|
+
* `aiautocommit_MODEL`: AI model to use (default: gpt-4-mini)
|
|
87
|
+
* `aiautocommit_CONFIG`: Custom config directory path
|
|
88
|
+
* `LOG_LEVEL`: Logging verbosity
|
|
89
|
+
* `aiautocommit_LOG_PATH`: Custom log file path
|
|
90
|
+
|
|
91
|
+
## Privacy Disclaimer
|
|
92
|
+
|
|
93
|
+
`gpt-commit` uses the [OpenAI API](https://platform.openai.com/docs) to generate commit messages. Both file names and contents from files that contain staged changes will be shared with OpenAI when using `gpt-commit`. OpenAI will process this data according to their [terms of use](https://openai.com/policies/terms-of-use) and [API data usage policies](https://openai.com/policies/api-data-usage-policies). On March 1st 2023 OpenAI pledged that by default, they would not use data submitted by customers via their API to train or improve their models, and that this data will be retained for a maximum of 30 days, after which it will be deleted.
|
|
94
|
+
|
|
95
|
+
## Special Thanks
|
|
96
|
+
|
|
97
|
+
[This project](https://github.com/markuswt/gpt-commit) inspired this project. It had a very simple codebase. I've taken the idea and expanded it to include a lot more features, specifically per-project prompt customization.
|
|
98
|
+
|
|
99
|
+
## Related Projects
|
|
100
|
+
|
|
101
|
+
I looked at a bunch of projects before building this one.
|
|
102
|
+
|
|
103
|
+
- https://github.com/abi/aiautocommit - python, inactive. Many files, not simple.
|
|
104
|
+
- https://github.com/Sett17/turboCommit - rust, inactive.
|
|
105
|
+
- https://github.com/Nutlope/aicommits - typescript. Node is so slow and I hate working with it.
|
|
106
|
+
- https://github.com/Elhameed/aicommits - python
|
|
107
|
+
- https://github.com/zurawiki/gptcommit - active, rust. Too complicated and no prompt customization.
|
|
108
|
+
- https://github.com/ywkim/gpt-commit - lisp, inactive.
|
|
109
|
+
- https://github.com/markuswt/gpt-commit - single file, python based. No commits > 1yr. Very simple codebase.
|
|
110
|
+
- https://github.com/josenerydev/gpt-commit - also python
|
|
111
|
+
- https://github.com/di-sukharev/opencommit - has conventional commit structure
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
aiautocommit/__init__.py
|
|
5
|
+
aiautocommit.egg-info/PKG-INFO
|
|
6
|
+
aiautocommit.egg-info/SOURCES.txt
|
|
7
|
+
aiautocommit.egg-info/dependency_links.txt
|
|
8
|
+
aiautocommit.egg-info/entry_points.txt
|
|
9
|
+
aiautocommit.egg-info/requires.txt
|
|
10
|
+
aiautocommit.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiautocommit
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aiautocommit"
|
|
3
|
+
version = "0.6.0"
|
|
4
|
+
description = "AI generated commit messages"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = ["click>=8.1.7", "openai>=1.54.3"]
|
|
8
|
+
authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
|
|
9
|
+
keywords = ["git", "commit", "automation", "ai", "openai"]
|
|
10
|
+
urls = { "Repository" = "https://github.com/iloveitaly/aiautocommit" }
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
aiautocommit = "aiautocommit:main"
|
|
14
|
+
|
|
15
|
+
# https://github.com/astral-sh/uv/issues/5200
|
|
16
|
+
[tool.uv]
|
|
17
|
+
package = true
|