#!/bin/bash set -euo pipefail # Always inspect and update the repository's real index. An inherited alternate # index could otherwise make the safety check observe different staged content. unset GIT_INDEX_FILE CONVENTIONAL_PATTERN='^(feat|fix|ref|refactor|perf|docs|test|build|ci|chore|style|meta|license|revert)(\([A-Za-z0-9._/-]+\))?(!)?: .+' MESSAGE="${1:-chore: update code}" shift || true validate_message() { local subject=$1 if [[ ! "$subject" =~ $CONVENTIONAL_PATTERN ]]; then echo "Commit message must use the conventional '(): ' format." >&2 exit 1 fi } validate_message "$MESSAGE" PATH_MODE=false if [[ "${1:-}" == "--" ]]; then PATH_MODE=true shift elif [[ "$#" -gt 0 ]]; then echo "Pass selected paths after an explicit -- separator." >&2 exit 1 fi if [[ "$PATH_MODE" == true && "$#" -eq 0 ]]; then echo "The -- separator requires at least one path." >&2 exit 1 fi BRANCH=$(git symbolic-ref --quiet --short HEAD) || { echo "Refusing to commit from a detached HEAD." >&2 exit 1 } REF_FORMAT=$(git rev-parse --show-ref-format 2>/dev/null || true) if [[ -z "$REF_FORMAT" || "$REF_FORMAT" == "--show-ref-format" ]]; then REF_FORMAT=$(git config --get extensions.refStorage || true) REF_FORMAT=${REF_FORMAT:-files} fi if [[ "$REF_FORMAT" != files ]]; then echo "Refusing to run with ref backend '$REF_FORMAT'; safe branch locking currently requires the files backend." >&2 exit 1 fi PUSH_REMOTE=$(git config --get "branch.$BRANCH.pushRemote" || true) if [[ -z "$PUSH_REMOTE" ]]; then PUSH_REMOTE=$(git config --get remote.pushDefault || true) fi FETCH_REMOTE=$(git config --get "branch.$BRANCH.remote" || true) MERGE_REF=$(git config --get "branch.$BRANCH.merge" || true) if [[ -z "$PUSH_REMOTE" ]]; then PUSH_REMOTE=${FETCH_REMOTE:-origin} fi if [[ "$PUSH_REMOTE" == "." ]] || ! git remote get-url --push "$PUSH_REMOTE" >/dev/null 2>&1; then echo "Configured push remote '$PUSH_REMOTE' does not exist or is not pushable." >&2 exit 1 fi PUSH_BRANCH=$BRANCH if [[ "$PUSH_REMOTE" == "$FETCH_REMOTE" && -n "$MERGE_REF" ]]; then if [[ "$MERGE_REF" != refs/heads/* ]]; then echo "Configured upstream '$MERGE_REF' is not a pushable branch ref." >&2 exit 1 fi PUSH_BRANCH=${MERGE_REF#refs/heads/} fi HAS_UPSTREAM=false if git rev-parse --verify --quiet '@{upstream}' >/dev/null; then HAS_UPSTREAM=true fi SET_UPSTREAM=false if [[ "$HAS_UPSTREAM" == false && "$PUSH_REMOTE" == origin ]]; then SET_UPSTREAM=true fi GIT_DIR=$(git rev-parse --absolute-git-dir) GIT_COMMON_DIR=$(git rev-parse --path-format=absolute --git-common-dir) LIVE_INDEX="$GIT_DIR/index" LIVE_INDEX_LOCK="$LIVE_INDEX.lock" BRANCH_REF="$GIT_COMMON_DIR/refs/heads/$BRANCH" BRANCH_REF_LOCK="$BRANCH_REF.lock" TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/smart-commit.XXXXXX") TEMP_INDEX="$TEMP_DIR/index" MESSAGE_FILE="$TEMP_DIR/COMMIT_EDITMSG" LOCK_HELD=false BRANCH_LOCK_HELD=false cleanup() { rm -rf "$TEMP_DIR" if [[ "$LOCK_HELD" == true ]]; then rm -f "$LIVE_INDEX_LOCK" fi if [[ "$BRANCH_LOCK_HELD" == true ]]; then rm -f "$BRANCH_REF_LOCK" fi } trap cleanup EXIT if ! (set -o noclobber; : > "$LIVE_INDEX_LOCK") 2>/dev/null; then echo "The Git index is busy; refusing to race another staging operation." >&2 exit 1 fi LOCK_HELD=true if ! git diff --cached --quiet; then echo "Refusing to commit because the index already contains staged changes." >&2 echo "Commit or unstage the existing index, then retry with the intended paths." >&2 exit 1 fi PARENT=$(git rev-parse --verify HEAD) if [[ -f "$LIVE_INDEX" ]]; then cp "$LIVE_INDEX" "$TEMP_INDEX" else GIT_INDEX_FILE="$TEMP_INDEX" git read-tree "$PARENT" fi if [[ "$PATH_MODE" == true ]]; then GIT_INDEX_FILE="$TEMP_INDEX" git add -- "$@" else GIT_INDEX_FILE="$TEMP_INDEX" git add -A fi if GIT_INDEX_FILE="$TEMP_INDEX" git diff --cached --quiet; then echo "No changes staged for commit." >&2 exit 1 fi EXPECTED_TREE=$(GIT_INDEX_FILE="$TEMP_INDEX" git write-tree) printf '%s\n' "$MESSAGE" > "$MESSAGE_FILE" GIT_INDEX_FILE="$TEMP_INDEX" git hook run --ignore-missing pre-commit if [[ $(GIT_INDEX_FILE="$TEMP_INDEX" git write-tree) != "$EXPECTED_TREE" ]]; then echo "Pre-commit hooks changed the isolated index; review those changes before retrying." >&2 exit 1 fi GIT_INDEX_FILE="$TEMP_INDEX" git hook run --ignore-missing prepare-commit-msg -- "$MESSAGE_FILE" message GIT_INDEX_FILE="$TEMP_INDEX" git hook run --ignore-missing commit-msg -- "$MESSAGE_FILE" if [[ $(GIT_INDEX_FILE="$TEMP_INDEX" git write-tree) != "$EXPECTED_TREE" ]]; then echo "Commit hooks changed the isolated index; review those changes before retrying." >&2 exit 1 fi validate_message "$(head -n 1 "$MESSAGE_FILE")" if [[ $(git config --bool commit.gpgsign || true) == true ]]; then CREATED_COMMIT=$(git commit-tree -S "$EXPECTED_TREE" -p "$PARENT" -F "$MESSAGE_FILE") else CREATED_COMMIT=$(git commit-tree "$EXPECTED_TREE" -p "$PARENT" -F "$MESSAGE_FILE") fi if ! git update-ref "refs/heads/$BRANCH" "$CREATED_COMMIT" "$PARENT"; then echo "The branch changed concurrently; refusing to replace or push it." >&2 exit 1 fi mkdir -p "$(dirname "$BRANCH_REF_LOCK")" if ! (set -o noclobber; : > "$BRANCH_REF_LOCK") 2>/dev/null; then CURRENT_BRANCH_COMMIT=$(git rev-parse --verify "refs/heads/$BRANCH") if [[ "$CURRENT_BRANCH_COMMIT" != "$CREATED_COMMIT" ]]; then GIT_INDEX_FILE="$TEMP_INDEX" git read-tree "$CURRENT_BRANCH_COMMIT" fi cp "$TEMP_INDEX" "$LIVE_INDEX_LOCK" mv -f "$LIVE_INDEX_LOCK" "$LIVE_INDEX" LOCK_HELD=false echo "The branch ref is busy after commit creation; refusing to continue to hooks or push." >&2 exit 1 fi BRANCH_LOCK_HELD=true CURRENT_BRANCH_COMMIT=$(git rev-parse --verify "refs/heads/$BRANCH") if [[ "$CURRENT_BRANCH_COMMIT" != "$CREATED_COMMIT" ]]; then GIT_INDEX_FILE="$TEMP_INDEX" git read-tree "$CURRENT_BRANCH_COMMIT" cp "$TEMP_INDEX" "$LIVE_INDEX_LOCK" mv -f "$LIVE_INDEX_LOCK" "$LIVE_INDEX" LOCK_HELD=false echo "The branch changed before its ref lock was acquired; refusing to push." >&2 exit 1 fi if ! GIT_INDEX_FILE="$TEMP_INDEX" git hook run --ignore-missing post-commit; then echo "Warning: post-commit hook failed after the commit was created; continuing with a consistent index." >&2 fi CURRENT_BRANCH_COMMIT=$(git rev-parse --verify "refs/heads/$BRANCH") if [[ "$CURRENT_BRANCH_COMMIT" != "$CREATED_COMMIT" ]]; then GIT_INDEX_FILE="$TEMP_INDEX" git read-tree "$CURRENT_BRANCH_COMMIT" cp "$TEMP_INDEX" "$LIVE_INDEX_LOCK" mv -f "$LIVE_INDEX_LOCK" "$LIVE_INDEX" LOCK_HELD=false echo "The branch changed after commit creation; refusing to push another commit." >&2 exit 1 fi cp "$TEMP_INDEX" "$LIVE_INDEX_LOCK" mv -f "$LIVE_INDEX_LOCK" "$LIVE_INDEX" LOCK_HELD=false PUSH_REFSPEC="$CREATED_COMMIT:refs/heads/$PUSH_BRANCH" git push "$PUSH_REMOTE" "$PUSH_REFSPEC" if [[ "$SET_UPSTREAM" == true ]]; then git config "branch.$BRANCH.remote" "$PUSH_REMOTE" git config "branch.$BRANCH.merge" "refs/heads/$PUSH_BRANCH" fi rm -f "$BRANCH_REF_LOCK" BRANCH_LOCK_HELD=false echo "✅ Successfully pushed $BRANCH to $PUSH_REMOTE/$PUSH_BRANCH"