Skip to content

Commit e712376

Browse files
authored
Update Status file (#4)
Update auto_commit.py and README.md
1 parent 076ead4 commit e712376

File tree

2 files changed

+95
-46
lines changed

2 files changed

+95
-46
lines changed

README.md

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11

22
### Overview
33

4-
This is a Python script designed to read git diff output and leverage Ollama to generate commit messages for each file. When you run this script, commits will be automatically created for all staged files.
4+
`auto_commit.py` is a Python script that automates the process of generating concise, conventional commit messages for your Git repository changes using an LLM (via Ollama). It can commit all changes at once or commit each file separately, with AI-generated commit messages based on the actual diffs.
55

66
<img src="https://github.com/user-attachments/assets/f39344db-10c5-4dbc-a3e6-2ce275d52004" />
77

8+
### Features
9+
10+
- AI-generated commit messages: Uses an LLM to analyze Git diffs and suggest relevant, conventional commit messages (e.g., feat:, fix:, chore:).
11+
- Commit all or per file: Optionally commits all changes together or each file separately, each with its own message.
12+
- Handles new, modified, and deleted files.
13+
- Works with both staged and unstaged changes.
14+
15+
### How It Works
16+
17+
1. Detects changed files (staged and unstaged).
18+
2. Gets diffs for each file.
19+
3. Sends diffs and file info to the LLM via Ollama to generate a commit message.
20+
4. Commits changes using the generated message(s).
21+
22+
823
### Installation
924

25+
- Python 3.7+
26+
1027
- [Olama](https://ollama.com/download)
1128

1229
- [Ollama Model](https://ollama.com/library/gemma3)
@@ -17,27 +34,43 @@ This script currently uses the `gemma3:4b` model.
1734
ollama run gemma3:4b
1835
```
1936

20-
- [Python Ollama](https://github.com/ollama/ollama-python)
37+
- [Ollama Python client](https://github.com/ollama/ollama-python)
2138
```
2239
pip install ollama
2340
```
2441

42+
- Git installed and available in PATH
43+
2544
### Usage
2645

27-
Place the `auto_commit.py` file in the root directory of your project.
2846
If ollama server is running, run the script using the command
2947

3048
```
31-
python3 auto_commit.py
49+
python auto_commit.py <repository_path> [single]
50+
```
51+
52+
- <repository_path>: Path to your Git repository.
53+
- single (optional): If provided, commits each file separately; otherwise, all changes are committed together.
54+
55+
### Example
56+
57+
Commit all changes in git:
58+
59+
```
60+
python3 auto_commit.py /Users/ttpho/Documents/GitHub/chat
3261
```
3362

34-
Create commit per file
63+
Commit each file separately:
3564

3665
```
37-
python3 auto_commit.py single
66+
python3 auto_commit.py /Users/ttpho/Documents/GitHub/chat single
3867
```
3968

69+
### Notes
4070

71+
- The script uses the `gemma3:4b` model by default. You can change the model by editing the model variable.
72+
- Commit messages are limited to 72 characters and follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style.
73+
- The script will print the generated commit messages before committing.
4174

4275
### Miscellaneous
4376

auto_commit.py

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import asyncio
2+
import os
23
import subprocess
34
import sys
45
from ollama import AsyncClient
56

67
model = "gemma3:4b"
78
prompt = f"""
8-
Given the following Git diff and the list of changed files (with file types), suggest a single concise and relevant commit message that best summarizes all the changes made. Use a conventional commit style (e.g., feat:, fix:, chore:, docs:, refactor:). The message should be no longer than 72 characters.
9+
Given the following Git diff and the list of changed files (with file types), suggest a single concise and relevant commit message that best summarizes all the changes made.
10+
Use a conventional commit style (e.g., feat:, fix:, chore:, docs:, refactor:).
11+
The message should be no longer than 72 characters.
912
Just return the commit messages without any additional text or explanation, without any Markdown formatting.
1013
Input:
1114
Git Diff:
@@ -21,37 +24,46 @@
2124
"""
2225
client = AsyncClient()
2326

24-
25-
async def get_changed_files():
27+
async def get_changed_files(repository_path):
2628
# Git add all
2729
subprocess.run(
2830
["git", "add", "."],
29-
capture_output=True, text=True
31+
capture_output=True, text=True, cwd=repository_path
3032
)
3133
# Get all staged and unstaged files (excluding untracked)
3234
result = subprocess.run(
3335
["git", "diff", "--name-only"],
34-
capture_output=True, text=True
36+
capture_output=True, text=True, cwd=repository_path
3537
)
3638
unstaged = set(result.stdout.splitlines())
3739
result = subprocess.run(
3840
["git", "diff", "--name-only", "--staged"],
39-
capture_output=True, text=True
41+
capture_output=True, text=True, cwd=repository_path
4042
)
4143
staged = set(result.stdout.splitlines())
4244
# Union of both sets
4345
return sorted(unstaged | staged)
4446

4547

46-
async def get_diff_for_file(filename, staged=False):
48+
async def get_diff_for_file(file_path, repository_path, staged=False):
4749
cmd = ["git", "diff"]
4850
if staged:
4951
cmd.append("--staged")
5052
cmd.append("--")
51-
cmd.append(filename)
52-
result = subprocess.run(cmd, capture_output=True, text=True)
53+
cmd.append(file_path)
54+
result = subprocess.run(cmd, capture_output=True, text=True, cwd=repository_path)
5355
return result.stdout
5456

57+
def replace_backticks(text):
58+
"""Replaces all occurrences of ``` with an empty string.
59+
60+
Args:
61+
text: The input string.
62+
63+
Returns:
64+
The string with all ``` delimiters replaced by empty strings.
65+
"""
66+
return text.replace("```", "")
5567

5668
async def get_commit_messages(diff, file_with_type):
5769
# Use the Ollama chat model to get commit messages
@@ -65,12 +77,13 @@ async def get_commit_messages(diff, file_with_type):
6577
},
6678
]
6779
response = await client.chat(model=model, messages=messages)
68-
return response['message']['content']
80+
content = response['message']['content']
81+
return replace_backticks(content)
6982
except Exception:
7083
return ""
7184

7285

73-
def status_file(file_path):
86+
def status_file(file_path, repository_path):
7487
"""
7588
Creates a descriptive commit message for changes to a single file,
7689
detecting if it was added, modified, or deleted.
@@ -79,15 +92,15 @@ def status_file(file_path):
7992
# Check if the file is new (not tracked yet)
8093
status_new_process = subprocess.run(
8194
['git', 'status', '--porcelain', file_path],
82-
capture_output=True, text=True, check=True
95+
capture_output=True, text=True, check=True, cwd=repository_path,
8396
)
8497
if status_new_process.stdout.strip().startswith("??"):
8598
return "Add"
8699

87100
# Check if the file was deleted
88101
status_deleted_process = subprocess.run(
89102
['git', 'diff', '--staged', '--name-status', file_path],
90-
capture_output=True, text=True, check=True,
103+
capture_output=True, text=True, check=True, cwd=repository_path,
91104
)
92105
if status_deleted_process.stdout.strip().startswith("D"):
93106
return "Remove"
@@ -99,12 +112,13 @@ def status_file(file_path):
99112
return ""
100113

101114

102-
async def diff_single_file(file):
115+
async def diff_single_file(file_path, repository_path):
103116
commit_messages = []
104-
status = status_file(file).strip()
105-
file_with_type = f"{file} : {status}"
106-
unstaged_diff = (await get_diff_for_file(file, staged=False)).strip()
107-
staged_diff = (await get_diff_for_file(file, staged=True)).strip()
117+
status = status_file(file_path, repository_path).strip()
118+
file_name = os.path.basename(file_path).strip()
119+
file_with_type = f"{status} : {file_name}"
120+
unstaged_diff = (await get_diff_for_file(file_path, repository_path, staged=False)).strip()
121+
staged_diff = (await get_diff_for_file(file_path, repository_path, staged=True)).strip()
108122
messages_staged_diff = (await get_commit_messages(staged_diff, file_with_type)).strip()
109123
messages_unstaged_diff = (await get_commit_messages(unstaged_diff, file_with_type)).strip()
110124
if messages_staged_diff:
@@ -114,20 +128,20 @@ async def diff_single_file(file):
114128
return commit_messages
115129

116130

117-
async def git_commit_everything(message):
131+
async def git_commit_everything(message, repository_path):
118132
"""
119133
Stages all changes (including new, modified, deleted files), commits with the given message,
120134
and pushes the commit to the current branch on the default remote ('origin').
121135
"""
122136
if not message:
123137
return
124138
# Stage all changes (new, modified, deleted)
125-
subprocess.run(['git', 'add', '-A'], check=True)
139+
subprocess.run(['git', 'add', '-A'], check=True, cwd=repository_path,)
126140
# Commit with the provided message
127-
subprocess.run(['git', 'commit', '-m', message], check=True)
141+
subprocess.run(['git', 'commit', '-m', message], check=True, cwd=repository_path,)
128142

129143

130-
async def git_commit_file(file, message):
144+
async def git_commit_file(file_path, repository_path, message):
131145
"""
132146
Stages all changes (including new, modified, deleted files), commits with the given message,
133147
and pushes the commit to the current branch on the default remote ('origin').
@@ -136,42 +150,44 @@ async def git_commit_file(file, message):
136150
return
137151

138152
try:
139-
subprocess.run(['git', 'add', file], check=True)
153+
subprocess.run(['git', 'add', file_path], check=True, cwd=repository_path,)
140154
except:
141155
print("An exception occurred")
142156
# Commit with the provided message
143-
subprocess.run(['git', 'commit', file, '-m', message], check=True)
157+
subprocess.run(['git', 'commit', file_path, '-m', message], check=True, cwd=repository_path,)
144158

145159

146-
async def commit_comment_per_file(files):
147-
for file in files:
148-
commit_messages = await diff_single_file(file)
160+
async def commit_comment_per_file(all_file_path, repository_path):
161+
for file_path in all_file_path:
162+
commit_messages = await diff_single_file(file_path, repository_path)
149163
commit_messages_text = "\n".join(commit_messages)
150-
print(f"{file}: {commit_messages_text}")
151-
await git_commit_file(file, commit_messages_text)
164+
print(f"{file_path}: {commit_messages_text}")
165+
await git_commit_file(file_path, repository_path, commit_messages_text)
152166

153167

154-
async def comit_comment_all(files):
168+
async def commit_comment_all(all_file_path, repository_path):
155169
all_message = []
156-
for file in files:
157-
commit_messages = await diff_single_file(file)
170+
for file_path in all_file_path:
171+
commit_messages = await diff_single_file(file_path, repository_path)
158172
commit_messages_text = "\n".join(commit_messages)
159-
print(f"{file}: {commit_messages_text}")
173+
print(f"{file_path}: {commit_messages_text}")
160174
all_message.extend(commit_messages)
161-
await git_commit_everything(message="\n".join(all_message))
175+
await git_commit_everything(message="\n".join(all_message), repository_path = repository_path)
162176

163177

164178
async def main():
165-
files = await get_changed_files()
166-
if not files:
179+
repository_path = sys.argv[1] if len(sys.argv) > 1 else None
180+
is_commit_per_file = True if (len(sys.argv) > 2 and sys.argv[2] == 'single') else False
181+
182+
all_file_path = await get_changed_files(repository_path)
183+
if not all_file_path:
167184
print("No changes detected.")
168185
return
169-
is_commit_per_file = True if (
170-
len(sys.argv) > 1 and sys.argv[1] == 'single') else False
186+
171187
if is_commit_per_file:
172-
await commit_comment_per_file(files)
188+
await commit_comment_per_file(all_file_path, repository_path)
173189
else:
174-
await comit_comment_all(files)
190+
await commit_comment_all(all_file_path, repository_path)
175191

176192
if __name__ == "__main__":
177193
asyncio.run(main())

0 commit comments

Comments
 (0)