11import asyncio
2+ import os
23import subprocess
34import sys
45from ollama import AsyncClient
56
67model = "gemma3:4b"
78prompt = 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.
912Just return the commit messages without any additional text or explanation, without any Markdown formatting.
1013Input:
1114 Git Diff:
2124"""
2225client = 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
5668async 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
164178async 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
176192if __name__ == "__main__" :
177193 asyncio .run (main ())
0 commit comments