Skip to content

Instantly share code, notes, and snippets.

@AshesOfPhoenix
Forked from karpathy/add_to_zshrc.sh
Last active March 31, 2025 06:27
Show Gist options
  • Save AshesOfPhoenix/6f6700318c10df9500d8f95d32cd133b to your computer and use it in GitHub Desktop.
Save AshesOfPhoenix/6f6700318c10df9500d8f95d32cd133b to your computer and use it in GitHub Desktop.
Git Commit Message & Branch Summary AI
# -----------------------------------------------------------------------------
# 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