XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 1
返回提交历史

XFEstudio/gpt4free

feat: enhance commit tool with advanced options and error handling

- Added command-line argument parsing with options for model selection, editing, and no-commit mode - Implemented fallback mechanism with multiple AI models if the primary model fails - Added spinner display to indicate progress during API calls - Created functions to filter sensitive data from diffs before sending to API - Added diff truncation capabilities for handling large changesets - Implemented commit message editing in user's configured editor - Added model listing functionality to show available AI options - Enhanced error handling with retries and better error reporting - Added keyboard interrupt handling for graceful termination - Improved type annotations throughout the codebase - Added constants for configuration parameters like retry delay and max diff size

2e13110e
kqlio67 <kqlio67@users.noreply.github.com>
提交于

代码差异

1 个文件 +271 -35
Modified etc/tool/commit.py +271 -35
@@ -7,13 +7,52 @@ staged changes. It analyzes the git diff and suggests appropriate commit
7 7 messages following conventional commit format.
8 8
9 9 Usage:
10 python -m etc.tool.commit
10 python -m etc.tool.commit [options]
11
12 Options:
13 --model MODEL Specify the AI model to use (default: claude-3.7-sonnet)
14 --edit Edit the generated commit message before committing
15 --no-commit Generate message only without committing
16 --list-models List available AI models and exit
17 --help Show this help message
11 18 """
12 19 import subprocess
13 20 import sys
21 import os
22 import argparse
23 import tempfile
24 import time
25 from typing import Optional, Dict, Any, List, Tuple
26
14 27 from g4f.client import Client
28 from g4f.models import ModelUtils
15 29
16 def get_git_diff():
30 # Constants
31 DEFAULT_MODEL = "claude-3.7-sonnet"
32 FALLBACK_MODELS = ["claude-3.5-sonnet", "o1", "o3-mini", "gpt-4o"]
33 MAX_DIFF_SIZE = None # Set to None to disable truncation, or a number for character limit
34 MAX_RETRIES = 3
35 RETRY_DELAY = 2 # Seconds
36
37 def parse_arguments():
38 """Parse command line arguments"""
39 parser = argparse.ArgumentParser(
40 description="AI Commit Message Generator",
41 formatter_class=argparse.RawDescriptionHelpFormatter,
42 epilog=__doc__
43 )
44 parser.add_argument("--model", type=str, default=DEFAULT_MODEL,
45 help=f"AI model to use (default: {DEFAULT_MODEL})")
46 parser.add_argument("--edit", action="store_true",
47 help="Edit the generated commit message before committing")
48 parser.add_argument("--no-commit", action="store_true",
49 help="Generate message only without committing")
50 parser.add_argument("--list-models", action="store_true",
51 help="List available AI models and exit")
52
53 return parser.parse_args()
54
55 def get_git_diff() -> Optional[str]:
17 56 """Get the current git diff for staged changes"""
18 57 try:
19 58 diff_process = subprocess.run(
@@ -21,20 +60,102 @@ def get_git_diff():
21 60 capture_output=True,
22 61 text=True
23 62 )
63 if diff_process.returncode != 0:
64 print(f"Error: git diff command failed with code {diff_process.returncode}")
65 return None
66
24 67 return diff_process.stdout
25 68 except Exception as e:
26 69 print(f"Error running git diff: {e}")
27 70 return None
28 71
29 def generate_commit_message(diff_text):
72 def truncate_diff(diff_text: str, max_size: int = MAX_DIFF_SIZE) -> str:
73 """Truncate diff if it's too large, preserving the most important parts"""
74 if max_size is None or len(diff_text) <= max_size:
75 return diff_text
76
77 print(f"Warning: Diff is large ({len(diff_text)} chars), truncating to {max_size} chars")
78
79 # Split by file sections and keep as many complete files as possible
80 sections = diff_text.split("diff --git ")
81 header = sections[0]
82 file_sections = ["diff --git " + s for s in sections[1:]]
83
84 result = header
85 for section in file_sections:
86 if len(result) + len(section) <= max_size:
87 result += section
88 else:
89 break
90
91 return result
92
93 def filter_sensitive_data(diff_text: str) -> str:
94 """Filter out potentially sensitive data from the diff"""
95 # List of patterns that might indicate sensitive data
96 sensitive_patterns = [
97 ("password", "***REDACTED***"),
98 ("secret", "***REDACTED***"),
99 ("token", "***REDACTED***"),
100 ("api_key", "***REDACTED***"),
101 ("apikey", "***REDACTED***"),
102 ("auth", "***REDACTED***"),
103 ("credential", "***REDACTED***"),
104 ]
105
106 # Simple pattern matching - in a real implementation, you might want more sophisticated regex
107 filtered_text = diff_text
108 for pattern, replacement in sensitive_patterns:
109 # Only replace if it looks like an assignment or declaration
110 filtered_text = filtered_text.replace(f'{pattern}="', f'{pattern}="{replacement}')
111 filtered_text = filtered_text.replace(f"{pattern}='", f"{pattern}='{replacement}'")
112 filtered_text = filtered_text.replace(f"{pattern}:", f"{pattern}: {replacement}")
113 filtered_text = filtered_text.replace(f"{pattern} =", f"{pattern} = {replacement}")
114
115 return filtered_text
116
117 def show_spinner(duration: int = None):
118 """Display a simple spinner to indicate progress"""
119 import itertools
120 import threading
121 import time
122
123 spinner = itertools.cycle(['-', '/', '|', '\\'])
124 stop_spinner = threading.Event()
125
126 def spin():
127 while not stop_spinner.is_set():
128 sys.stdout.write(f"\rGenerating commit message... {next(spinner)} ")
129 sys.stdout.flush()
130 time.sleep(0.1)
131
132 spinner_thread = threading.Thread(target=spin)
133 spinner_thread.start()
134
135 try:
136 if duration:
137 time.sleep(duration)
138 stop_spinner.set()
139 return stop_spinner
140 except:
141 stop_spinner.set()
142 raise
143
144 def generate_commit_message(diff_text: str, model: str = DEFAULT_MODEL) -> Optional[str]:
30 145 """Generate a commit message based on the git diff"""
31 146 if not diff_text or diff_text.strip() == "":
32 147 return "No changes staged for commit"
33 148
149 # Filter sensitive data
150 filtered_diff = filter_sensitive_data(diff_text)
151
152 # Truncate if necessary
153 truncated_diff = truncate_diff(filtered_diff)
154
34 155 client = Client()
35 156
36 157 prompt = f"""
37 {diff_text}
158 {truncated_diff}
38 159 ```
39 160
40 161 Analyze ONLY the exact changes in this git diff and create a precise commit message.
@@ -56,50 +177,165 @@ def generate_commit_message(diff_text):
56 177 IMPORTANT: Be 100% factual. Only mention code that was actually changed. Never invent or assume changes not shown in the diff. If unsure about a change's purpose, describe what changed rather than why. Output nothing except for the commit message, and don't surround it in quotes.
57 178 """
58 179
59 try:
60 response = client.chat.completions.create(
61 model="claude-3.7-sonnet",
62 messages=[{"role": "user", "content": prompt}]
63 )
64
65 return response.choices[0].message.content.strip()
66 except Exception as e:
67 print(f"Error generating commit message: {e}")
68 return None
180 for attempt in range(MAX_RETRIES):
181 try:
182 # Start spinner
183 spinner = show_spinner()
184
185 # Make API call
186 response = client.chat.completions.create(
187 model=model,
188 messages=[{"role": "user", "content": prompt}]
189 )
190
191 # Stop spinner and clear line
192 spinner.set()
193 sys.stdout.write("\r" + " " * 50 + "\r")
194 sys.stdout.flush()
195
196 return response.choices[0].message.content.strip()
197 except Exception as e:
198 # Stop spinner if it's running
199 if 'spinner' in locals() and spinner:
200 spinner.set()
201 sys.stdout.write("\r" + " " * 50 + "\r")
202 sys.stdout.flush()
203
204 print(f"Error generating commit message (attempt {attempt+1}/{MAX_RETRIES}): {e}")
205 if attempt < MAX_RETRIES - 1:
206 print(f"Retrying in {RETRY_DELAY} seconds...")
207 time.sleep(RETRY_DELAY)
208 # Try with a fallback model if available
209 if attempt < len(FALLBACK_MODELS):
210 fallback = FALLBACK_MODELS[attempt]
211 print(f"Trying with fallback model: {fallback}")
212 model = fallback
213
214 return None
69 215
70 def main():
71 print("Fetching git diff...")
72 diff = get_git_diff()
216 def edit_commit_message(message: str) -> str:
217 """Allow user to edit the commit message in their default editor"""
218 with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.txt') as temp:
219 temp.write(message)
220 temp_path = temp.name
73 221
74 if diff is None:
75 print("Failed to get git diff. Are you in a git repository?")
76 sys.exit(1)
222 # Get the default editor from git config or environment
223 try:
224 editor = subprocess.run(
225 ["git", "config", "--get", "core.editor"],
226 capture_output=True, text=True
227 ).stdout.strip()
228 except:
229 editor = os.environ.get('EDITOR', 'vim')
230
231 if not editor:
232 editor = 'vim' # Default fallback
233
234 # Open the editor
235 try:
236 subprocess.run([editor, temp_path], check=True)
237 except subprocess.CalledProcessError:
238 print("Warning: Editor exited with an error")
239 except FileNotFoundError:
240 print(f"Warning: Editor '{editor}' not found, falling back to basic input")
241 print("Edit your commit message (Ctrl+D when done):")
242 edited_message = sys.stdin.read().strip()
243 os.unlink(temp_path)
244 return edited_message
77 245
78 if diff.strip() == "":
79 print("No changes staged for commit. Stage changes with 'git add' first.")
80 sys.exit(0)
246 # Read the edited message
247 with open(temp_path, 'r') as temp:
248 edited_message = temp.read()
81 249
82 print("Generating commit message...")
83 commit_message = generate_commit_message(diff)
250 # Clean up
251 os.unlink(temp_path)
84 252
85 if commit_message:
253 return edited_message
254
255 def list_available_models() -> List[str]:
256 """List available AI models that can be used for commit message generation"""
257 # Filter for text models that are likely to be good for code understanding
258 relevant_models = []
259
260 for model_name, model in ModelUtils.convert.items():
261 # Skip image, audio, and video models
262 if model_name and not model_name.startswith(('dall', 'sd-', 'flux', 'midjourney')):
263 relevant_models.append(model_name)
264
265 return sorted(relevant_models)
266
267 def make_commit(message: str) -> bool:
268 """Make a git commit with the provided message"""
269 try:
270 subprocess.run(
271 ["git", "commit", "-m", message],
272 check=True
273 )
274 return True
275 except subprocess.CalledProcessError as e:
276 print(f"Error making commit: {e}")
277 return False
278
279 def main():
280 """Main function"""
281 try:
282 args = parse_arguments()
283
284 # If --list-models is specified, list available models and exit
285 if args.list_models:
286 print("Available AI models for commit message generation:")
287 for model in list_available_models():
288 print(f" - {model}")
289 sys.exit(0)
290
291 print("Fetching git diff...")
292 diff = get_git_diff()
293
294 if diff is None:
295 print("Failed to get git diff. Are you in a git repository?")
296 sys.exit(1)
297
298 if diff.strip() == "":
299 print("No changes staged for commit. Stage changes with 'git add' first.")
300 sys.exit(0)
301
302 print(f"Using model: {args.model}")
303 commit_message = generate_commit_message(diff, args.model)
304
305 if not commit_message:
306 print("Failed to generate commit message after multiple attempts.")
307 sys.exit(1)
308
86 309 print("\nGenerated commit message:")
87 310 print("-" * 50)
88 311 print(commit_message)
89 312 print("-" * 50)
90 313
314 if args.edit:
315 print("\nOpening editor to modify commit message...")
316 commit_message = edit_commit_message(commit_message)
317 print("\nEdited commit message:")
318 print("-" * 50)
319 print(commit_message)
320 print("-" * 50)
321
322 if args.no_commit:
323 print("\nCommit message generated but not committed (--no-commit flag used).")
324 sys.exit(0)
325
91 326 user_input = input("\nDo you want to use this commit message? (y/n): ")
92 327 if user_input.lower() == 'y':
93 try:
94 subprocess.run(
95 ["git", "commit", "-m", commit_message],
96 check=True
97 )
328 if make_commit(commit_message):
98 329 print("Commit successful!")
99 except subprocess.CalledProcessError as e:
100 print(f"Error making commit: {e}")
101 else:
102 print("Failed to generate commit message.")
330 else:
331 print("Commit failed.")
332 sys.exit(1)
333 else:
334 print("Commit aborted.")
335
336 except KeyboardInterrupt:
337 print("\nOperation cancelled by user.")
338 sys.exit(130) # Standard exit code for SIGINT
103 339
104 340 if __name__ == "__main__":
105 341 main()