-
-
Save AshesOfPhoenix/6f6700318c10df9500d8f95d32cd133b to your computer and use it in GitHub Desktop.
Git Commit Message & Branch Summary AI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ----------------------------------------------------------------------------- | |
| # AI-powered Git Commit Function | |
| # Copy paste this gist into your ~/.bashrc or ~/.zshrc to gain the `gcm` command. It: | |
| # 1) gets the current staged changed diff | |
| # 2) sends them to an LLM to write the git commit message | |
| # 3) allows you to easily accept, edit, regenerate, cancel | |
| # But - just read and edit the code however you like | |
| # the `llm` CLI util is awesome, can get it here: https://llm.datasette.io/en/stable/ | |
| # Unalias gcm and gcs if it exists | |
| unalias gcm 2>/dev/null | |
| unalias gcs 2>/dev/null | |
| # Define the prompt using a literal here document | |
| read -r -d '' prompt <<'EOF' | |
| Below is a diff of all staged changes, coming from the command: | |
| ``` | |
| git diff --cached | |
| ``` | |
| **Objective:** Analyze the provided `git diff` input and generate a single, concise Git commit message that strictly adheres to the Conventional Commits v1.0.0 specification (Reference: https://www.conventionalcommits.org/en/v1.0.0/). | |
| **Input:** A multi-line string containing the output of a `git diff` command. | |
| **Output:** A single string formatted as a Git commit message according to the rules below. DO NOT WRAP IN ``` | |
| **Commit Message Structure & Rules:** | |
| 1. **Format:** Follow the structure: | |
| ``` | |
| <type>[optional scope]: <description> | |
| [optional body] | |
| [optional footer(s)] | |
| ``` | |
| 2. **Header (`<type>[optional scope]: <description>`)** | |
| * **`type`:** Mandatory. Must be one of the following lowercase strings: | |
| * `feat`: A new feature is introduced. | |
| * `fix`: A bug fix. | |
| * `docs`: Documentation changes only. | |
| * `style`: Code style changes (formatting, whitespace, etc.) that do not affect meaning. | |
| * `refactor`: Code changes that neither fix a bug nor add a feature. | |
| * `perf`: Code changes that improve performance. | |
| * `test`: Adding missing tests or correcting existing tests. | |
| * `chore`: Changes to the build process, auxiliary tools, or libraries. | |
| * Infer the most appropriate `type` by analyzing the *primary purpose* of the changes in the `git diff`. | |
| * **`scope`**: Optional. A noun in parentheses describing the section of the codebase affected (e.g., `(api)`, `(ui)`, `(auth)`). Infer this from the file paths or context in the `diff`. Omit if the change is global or hard to pinpoint. | |
| * **`description`**: Mandatory. A concise summary of the change. | |
| * Use imperative, present tense (e.g., `Update user model`, not `Updated` or `Updates`). | |
| * Start with a capital letter if it follows a scope or is the first word after the type colon. Start with a lowercase letter if it directly follows the type and colon (e.g., `fix: correct login validation`). *Correction based on common practice: Conventional Commits usually recommend starting the description lowercase unless it's grammatically necessary (like a proper noun), but the attached `commit-message-naming.md` preferred capitalization. Let's stick to the spec's usual lowercase start unless a scope is present or it's a proper noun.* **Final Decision:** Start the description with a lowercase letter unless a scope is present, then capitalize. | |
| * Do NOT end with a period. | |
| * Maximum 50 characters. | |
| 3. **Body (Optional)** | |
| * Use only if the change requires more context than the description provides (explain *why* the change was made, previous behavior, etc.). | |
| * Separate from the header by **exactly one blank line**. | |
| * Wrap lines at 72 characters. | |
| * **Must include a bulleted list of changed files**, extracted from the `git diff` input, under the heading `Files Changed:`. Example: | |
| ``` | |
| Files Changed: | |
| - path/to/modified/file.ext | |
| - path/to/another/file.py | |
| ``` | |
| 4. **Footer (Optional)** | |
| * Separate from the body by **exactly one blank line**. | |
| * Used for metadata like referencing issue tracker IDs (e.g., `Refs: #123`) or indicating breaking changes. | |
| * **Breaking Changes:** If the commit introduces incompatible API changes, denote this EITHER by appending `!` after the `type`/`scope` (e.g., `feat(api)!:`) OR by starting a paragraph in the footer with `BREAKING CHANGE: ` followed by a description of the change and migration instructions. Both `!` and the footer section can be used. | |
| **Processing Instructions:** | |
| 1. Parse the input `git diff` to understand the modifications, additions, and deletions across all files. | |
| 2. Identify the primary intent to select the correct `type`. | |
| 3. Determine if a specific `scope` can be reasonably inferred. | |
| 4. Summarize the change accurately for the `description`, adhering to tense and length limits. | |
| 5. If necessary, elaborate on the reasoning in the `body`. | |
| 6. Extract all unique file paths mentioned in the `diff` (lines starting with `--- a/` or `+++ b/`) and list them under the `Files Changed:` heading in the body. | |
| 7. Check for indicators of breaking changes to apply the `!` marker and/or `BREAKING CHANGE:` footer. | |
| 8. Assemble the components into the final commit message string, strictly following the blank line separation rules. | |
| **Example:** | |
| *Input `git diff` Snippet:* | |
| ```diff | |
| diff --git a/src/utils/validation.js b/src/utils/validation.js | |
| index abc..def 100644 | |
| --- a/src/utils/validation.js | |
| +++ b/src/utils/validation.js | |
| @@ -1,5 +1,6 @@ | |
| function isValidEmail(email) { | |
| - // Basic regex, placeholder | |
| - return /.+@.+\..+/.test(email); | |
| + // RFC 5322 compliant regex | |
| + const emailRegex = new RegExp(/^(([^<>()$$$$\\.,;:\s@"]+(\.[^<>()$$$$\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/); | |
| + return emailRegex.test(email); | |
| } | |
| module.exports = { isValidEmail }; | |
| diff --git a/tests/validation.test.js b/tests/validation.test.js | |
| index ghi..jkl 100644 | |
| --- a/tests/validation.test.js | |
| +++ b/tests/validation.test.js | |
| @@ -1,6 +1,7 @@ | |
| const { isValidEmail } = require('../src/utils/validation'); | |
| test('validates emails correctly', () => { | |
| - expect(isValidEmail('[email protected]')).toBe(true); | |
| expect(isValidEmail('[email protected]')).toBe(true); | |
| + expect(isValidEmail('invalid-email')).toBe(false); | |
| }); | |
| ``` | |
| *Expected Output Commit Message (do not wrap in ```):* | |
| fix(utils): improve email validation regex | |
| The previous email validation used a basic placeholder regex. This | |
| commit updates it to a more robust RFC 5322 compliant pattern to | |
| correctly validate a wider range of email formats and reject invalid | |
| ones. | |
| Files Changed: | |
| - src/utils/validation.js | |
| - tests/validation.test.js | |
| EOF | |
| # Generate commit summary | |
| gcs() { | |
| # Define color codes for enhanced readability | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m' | |
| NC='\033[0m' # No Color | |
| # Check if in a git repository | |
| if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then | |
| echo -e "${RED}Not inside a git repository. Please navigate to a git repository and try again.${NC}" | |
| return 1 | |
| fi | |
| # Check if `llm` package is installed | |
| if ! pip show llm > /dev/null 2>&1; then | |
| echo -e "${RED}The 'llm' package is not installed. Please install it using 'pip install llm' and try again. See more info here: https://simonwillison.net/2023/May/18/cli-tools-for-llms/${NC}" | |
| return 1 | |
| fi | |
| local model_id=${1:-'gpt-4o'} | |
| count_prompt_tokens() { | |
| gitlog=$(git log $(git reflog | grep 'checkout:' | head -n 1 | awk '{print $1}')..HEAD --oneline) | |
| if [ -z "$gitlog" ]; then | |
| return 1 | |
| fi | |
| echo $gitlog | ttok | |
| } | |
| # Function to generate commit summary | |
| generate_commit_summary() { | |
| gitlog=$(git log $(git reflog | grep 'checkout:' | head -n 1 | awk '{print $1}')..HEAD --oneline) | |
| if [ -z "$gitlog" ]; then | |
| return 1 | |
| fi | |
| echo $gitlog | llm -m "$model_id" -o "temperature" 0.2 " | |
| Below is a list of all commit messages since the last branch checkout: | |
| \`\`\` | |
| git log | |
| \`\`\` | |
| Write concise, informative commit message summary in imperative mood, explain the 'why' behind changes, keep the summary under 200 characters, | |
| use bullet points for important features but keep the summary in paragraph, use past tense, avoid using the word refactor, | |
| instead explain what was done. Don't wrap the output in \`\`\`. What you write will be a merge summary. Include a title which is a very short wrap up summary of all the commits." | |
| } | |
| # Counting tokens | |
| echo -e "${CYAN}Counting the number of tokens for the request...${NC}" | |
| total_tokens=$(count_prompt_tokens) | |
| if [ "$total_tokens" -le 3 ]; then | |
| echo -e "${RED}✘ No diff detected. Either there are no changes or they are staged. Exiting.${NC}" | |
| return 1 | |
| fi | |
| if [ "$total_tokens" -gt 1000 ]; then | |
| while true; do | |
| echo -e "${YELLOW}Total tokens: $total_tokens${NC}" | |
| read_input "Do you want to continue (y/n)? " | |
| choice=$REPLY | |
| case "$choice" in | |
| y|Y ) | |
| echo -e "${GREEN}✔ Continuing...${NC}" | |
| break | |
| ;; | |
| n|N ) | |
| echo -e "${RED}✘ Aborting.${NC}" | |
| return 1 | |
| ;; | |
| * ) | |
| echo -e "${RED}✘ Invalid choice. Please enter 'y' or 'n'.${NC}" | |
| ;; | |
| esac | |
| done | |
| else | |
| echo -e "${CYAN}Total number of input tokens: $total_tokens${NC}" | |
| fi | |
| # Main script | |
| echo -e "${CYAN}Generating AI-powered commit summary...${NC}" | |
| commit_summary=$(generate_commit_summary) | |
| if [ $? -ne 0 ]; then | |
| echo -e "${RED}No diff detected. Either there are no changes or they are staged. Exiting.${NC}" | |
| return 1 | |
| fi | |
| echo -e "\n${GREEN}Commit summary:${NC}" | |
| echo -e "${CYAN}\`\`\`\n$commit_summary\n\`\`\`${NC}" | |
| } | |
| # Generate commit message | |
| gcm() { | |
| # Define color codes for enhanced readability | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m' | |
| NC='\033[0m' # No Color | |
| # Check if in a git repository | |
| if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then | |
| echo -e "${RED}Not inside a git repository. Please navigate to a git repository and try again.${NC}" | |
| return 1 | |
| fi | |
| if ! pip show llm > /dev/null 2>&1; then | |
| echo -e "${RED}The 'llm' package is not installed. Please install it using 'pip install llm' and try again. See more info here: https://simonwillison.net/2023/May/18/cli-tools-for-llms/${NC}" | |
| return 1 | |
| fi | |
| if ! pip show ttok > /dev/null 2>&1; then | |
| echo -e "${RED}The 'ttok' package is not installed. Please install it using 'pip install ttok' and try again. See more info here: https://simonwillison.net/2023/May/18/cli-tools-for-llms/${NC}" | |
| return 1 | |
| fi | |
| local model_id=${1:-'gemini-2.0-flash'} | |
| count_prompt_tokens() { | |
| gitdiff=$(git diff) | |
| if [ -z "$gitdiff" ]; then | |
| return 1 | |
| fi | |
| echo $gitdiff | ttok | |
| } | |
| # Default excluded patterns | |
| DEFAULT_EXCLUDED_PATTERNS=('pnpm-lock.yaml' 'package-lock.json' 'yarn.lock') | |
| # Function to prompt user for file exclusions | |
| prompt_for_exclusions() { | |
| local excluded_files=() | |
| echo -e "${CYAN}Default excluded files: ${DEFAULT_EXCLUDED_PATTERNS[*]}${NC}" | |
| read_input "Do you want to include these files? (y/n): " | |
| if [[ ! $REPLY =~ ^[Yy]$ ]]; then | |
| excluded_files=("${DEFAULT_EXCLUDED_PATTERNS[@]}") | |
| fi | |
| while true; do | |
| read_input "Enter additional files to exclude (or press Enter to finish): " | |
| [[ -z $REPLY ]] && break | |
| excluded_files+=("$REPLY") | |
| done | |
| echo -e "${GREEN}Excluded files: ${excluded_files[*]}${NC}" | |
| GCM_EXCLUDED_PATTERNS=("${excluded_files[@]}") | |
| } | |
| # Call the function to set up exclusions | |
| # prompt_for_exclusions | |
| # Add user-defined exclusions from environment variable, if any | |
| if [ -n "$GCM_EXCLUDED_PATTERNS_USER" ]; then | |
| IFS=';' read -r -a USER_EXCLUSIONS <<< "$GCM_EXCLUDED_PATTERNS_USER" | |
| GCM_EXCLUDED_PATTERNS+=("${USER_EXCLUSIONS[@]}") | |
| fi | |
| # Function to generate commit message | |
| generate_commit_message() { | |
| local exclude_args=() | |
| for pattern in "${GCM_EXCLUDED_PATTERNS[@]}"; do | |
| exclude_args+=(":(exclude)$pattern") | |
| done | |
| # echo -e "${CYAN}Excluding files: ${GCM_EXCLUDED_PATTERNS[*]}${NC}" | |
| local gitdiff error_message | |
| gitdiff=$(git diff -- . "${exclude_args[@]}" 2>&1) | |
| if [ $? -ne 0 ]; then | |
| error_message="Failed to get git diff: $gitdiff" | |
| echo -e "${RED}$error_message${NC}" >&2 | |
| return 1 | |
| fi | |
| if [ -z "$gitdiff" ]; then | |
| echo -e "${YELLOW}No changes detected in tracked files.${NC}" >&2 | |
| return 1 | |
| fi | |
| prompt_file="aigit_prompt.txt" | |
| # Check if prompt file exists | |
| # if [ ! -f "$prompt_file" ]; then | |
| # echo "Error: Prompt file '$prompt_file' not found." >&2 | |
| # exit 1 | |
| # fi | |
| local llm_output | |
| llm_output=$(echo "$gitdiff" | llm -m "$model_id" -o "temperature" 0.2 "$prompt" 2>&1) | |
| # echo -e "${CYAN}Generated commit prompt: ${NC}" | |
| # echo -e "$llm_output" | |
| if [ $? -ne 0 ]; then | |
| error_message="Failed to generate commit message: $llm_output" | |
| echo -e "${RED}$error_message${NC}" >&2 | |
| return 1 | |
| fi | |
| echo "$llm_output" | |
| } | |
| # Function to read user input compatibly with both Bash and Zsh | |
| read_input() { | |
| if [ -n "$ZSH_VERSION" ]; then | |
| echo -n "$1" | |
| read -r REPLY | |
| else | |
| read -p "$1" -r REPLY | |
| fi | |
| } | |
| # Counting tokens | |
| echo -e "${CYAN}Counting the number of tokens for the request...${NC}" | |
| total_tokens=$(count_prompt_tokens) | |
| if [ "$total_tokens" -le 3 ]; then | |
| echo -e "${RED}No diff detected. Either there are no changes or they are staged. Exiting.${NC}" | |
| return 1 | |
| fi | |
| if [ "$total_tokens" -gt 5000 ]; then | |
| while true; do | |
| echo -e "${YELLOW}Total tokens: $total_tokens${NC}" | |
| read_input "Do you want to continue (y/n)? " | |
| choice=$REPLY | |
| case "$choice" in | |
| y|Y ) | |
| echo -e "${GREEN}✔ Continuing...${NC}" | |
| break | |
| ;; | |
| n|N ) | |
| echo -e "${RED}✘ Aborting.${NC}" | |
| return 1 | |
| ;; | |
| * ) | |
| echo -e "${RED}✘ Invalid choice. Please enter 'y' or 'n'.${NC}" | |
| ;; | |
| esac | |
| done | |
| else | |
| echo -e "${CYAN}Total number of input tokens: $total_tokens${NC}" | |
| fi | |
| # Main script | |
| echo -e "${CYAN}Generating AI-powered commit message...${NC}" | |
| commit_message=$(generate_commit_message 2>&1) | |
| if [ $? -ne 0 ]; then | |
| echo -e "${RED}✘ Error occurred during commit message generation. Exiting.${NC}" >&2 | |
| return 1 | |
| fi | |
| # echo -e "${GREEN}Generated commit message:${NC}" | |
| # echo -e "${CYAN}$commit_message${NC}" | |
| while true; do | |
| echo -e "${GREEN}Proposed commit message:${NC}" | |
| echo -e "${CYAN}\`\`\`\n$commit_message\n\`\`\`${NC}" | |
| read_input "Do you want to (a)ccept, (e)dit, (r)egenerate, or (c)ancel? " | |
| choice=$REPLY | |
| case "$choice" in | |
| a|A ) | |
| git add . | |
| if git commit -m "$commit_message"; then | |
| echo -e "${GREEN}✔ Changes committed successfully!${NC}" | |
| return 0 | |
| else | |
| echo -e "${RED}✘ Commit failed. Please check your changes and try again.${NC}" | |
| return 1 | |
| fi | |
| ;; | |
| e|E ) | |
| print -n "Enter your commit message: " | |
| commit_message="${commit_message//\"/\\\"}" | |
| BUFFER="$commit_message" | |
| vared BUFFER | |
| commit_message="$BUFFER" | |
| if [ -n "$commit_message" ] && git commit -m "$commit_message"; then | |
| echo -e "${GREEN}✔ Changes committed successfully with your message!${NC}" | |
| return 0 | |
| else | |
| echo -e "${RED}✘ Commit failed. Please check your message and try again.${NC}" | |
| return 1 | |
| fi | |
| ;; | |
| r|R ) | |
| echo -e "${CYAN}Regenerating commit message...${NC}" | |
| commit_message=$(generate_commit_message) | |
| ;; | |
| c|C ) | |
| echo -e "${RED}✘ Commit cancelled.${NC}" | |
| return 1 | |
| ;; | |
| * ) | |
| echo -e "${RED}✘ Invalid choice. Please try again.${NC}" | |
| ;; | |
| esac | |
| done | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment