AI-Powered Git: Autocomplete Your Commits

AI-powered git commit illustration

I've always made an effort to write clear, concise git commit messages. They're not just breadcrumbs; they can be a great form of documentation that helps the team and "future me" understand the history of our codebase. When I'm diving into a project, whether it's one I haven't touched in months or something entirely new to me, good and descriptive commit messages can guide you through the code's history. They explain the 'why' behind changes, making it easier for everyone to grasp the project's development over time.

But I'll be honest – after a long coding session, staring at that blank commit message field can feel like a chore. It's tempting to just type "fixed stuff", or "wip" and call it a day. That's why I'm excited to share a little script I've been working on that's been a game-changer for my workflow. I've been using it for a couple of weeks now, and now I can't live without it. It's helping me write great PR and commit messages without the mental drain.

The Spark of Inspiration

Before I dive into the details, I want to give credit where it's due. This project was born from a brilliant idea I came across on Twitter. Tien Dinh (@tdinh_me) shared a concept that immediately resonated with me. It was one of those "why didn't I think of that?" moments.

The inspirational tweet

My AI-Powered Git Commit Function

Building on Tien's concept, I've created a Zsh function that leverages OpenAI's GPT-40-mini model to generate commit messages based on my staged changes. But I didn't stop there – I extended it to help with pull request titles and descriptions too. Let me walk you through how it works and how you can start using it in your own workflow.

How It Works

  1. Analyzing Staged Changes: First, the function checks if I have any staged files. If I do, it generates a diff of these changes.
  2. AI-Generated Commit Message: Using the OpenAI API, it sends the diff along with a prompt I've crafted to generate a concise, meaningful commit message.
  3. User Interaction: I'm then presented with the generated commit message. I can use it as-is, edit it if I want to tweak something, or cancel if it's not quite right.
  4. Pull Request Creation: After committing, the function asks if I want to create a pull request. If I do, it can push my changes and use AI to generate a PR title and description based on my commit history.
  5. GitHub Integration: If I have the GitHub CLI installed, it automatically creates the pull request for me and opens it in my browser. Super convenient!

Setting It Up

To start using this function, follow these steps:

  1. Create a new file in your dotfiles directory (or wherever you prefer) called (for example) gcai.sh. I think of it as Git Commit AI script.

  2. Copy the function code into this file. You can find the full script in my dotfiles repository and in its entire at the end of til article.

  3. Add the script file to your .zshrc file. I've added the following line to my .zshrc:

~/.dotfiles/.zshrc
zsh
export DOTFILES=$HOME/.dotfiles # or wherever you store your dotfiles

[ -f $DOTFILES/gcai.sh ] && source $DOTFILES/gcai.sh
  1. Install the OpenAI CLI and Github CLI (if not already installed).
~/.dotfiles/.zshrc
zsh
python3.11 -m pip install --upgrade openai
brew install gh
  1. Reload your terminal or run source ~/.zshrc or source ~/.dotfiles/.zshrc depending on where the file lives.

  2. Make sure you have the OpenAI API key set in your environment variables:

zsh
export OPENAI_API_KEY='your-api-key-here'

Now you're ready to use the gcai function in your git workflow!

Using the Function

Here's a quick rundown of how to use the function:

  1. Stage your changes as usual with git add.
  2. Instead of git commit, run gcai.
  3. Review the suggested commit message.
  4. Press Enter to use it, 'e' to edit it, or 'c' to cancel.
  5. If you choose to create a pull request, follow the prompts to push your changes and generate the PR description.

If you have already created the commits and want to generate a PR based on your commit history, you can run prai from your branch. The name is as in pull request AI.

The Benefits

Since I started using this function, I've noticed several improvements in my workflow:

  1. Time-saving: No more staring at the screen trying to come up with the perfect commit message.
  2. Consistency: The AI tends to generate messages in a consistent format, which helps maintain a clean git history.
  3. Detailed PRs: The pull request descriptions are often more comprehensive than what I would write manually, especially for larger changes.
  4. Fun: Using AI is fun! I've created more and smaller commits since implementing this function.

Customization and Improvement

