|
#!/usr/bin/env -S uv run |
|
# /// script |
|
# requires-python = ">=3.10" |
|
# dependencies = [ |
|
# "openai", |
|
# ] |
|
# /// |
|
""" |
|
AI-powered Git Commit Script |
|
Generates commit messages using OpenAI API based on staged changes |
|
""" |
|
|
|
import os |
|
import subprocess |
|
import sys |
|
from openai import OpenAI |
|
|
|
|
|
def run_git_command(args): |
|
"""Run a git command and return the output.""" |
|
try: |
|
result = subprocess.run( |
|
["git"] + args, |
|
capture_output=True, |
|
text=True, |
|
check=True |
|
) |
|
return result.stdout.strip() |
|
except subprocess.CalledProcessError as e: |
|
print(f"Error running git command: {e.stderr}") |
|
sys.exit(1) |
|
|
|
|
|
def get_staged_diff(): |
|
"""Get the diff of staged changes.""" |
|
diff = run_git_command(["diff", "--cached"]) |
|
if not diff: |
|
print("No staged changes found. Please stage your changes first with 'git add'.") |
|
sys.exit(1) |
|
return diff |
|
|
|
|
|
def get_staged_files(): |
|
"""Get list of staged files.""" |
|
files = run_git_command(["diff", "--cached", "--name-only"]) |
|
return files.split('\n') if files else [] |
|
|
|
|
|
def generate_commit_message(diff, files): |
|
"""Generate commit message using OpenAI API.""" |
|
api_key = os.environ.get("OPENAI_API_KEY") |
|
|
|
if not api_key: |
|
print("Error: OPENAI_API_KEY not found in environment variables.") |
|
print("Please set it in your ~/.zshrc or export it in your current session:") |
|
print(" export OPENAI_API_KEY='your-api-key-here'") |
|
sys.exit(1) |
|
|
|
client = OpenAI(api_key=api_key) |
|
|
|
# Prepare the prompt |
|
files_list = "\n".join(files) |
|
prompt = f"""Based on the following git diff, generate a concise and descriptive commit message. |
|
|
|
The commit message should: |
|
- Start with a conventional commit type (feat, fix, docs, style, refactor, test, chore) |
|
- Be clear and specific about what changed |
|
- Be under 72 characters for the first line |
|
- Optionally include a blank line and more details if the change is complex |
|
|
|
Files changed: |
|
{files_list} |
|
|
|
Git diff: |
|
{diff[:4000]} |
|
|
|
Generate only the commit message, nothing else.""" |
|
|
|
try: |
|
response = client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=[ |
|
{"role": "system", "content": "You are a helpful assistant that writes clear, concise git commit messages following conventional commit standards."}, |
|
{"role": "user", "content": prompt} |
|
], |
|
temperature=0.7, |
|
max_tokens=200 |
|
) |
|
|
|
commit_message = response.choices[0].message.content.strip() |
|
# Remove surrounding quotes if present |
|
commit_message = commit_message.strip('"\'') |
|
return commit_message |
|
|
|
except Exception as e: |
|
print(f"Error calling OpenAI API: {e}") |
|
sys.exit(1) |
|
|
|
|
|
def commit_changes(message): |
|
"""Commit the staged changes with the generated message.""" |
|
try: |
|
subprocess.run( |
|
["git", "commit", "-m", message], |
|
check=True |
|
) |
|
print("\n✓ Changes committed successfully!") |
|
print(f"\nCommit message:\n{message}") |
|
except subprocess.CalledProcessError as e: |
|
print(f"Error committing changes: {e}") |
|
sys.exit(1) |
|
|
|
|
|
def main(): |
|
print("Checking staged changes...") |
|
|
|
# Get staged files and diff |
|
staged_files = get_staged_files() |
|
staged_diff = get_staged_diff() |
|
|
|
print(f"\nStaged files ({len(staged_files)}):") |
|
for file in staged_files: |
|
print(f" - {file}") |
|
|
|
print("\nGenerating commit message using OpenAI...") |
|
commit_message = generate_commit_message(staged_diff, staged_files) |
|
|
|
print(f"\nProposed commit message:") |
|
print("-" * 60) |
|
print(commit_message) |
|
print("-" * 60) |
|
|
|
# Ask for confirmation |
|
response = input("\nProceed with this commit message? (y/n): ").lower().strip() |
|
|
|
if response == 'y': |
|
commit_changes(commit_message) |
|
else: |
|
print("\nCommit cancelled.") |
|
sys.exit(0) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |