1
1
import asyncio
2
+ import os
2
3
import subprocess
3
4
import sys
4
5
from ollama import AsyncClient
5
6
6
7
model = "gemma3:4b"
7
8
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.
9
12
Just return the commit messages without any additional text or explanation, without any Markdown formatting.
10
13
Input:
11
14
Git Diff:
21
24
"""
22
25
client = AsyncClient ()
23
26
24
-
25
- async def get_changed_files ():
27
+ async def get_changed_files (repository_path ):
26
28
# Git add all
27
29
subprocess .run (
28
30
["git" , "add" , "." ],
29
- capture_output = True , text = True
31
+ capture_output = True , text = True , cwd = repository_path
30
32
)
31
33
# Get all staged and unstaged files (excluding untracked)
32
34
result = subprocess .run (
33
35
["git" , "diff" , "--name-only" ],
34
- capture_output = True , text = True
36
+ capture_output = True , text = True , cwd = repository_path
35
37
)
36
38
unstaged = set (result .stdout .splitlines ())
37
39
result = subprocess .run (
38
40
["git" , "diff" , "--name-only" , "--staged" ],
39
- capture_output = True , text = True
41
+ capture_output = True , text = True , cwd = repository_path
40
42
)
41
43
staged = set (result .stdout .splitlines ())
42
44
# Union of both sets
43
45
return sorted (unstaged | staged )
44
46
45
47
46
- async def get_diff_for_file (filename , staged = False ):
48
+ async def get_diff_for_file (file_path , repository_path , staged = False ):
47
49
cmd = ["git" , "diff" ]
48
50
if staged :
49
51
cmd .append ("--staged" )
50
52
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 )
53
55
return result .stdout
54
56
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 ("```" , "" )
55
67
56
68
async def get_commit_messages (diff , file_with_type ):
57
69
# Use the Ollama chat model to get commit messages
@@ -65,12 +77,13 @@ async def get_commit_messages(diff, file_with_type):
65
77
},
66
78
]
67
79
response = await client .chat (model = model , messages = messages )
68
- return response ['message' ]['content' ]
80
+ content = response ['message' ]['content' ]
81
+ return replace_backticks (content )
69
82
except Exception :
70
83
return ""
71
84
72
85
73
- def status_file (file_path ):
86
+ def status_file (file_path , repository_path ):
74
87
"""
75
88
Creates a descriptive commit message for changes to a single file,
76
89
detecting if it was added, modified, or deleted.
@@ -79,15 +92,15 @@ def status_file(file_path):
79
92
# Check if the file is new (not tracked yet)
80
93
status_new_process = subprocess .run (
81
94
['git' , 'status' , '--porcelain' , file_path ],
82
- capture_output = True , text = True , check = True
95
+ capture_output = True , text = True , check = True , cwd = repository_path ,
83
96
)
84
97
if status_new_process .stdout .strip ().startswith ("??" ):
85
98
return "Add"
86
99
87
100
# Check if the file was deleted
88
101
status_deleted_process = subprocess .run (
89
102
['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 ,
91
104
)
92
105
if status_deleted_process .stdout .strip ().startswith ("D" ):
93
106
return "Remove"
@@ -99,12 +112,13 @@ def status_file(file_path):
99
112
return ""
100
113
101
114
102
- async def diff_single_file (file ):
115
+ async def diff_single_file (file_path , repository_path ):
103
116
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 ()
108
122
messages_staged_diff = (await get_commit_messages (staged_diff , file_with_type )).strip ()
109
123
messages_unstaged_diff = (await get_commit_messages (unstaged_diff , file_with_type )).strip ()
110
124
if messages_staged_diff :
@@ -114,20 +128,20 @@ async def diff_single_file(file):
114
128
return commit_messages
115
129
116
130
117
- async def git_commit_everything (message ):
131
+ async def git_commit_everything (message , repository_path ):
118
132
"""
119
133
Stages all changes (including new, modified, deleted files), commits with the given message,
120
134
and pushes the commit to the current branch on the default remote ('origin').
121
135
"""
122
136
if not message :
123
137
return
124
138
# 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 , )
126
140
# 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 , )
128
142
129
143
130
- async def git_commit_file (file , message ):
144
+ async def git_commit_file (file_path , repository_path , message ):
131
145
"""
132
146
Stages all changes (including new, modified, deleted files), commits with the given message,
133
147
and pushes the commit to the current branch on the default remote ('origin').
@@ -136,42 +150,44 @@ async def git_commit_file(file, message):
136
150
return
137
151
138
152
try :
139
- subprocess .run (['git' , 'add' , file ], check = True )
153
+ subprocess .run (['git' , 'add' , file_path ], check = True , cwd = repository_path , )
140
154
except :
141
155
print ("An exception occurred" )
142
156
# 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 , )
144
158
145
159
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 )
149
163
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 )
152
166
153
167
154
- async def comit_comment_all ( files ):
168
+ async def commit_comment_all ( all_file_path , repository_path ):
155
169
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 )
158
172
commit_messages_text = "\n " .join (commit_messages )
159
- print (f"{ file } : { commit_messages_text } " )
173
+ print (f"{ file_path } : { commit_messages_text } " )
160
174
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 )
162
176
163
177
164
178
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 :
167
184
print ("No changes detected." )
168
185
return
169
- is_commit_per_file = True if (
170
- len (sys .argv ) > 1 and sys .argv [1 ] == 'single' ) else False
186
+
171
187
if is_commit_per_file :
172
- await commit_comment_per_file (files )
188
+ await commit_comment_per_file (all_file_path , repository_path )
173
189
else :
174
- await comit_comment_all ( files )
190
+ await commit_comment_all ( all_file_path , repository_path )
175
191
176
192
if __name__ == "__main__" :
177
193
asyncio .run (main ())
0 commit comments