gpt4free/etc/tool/commit.py
kqlio67 f4cd4890d3
feat: enhance provider support and add PuterJS provider (#2999)
* feat: enhance provider support and add PuterJS provider

- Add new PuterJS provider with extensive model support and authentication handling
- Add three new OIVSCode providers (OIVSCodeSer2, OIVSCodeSer5, OIVSCodeSer0501)
- Fix Blackbox provider with improved model handling and session generation
- Update model aliases across multiple providers for consistency
- Mark DDG provider as not working
- Move TypeGPT to not_working directory
- Fix model name formatting in DeepInfraChat and other providers (qwen3 → qwen-3)
- Add get_model method to LambdaChat and other providers for better model alias handling
- Add ModelNotFoundError import to providers that need it
- Update model definitions in models.py with new providers and aliases
- Fix client/stubs.py to allow arbitrary types in ChatCompletionMessage

* Fix conflicts g4f/Provider/needs_auth/Grok.py

* fix: update Blackbox provider default settings

- Changed  parameter to use only the passed value without fallback to 1024
- Set  to  instead of  in request payload

* feat: add WeWordle provider with gpt-4 support

- Created new WeWordle.py provider file implementing AsyncGeneratorProvider
- Added WeWordle class with API endpoint at wewordle.org/gptapi/v1/web/turbo
- Set provider properties: working=True, needs_auth=False, supports_stream=True
- Configured default_model as 'gpt-4' with retry mechanism for API requests
- Implemented URL sanitization logic to handle malformed URLs
- Added response parsing for different JSON response formats
- Added WeWordle to Provider/__init__.py imports
- Added WeWordle to default model providers list in models.py
- Added WeWordle to gpt_4 best_provider list in models.py

* feat: add DocsBot provider with GPT-4o support

- Added new DocsBot.py provider file implementing AsyncGeneratorProvider and ProviderModelMixin
- Created Conversation class extending JsonConversation to track conversation state
- Implemented create_async_generator method with support for:
  - Streaming and non-streaming responses
  - System messages
  - Message history
  - Image handling via data URIs
  - Conversation tracking
- Added DocsBot to Provider/__init__.py imports
- Added DocsBot to default and default_vision model providers in models.py
- Added DocsBot as a provider for gpt_4o model in models.py
- Set default_model and vision support to 'gpt-4o'
- Implemented API endpoint communication with docsbot.ai

* feat: add OpenAIFM provider and update audio model references

- Added new OpenAIFM provider in g4f/Provider/audio/OpenAIFM.py for text-to-speech functionality
- Updated PollinationsAI.py to rename "gpt-4o-audio" to "gpt-4o-mini-audio"
- Added OpenAIFM to audio provider imports in g4f/Provider/audio/__init__.py
- Modified save_response_media() in g4f/image/copy_images.py to handle source_url separately from media_url
- Added new gpt_4o_mini_tts AudioModel in g4f/models.py with OpenAIFM as best provider
- Updated ModelUtils dictionary in models.py to include both gpt_4o_mini_audio and gpt_4o_mini_tts

* fix: improve PuterJS provider and add Gemini to best providers

- Changed client_id generation in PuterJS from time-based to UUID format
- Fixed duplicate json import in PuterJS.py
- Added uuid module import in PuterJS.py
- Changed host header from "api.puter.com" to "puter.com"
- Modified error handling to use Exception instead of RateLimitError
- Added Gemini to best_provider list for gemini-2.5-flash model
- Added Gemini to best_provider list for gemini-2.5-pro model
- Fixed missing newline at end of Gemini.py file

---------

Co-authored-by: kqlio67 <kqlio67.noreply.github.com>
2025-05-19 11:44:15 +02:00

344 lines
12 KiB
Python
Executable file

#!/usr/bin/env python3
"""
AI Commit Message Generator using gpt4free (g4f)
This tool uses AI to generate meaningful git commit messages based on
staged changes. It analyzes the git diff and suggests appropriate commit
messages following conventional commit format.
Usage:
python -m etc.tool.commit [options]
Options:
--model MODEL Specify the AI model to use
--edit Edit the generated commit message before committing
--no-commit Generate message only without committing
--list-models List available AI models and exit
--help Show this help message
"""
import subprocess
import sys
import os
import argparse
import tempfile
import time
from typing import Optional, Any, List
from g4f.client import Client
from g4f.models import ModelUtils
from g4f import debug
debug.logging = True
# Constants
DEFAULT_MODEL = "gpt-4o"
FALLBACK_MODELS = []
MAX_DIFF_SIZE = None # Set to None to disable truncation, or a number for character limit
MAX_RETRIES = 3
RETRY_DELAY = 2 # Seconds
def parse_arguments():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(
description="AI Commit Message Generator",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument("--model", type=str, default=DEFAULT_MODEL,
help=f"AI model to use (default: {DEFAULT_MODEL})")
parser.add_argument("--edit", action="store_true",
help="Edit the generated commit message before committing")
parser.add_argument("--no-commit", action="store_true",
help="Generate message only without committing")
parser.add_argument("--list-models", action="store_true",
help="List available AI models and exit")
return parser.parse_args()
def get_git_diff() -> Optional[str]:
"""Get the current git diff for staged changes"""
try:
diff_process = subprocess.run(
["git", "diff", "--staged"],
capture_output=True,
text=True
)
if diff_process.returncode != 0:
print(f"Error: git diff command failed with code {diff_process.returncode}")
return None
return diff_process.stdout
except Exception as e:
print(f"Error running git diff: {e}")
return None
def truncate_diff(diff_text: str, max_size: int = MAX_DIFF_SIZE) -> str:
"""Truncate diff if it's too large, preserving the most important parts"""
if max_size is None or len(diff_text) <= max_size:
return diff_text
print(f"Warning: Diff is large ({len(diff_text)} chars), truncating to {max_size} chars")
# Split by file sections and keep as many complete files as possible
sections = diff_text.split("diff --git ")
header = sections[0]
file_sections = ["diff --git " + s for s in sections[1:]]
result = header
for section in file_sections:
if len(result) + len(section) <= max_size:
result += section
else:
break
return result
def filter_sensitive_data(diff_text: str) -> str:
"""Filter out potentially sensitive data from the diff"""
# List of patterns that might indicate sensitive data
sensitive_patterns = [
("password", "***REDACTED***"),
("secret", "***REDACTED***"),
("token", "***REDACTED***"),
("api_key", "***REDACTED***"),
("apikey", "***REDACTED***"),
("auth", "***REDACTED***"),
("credential", "***REDACTED***"),
]
# Simple pattern matching - in a real implementation, you might want more sophisticated regex
filtered_text = diff_text
for pattern, replacement in sensitive_patterns:
# Only replace if it looks like an assignment or declaration
filtered_text = filtered_text.replace(f'{pattern}="', f'{pattern}="{replacement}')
filtered_text = filtered_text.replace(f"{pattern}='", f"{pattern}='{replacement}'")
filtered_text = filtered_text.replace(f"{pattern}:", f"{pattern}: {replacement}")
filtered_text = filtered_text.replace(f"{pattern} =", f"{pattern} = {replacement}")
return filtered_text
def show_spinner(duration: int = None):
"""Display a simple spinner to indicate progress"""
import itertools
import threading
import time
spinner = itertools.cycle(['-', '/', '|', '\\'])
stop_spinner = threading.Event()
def spin():
while not stop_spinner.is_set():
sys.stdout.write(f"\rGenerating commit message... {next(spinner)} ")
sys.stdout.flush()
time.sleep(0.1)
spinner_thread = threading.Thread(target=spin)
spinner_thread.start()
try:
if duration:
time.sleep(duration)
stop_spinner.set()
return stop_spinner
except:
stop_spinner.set()
raise
def generate_commit_message(diff_text: str, model: str = DEFAULT_MODEL) -> Optional[str]:
"""Generate a commit message based on the git diff"""
if not diff_text or diff_text.strip() == "":
return "No changes staged for commit"
# Filter sensitive data
filtered_diff = filter_sensitive_data(diff_text)
# Truncate if necessary
truncated_diff = truncate_diff(filtered_diff)
client = Client()
prompt = f"""
{truncated_diff}
```
Analyze ONLY the exact changes in this git diff and create a precise commit message.
FORMAT:
1. First line: "<type>: <summary>" (max 70 chars)
- Type: feat, fix, docs, refactor, test, etc.
- Summary must describe ONLY actual changes shown in the diff
2. Leave one blank line
3. Add sufficient bullet points to:
- Describe ALL specific changes seen in the diff
- Reference exact functions/files/components that were modified
- Do NOT mention anything not explicitly shown in the code changes
- Avoid general statements or assumptions not directly visible in diff
- Include enough points to cover all significant changes (don't limit to a specific number)
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.
"""
for attempt in range(MAX_RETRIES):
try:
# Start spinner
spinner = show_spinner()
# Make API call
response = client.chat.completions.create(
prompt,
model=model,
stream=True,
)
content = []
for chunk in response:
if isinstance(chunk.choices[0].delta.content, str):
# Stop spinner and clear line
if spinner:
spinner.set()
print(" " * 50 + "\n", flush=True)
spinner = None
content.append(chunk.choices[0].delta.content)
print(chunk.choices[0].delta.content, end="", flush=True)
return "".join(content).strip("`").strip()
except Exception as e:
# Stop spinner if it's running
if 'spinner' in locals() and spinner:
spinner.set()
sys.stdout.write("\r" + " " * 50 + "\r")
sys.stdout.flush()
print(f"Error generating commit message (attempt {attempt+1}/{MAX_RETRIES}): {e}")
if attempt < MAX_RETRIES - 1:
print(f"Retrying in {RETRY_DELAY} seconds...")
time.sleep(RETRY_DELAY)
# Try with a fallback model if available
if attempt < len(FALLBACK_MODELS):
fallback = FALLBACK_MODELS[attempt]
print(f"Trying with fallback model: {fallback}")
model = fallback
return None
def edit_commit_message(message: str) -> str:
"""Allow user to edit the commit message in their default editor"""
with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.txt') as temp:
temp.write(message)
temp_path = temp.name
# Get the default editor from git config or environment
try:
editor = subprocess.run(
["git", "config", "--get", "core.editor"],
capture_output=True, text=True
).stdout.strip()
except:
editor = os.environ.get('EDITOR', 'vim')
if not editor:
editor = 'vim' # Default fallback
# Open the editor
try:
subprocess.run([editor, temp_path], check=True)
except subprocess.CalledProcessError:
print("Warning: Editor exited with an error")
except FileNotFoundError:
print(f"Warning: Editor '{editor}' not found, falling back to basic input")
print("Edit your commit message (Ctrl+D when done):")
edited_message = sys.stdin.read().strip()
os.unlink(temp_path)
return edited_message
# Read the edited message
with open(temp_path, 'r') as temp:
edited_message = temp.read()
# Clean up
os.unlink(temp_path)
return edited_message
def list_available_models() -> List[str]:
"""List available AI models that can be used for commit message generation"""
# Filter for text models that are likely to be good for code understanding
relevant_models = []
for model_name, model in ModelUtils.convert.items():
# Skip image, audio, and video models
if model_name and not model_name.startswith(('dall', 'sd-', 'flux', 'midjourney')):
relevant_models.append(model_name)
return sorted(relevant_models)
def make_commit(message: str) -> bool:
"""Make a git commit with the provided message"""
try:
subprocess.run(
["git", "commit", "-m", message],
check=True
)
return True
except subprocess.CalledProcessError as e:
print(f"Error making commit: {e}")
return False
def main():
"""Main function"""
try:
args = parse_arguments()
# If --list-models is specified, list available models and exit
if args.list_models:
print("Available AI models for commit message generation:")
for model in list_available_models():
print(f" - {model}")
sys.exit(0)
print("Fetching git diff...")
diff = get_git_diff()
if diff is None:
print("Failed to get git diff. Are you in a git repository?")
sys.exit(1)
if diff.strip() == "":
print("No changes staged for commit. Stage changes with 'git add' first.")
sys.exit(0)
print(f"Using model: {args.model}")
commit_message = generate_commit_message(diff, args.model)
if not commit_message:
print("Failed to generate commit message after multiple attempts.")
sys.exit(1)
if args.edit:
print("\nOpening editor to modify commit message...")
commit_message = edit_commit_message(commit_message)
print("\nEdited commit message:")
print("-" * 50)
print(commit_message)
print("-" * 50)
if args.no_commit:
print("\nCommit message generated but not committed (--no-commit flag used).")
sys.exit(0)
user_input = input("\nDo you want to use this commit message? (y/n): ")
if user_input.lower() == 'y':
if make_commit(commit_message):
print("Commit successful!")
else:
print("Commit failed.")
sys.exit(1)
else:
print("Commit aborted.")
except KeyboardInterrupt:
print("\nOperation cancelled by user.")
sys.exit(130) # Standard exit code for SIGINT
if __name__ == "__main__":
main()