This is just the beginning. There's lots of room for customization and improvement:

  • You could modify the prompts to fit your team's commit message style guide.
  • The function could be extended to support different AI models or APIs.
  • You might add more interactive features, like suggesting code reviews based on the changes.

The script

~/.dotfiles/gcai.sh
sh
# Function for generating and making a commit
ai_commit() {
  # Check if there are staged files
  STAGED_FILES=$(git diff --cached --name-only)
  if [[ -z "$STAGED_FILES" ]]; then
    echo "No files are staged for commit."
    return 1
  fi

  # Get the diff of staged files
  GIT_DIFF=$(git diff --cached)

  # Build the prompt for the API
  SYSTEM_PROMPT="You are a helpful assistant that writes effective git commit messages based on changes provided. Provide your response in a structured format with a subject line and an optional body. The subject line should be no longer than 50 characters if possible, and never exceed 72 characters. Use the imperative mood, start with a capital letter, and do not use punctuation at the end for the subject line. If you think a body is necessary to provide more context or explanation, include it after a blank line. Use the following structure:
SUBJECT: <subject line here>
BODY:
<body content here, if needed>"
  USER_PROMPT="Generate a git commit message for the following changes:

$GIT_DIFF"

  COMMIT_MESSAGE=$(openai api chat.completions.create \
    -m gpt-4o-mini \
    -g system "$SYSTEM_PROMPT" \
    -g user "$USER_PROMPT" \
    --temperature 0.7)

  if [[ -z "$COMMIT_MESSAGE" ]]; then
    echo "Failed to generate commit message or received empty message."
    echo "Would you like to enter a commit message manually? (y/n)"
    read -k1 MANUAL_INPUT
    echo
    if [[ "$MANUAL_INPUT" == "y" ]]; then
      echo "Enter your commit subject line:"
      read -r COMMIT_SUBJECT
      echo "Enter your commit body (press Ctrl+D when finished, leave empty if not needed):"
      COMMIT_BODY=$(cat)
    else
      echo "Commit canceled."
      return 1
    fi
  else
    # Extract the subject and body
    COMMIT_SUBJECT=$(echo "$COMMIT_MESSAGE" | sed -n 's/^SUBJECT: //p')
    COMMIT_BODY=$(echo "$COMMIT_MESSAGE" | sed -n '/^BODY:/,$p' | sed '1d')
  fi

  # Truncate the subject if it's too long
  if [ ${#COMMIT_SUBJECT} -gt 72 ]; then
    COMMIT_SUBJECT="${COMMIT_SUBJECT:0:69}..."
  fi

  # Display the commit message and staged files
  echo
  echo "Generated commit message:"
  echo "-------------------------"
  echo "Subject: $COMMIT_SUBJECT"
  if [[ -n "$COMMIT_BODY" ]]; then
    echo
    echo "Body:"
    echo "$COMMIT_BODY"
  fi
  echo "-------------------------"
  echo
  echo "Staged files:"
  echo "-------------------------"
  echo "$STAGED_FILES"
  echo "-------------------------"
  echo

  # Prompt for user confirmation or editing
  echo "Press 'e' to edit the commit message, 'c' to cancel, or any other key to confirm and commit:"
  read -k1 USER_INPUT
  echo
  if [[ "$USER_INPUT" == "c" ]]; then
    echo "Commit canceled."
    return 1
  elif [[ "$USER_INPUT" == "e" ]]; then
    # Open the commit message in the default editor
    TEMP_FILE=$(mktemp)
    echo "$COMMIT_SUBJECT" > "$TEMP_FILE"
    if [[ -n "$COMMIT_BODY" ]]; then
      echo >> "$TEMP_FILE"
      echo "$COMMIT_BODY" >> "$TEMP_FILE"
    fi
    "${EDITOR:-nano}" "$TEMP_FILE"
    COMMIT_SUBJECT=$(head -n 1 "$TEMP_FILE")
    COMMIT_BODY=$(tail -n +3 "$TEMP_FILE")
    rm "$TEMP_FILE"
  fi

  # Commit the changes
  if [[ -n "$COMMIT_BODY" ]]; then
    git commit -m "$COMMIT_SUBJECT" -m "$COMMIT_BODY"
  else
    git commit -m "$COMMIT_SUBJECT"
  fi
}

# Function for creating a pull request
ai_pr() {
  # Get the current branch name
  CURRENT_BRANCH=$(git branch --show-current)

  # List of protected branches
  PROTECTED_BRANCHES=("main" "master" "develop" "release")

  # Check if the current branch is protected
  if [[ " ${PROTECTED_BRANCHES[@]} " =~ " ${CURRENT_BRANCH} " ]]; then
    echo "You are on a protected branch: $CURRENT_BRANCH"
    echo "Let's create a new branch for your changes."

    # Get the commit history for the current branch
    COMMIT_HISTORY=$(git log -n 5 --pretty=format:"%s%n%b")

    # Generate branch name using AI
    SYSTEM_PROMPT="You are a helpful assistant that generates concise and descriptive git branch names based on commit messages. The branch name should be in kebab-case, start with a verb (e.g., add, update, fix, refactor), and be no longer than 50 characters. Do not include 'feature/' or 'fix/' prefixes. Provide only the branch name without any additional text or explanation."
    USER_PROMPT="Generate a git branch name based on the following commit messages:\n\n$COMMIT_HISTORY"

    SUGGESTED_BRANCH=$(openai api chat.completions.create \
      -m gpt-4o-mini \
      -g system "$SYSTEM_PROMPT" \
      -g user "$USER_PROMPT" \
      --temperature 0.7)

    echo "Suggested branch name: $SUGGESTED_BRANCH"
    echo "Do you want to use this branch name? (y/n)"
    read -k1 USE_SUGGESTED_BRANCH
    echo

    if [[ "$USE_SUGGESTED_BRANCH" != "y" ]]; then
      echo "Enter a new branch name (without 'feature/' or 'fix/' prefix):"
      read -r NEW_BRANCH
    else
      NEW_BRANCH=$SUGGESTED_BRANCH
    fi

    # Determine the prefix
    echo "Is this a new feature (f) or a bugfix (b)?"
    read -k1 BRANCH_TYPE
    echo

    case $BRANCH_TYPE in
      f|F)
        GCAIPREFIX="feature/"
        ;;
      b|B)
        GCAIPREFIX="fix/"
        ;;
      *)
        echo "Invalid option. Using no prefix."
        GCAIPREFIX=""
        ;;
    esac

    # Create and switch to the new branch with the appropriate prefix
    FULL_BRANCH_NAME="${GCAIPREFIX}${NEW_BRANCH}"
    git checkout -b "$FULL_BRANCH_NAME"
    echo "Created and switched to new branch: $FULL_BRANCH_NAME"

    # Push the new branch to origin
    git push -u origin "$FULL_BRANCH_NAME"
    CURRENT_BRANCH="$FULL_BRANCH_NAME"
  else
    # Check if the branch has an upstream branch
    UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)

    if [[ -z "$UPSTREAM" ]]; then
      echo "No upstream branch is set for $CURRENT_BRANCH."
      echo "Choose an option:"
      echo "1) Push to origin/$CURRENT_BRANCH"
      echo "2) Enter a custom branch name"
      read -k1 PUSH_OPTION
      echo

      case $PUSH_OPTION in
        1)
          git push -u origin "$CURRENT_BRANCH"
          ;;
        2)
          echo "Enter the name of the remote branch (without 'feature/' or 'fix/' prefix):"
          read -r REMOTE_BRANCH
          echo "Is this a new feature (f) or a bugfix (b)?"
          read -k1 BRANCH_TYPE
          echo

          case $BRANCH_TYPE in
            f|F)
              GCAIPREFIX="feature/"
              ;;
            b|B)
              GCAIPREFIX="fix/"
              ;;
            *)
              echo "Invalid option. Using no prefix."
              GCAIPREFIX=""
              ;;
          esac

          FULL_REMOTE_BRANCH="${GCAIPREFIX}${REMOTE_BRANCH}"
          git push -u origin "$CURRENT_BRANCH:$FULL_REMOTE_BRANCH"
          CURRENT_BRANCH="$FULL_REMOTE_BRANCH"
          ;;
        *)
          echo "Invalid option. Aborting."
          return 1
          ;;
      esac
    else
      # Push the current branch to its upstream
      git push
    fi
  fi

  # Get the repository URL
  REPO_URL=$(git config --get remote.origin.url)
  REPO_URL=${REPO_URL#*:}
  REPO_URL=${REPO_URL%.git}

  # Get the commit history for the current branch
  COMMIT_HISTORY=$(git log origin/main.."$CURRENT_BRANCH" --pretty=format:"%h %s")

  # Generate PR title and body using AI
  SYSTEM_PROMPT="You are a helpful assistant that generates concise and informative pull request titles and descriptions based on git commit history. Provide your response in a structured format with a title (max 72 characters) and a body (using markdown formatting). Use the following structure:
TITLE: <title here>
BODY:
<body content here>"
  USER_PROMPT="Generate a pull request title and body for the following commit history:\n\n$COMMIT_HISTORY"

  # Call the OpenAI API using the CLI
  AI_CONTENT=$(openai api chat.completions.create \
    -m gpt-4o-mini \
    -g system "$SYSTEM_PROMPT" \
    -g user "$USER_PROMPT" \
    --temperature 0.7 \
    --max-tokens 500)

  # Extract the title and body
  PR_TITLE=$(echo "$AI_CONTENT" | sed -n 's/^TITLE: //p')
  PR_BODY=$(echo "$AI_CONTENT" | sed -n '/^BODY:/,$p' | sed '1d')

  # Truncate the title if it's too long
  if [ ${#PR_TITLE} -gt 72 ]; then
    PR_TITLE="${PR_TITLE:0:69}..."
  fi

  # Display the generated title and body
  echo
  echo "Generated title:"
  echo "-------------------------"
  echo "$PR_TITLE"
  echo "-------------------------"
  echo
  echo "Generated body:"
  echo "-------------------------"
  echo "$PR_BODY"
  echo "-------------------------"
  echo

  # Ask user if they want to use the AI-generated content or enter their own
  echo "Do you want to use this AI-generated title and body? (y/n)"
  read -k1 USE_AI_CONTENT
  echo

  if [[ "$USE_AI_CONTENT" != "y" ]]; then
    echo "Enter a title for the pull request:"
    read -r PR_TITLE
    echo "Enter a body for the pull request (press Ctrl+D when finished):"
    PR_BODY=$(cat)
  fi

  # Create the pull request using GitHub CLI
  if command -v gh &> /dev/null; then
    gh pr create --title "$PR_TITLE" --body "$PR_BODY"

    # Open the pull request in the browser
    gh pr view --web
  else
    echo "GitHub CLI (gh) is not installed. Please install it to create pull requests automatically."
    echo "You can create a pull request manually at: https://github.com/$REPO_URL/pull/new/$CURRENT_BRANCH"
  fi
}

# Main function that combines commit and PR creation
gcai() {
  ai_commit
  if [ $? -eq 0 ]; then
    echo "Do you want to create a pull request? (y/n)"
    read -k1 CREATE_PR
    echo
    if [[ "$CREATE_PR" == "y" ]]; then
      ai_pr
    fi
  fi
}

# Main function that combines commit and PR creation
prai() {
  ai_pr
}

Wrapping Up

I've found this AI-powered git commit function to be a real game-changer in my daily workflow. It's a small tool, but it addresses a common pain point in a clever way. I'm excited to continue refining it and seeing how it evolves. For the latest versions, then check out my dotfiles repository.

I'd love to hear your thoughts on this approach. Have you tried using AI in your development workflow? Do you see potential improvements or concerns? Let me know on x.com.

Remember, tools like this are meant to assist, not replace, your judgment. Always review the suggested messages and make sure they accurately represent your changes. The suggestions are not always perfect, but they can be a great starting point.

Want to learn more? Check out these resources:

Happy committing!

Stay up to date

Get notified when I publish something new, and unsubscribe at any time.