{"slug": "sourcetree-ai-commit-message", "title": "SourceTree Ai Commit message", "summary": "A developer created a SourceTree custom action that uses Anthropic's Claude CLI to automatically generate commit messages from staged diffs. The script enforces Conventional Commits format, truncates large diffs, and presents the message in a dialog for review before committing.", "body_md": "| #!/bin/bash | |\n| # SourceTree Custom Action: generates a commit message with Claude from the | |\n| # staged files and commits directly. | |\n| # Usage from SourceTree: Parameter = $REPO | |\n| set -euo pipefail | |\n| # GUI apps on macOS (SourceTree included) start with a minimal PATH, so we | |\n| # need to manually add the paths where the `claude` binary might live. | |\n| export PATH=\"$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH\" | |\n| REPO=\"${1:-}\" | |\n| if [ -z \"$REPO\" ]; then | |\n| echo \"Error: missing repository path (use \\$REPO as the Custom Action parameter).\" | |\n| exit 1 | |\n| fi | |\n| cd \"$REPO\" | |\n| if git diff --cached --quiet; then | |\n| echo \"No staged files. Run 'git add' on your changes before running this action.\" | |\n| exit 1 | |\n| fi | |\n| CACHE_DIR=\"$HOME/.cache/sourcetree-ai-commit\" | |\n| mkdir -p \"$CACHE_DIR\" | |\n| CACHE_KEY=$(printf '%s' \"$REPO\" | tr '/' '_') | |\n| CACHE_FILE=\"$CACHE_DIR/${CACHE_KEY}.txt\" | |\n| generate_message() { | |\n| if ! command -v claude >/dev/null 2>&1; then | |\n| echo \"Error: 'claude' CLI not found in PATH.\" | |\n| exit 1 | |\n| fi | |\n| STAT=$(git diff --cached --stat) | |\n| RAW_DIFF=$(git diff --cached -- . \\ | |\n| ':(exclude)*.lock' \\ | |\n| ':(exclude)*.resolved' \\ | |\n| ':(exclude)*.xcworkspace/**' \\ | |\n| ':(exclude)*.pbxproj') | |\n| # Truncate with bash parameter expansion instead of \"| head -c\": with | |\n| # `pipefail` enabled, `head -c` closes the pipe as soon as it has enough | |\n| # bytes, `git diff` gets SIGPIPE and the whole pipeline \"fails\" (exit 141), | |\n| # killing the script with no output because of `set -e`. | |\n| DIFF=\"${RAW_DIFF:0:12000}\" | |\n| PROMPT=\"You are a commit message generator. Analyze the staged diff and return ONLY the commit message, with no explanations and no surrounding quotes. | |\n| Rules: | |\n| - Conventional Commits format (feat, fix, refactor, chore, docs, test, style, perf) followed by ':'. | |\n| - English, imperative mood, subject line max 72 characters. | |\n| - If the change warrants it, add a short bulleted body after a blank line explaining 'why', not 'what'. | |\n| - If there are several unrelated changes, prioritize the most relevant one in the subject. | |\n| - Plain text only: no markdown whatsoever — no backticks, no bold/italic with * or _, no headers, no code fences. Write identifiers (class/function names, etc.) as plain words, not wrapped in special quoting. | |\n| File summary: | |\n| ${STAT} | |\n| Diff: | |\n| ${DIFF}\" | |\n| MESSAGE=$(claude -p \"$PROMPT\" --output-format text 2>/dev/null | sed -e '/^[[:space:]]*$/d' -e '1{/^```/d;}' -e '${/^```$/d;}' -e 's/`//g') | |\n| if [ -z \"$MESSAGE\" ]; then | |\n| echo \"Error: Claude did not return a message.\" | |\n| exit 1 | |\n| fi | |\n| } | |\n| if [ -f \"$CACHE_FILE\" ]; then | |\n| MESSAGE=$(<\"$CACHE_FILE\") | |\n| else | |\n| generate_message | |\n| fi | |\n| escape_for_applescript() { | |\n| local input=\"$1\" | |\n| input=\"${input//\\\\/\\\\\\\\}\" | |\n| input=\"${input//\\\"/\\\\\\\"}\" | |\n| printf '%s' \"$input\" | |\n| } | |\n| while true; do | |\n| ESCAPED_MESSAGE=$(escape_for_applescript \"$MESSAGE\") | |\n| # AppleScript doesn't interpret \"\\n\" inside a string: each line break must be | |\n| # concatenated explicitly as the ASCII 10 character between literals. | |\n| AS_DEFAULT_ANSWER=$(printf '%s' \"$ESCAPED_MESSAGE\" | awk 'BEGIN{ORS=\"\"} {if(NR>1) printf \" & (ASCII character 10) & \"; printf \"\\\"%s\\\"\", $0}') | |\n| set +e | |\n| DIALOG_RESULT=$(osascript <<EOF | |\n| set msgText to $AS_DEFAULT_ANSWER | |\n| set dialogResult to display dialog \"Review and edit the commit message generated by Claude:\" default answer msgText with title \"Confirm commit\" buttons {\"Cancel\", \"Regenerate\", \"Commit\"} default button \"Commit\" cancel button \"Cancel\" with icon note | |\n| return (button returned of dialogResult) & linefeed & (text returned of dialogResult) | |\n| EOF | |\n| ) | |\n| DIALOG_EXIT=$? | |\n| set -e | |\n| if [ $DIALOG_EXIT -ne 0 ]; then | |\n| echo \"Commit canceled.\" | |\n| exit 0 | |\n| fi | |\n| # First line is the button name, everything after the first newline is | |\n| # the (possibly multi-line) edited message text. | |\n| DIALOG_BUTTON=\"${DIALOG_RESULT%%$'\\n'*}\" | |\n| DIALOG_TEXT=\"${DIALOG_RESULT#*$'\\n'}\" | |\n| if [ \"$DIALOG_BUTTON\" = \"Regenerate\" ]; then | |\n| generate_message | |\n| continue | |\n| fi | |\n| MESSAGE=\"$DIALOG_TEXT\" | |\n| break | |\n| done | |\n| TMP_MSG_FILE=$(mktemp) | |\n| trap 'rm -f \"$TMP_MSG_FILE\"' EXIT | |\n| echo \"$MESSAGE\" > \"$TMP_MSG_FILE\" | |\n| git commit -F \"$TMP_MSG_FILE\" | |\n| echo \"$MESSAGE\" > \"$CACHE_FILE\" | |\n| echo \"Commit created with message:\" | |\n| echo \"---\" | |\n| echo \"$MESSAGE\" |", "url": "https://wpnews.pro/news/sourcetree-ai-commit-message", "canonical_source": "https://gist.github.com/AlexApostolSource/45a1bdd43d5fae9b03c06b53ec70c29c", "published_at": "2026-07-12 15:33:15+00:00", "updated_at": "2026-07-13 05:39:06.136004+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "generative-ai"], "entities": ["SourceTree", "Claude", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/sourcetree-ai-commit-message", "markdown": "https://wpnews.pro/news/sourcetree-ai-commit-message.md", "text": "https://wpnews.pro/news/sourcetree-ai-commit-message.txt", "jsonld": "https://wpnews.pro/news/sourcetree-ai-commit-message.jsonld"}}