File size: 3,396 Bytes
fc93158 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | #!/usr/bin/env bash
set -euo pipefail
# Disable glob expansion to handle brackets in file paths
set -f
usage() {
printf 'Usage: %s [--force] "commit message" "file" ["file" ...]\n' "$(basename "$0")" >&2
exit 2
}
if [ "$#" -lt 2 ]; then
usage
fi
force_delete_lock=false
if [ "${1:-}" = "--force" ]; then
force_delete_lock=true
shift
fi
if [ "$#" -lt 2 ]; then
usage
fi
commit_message=$1
shift
if [[ "$commit_message" != *[![:space:]]* ]]; then
printf 'Error: commit message must not be empty\n' >&2
exit 1
fi
if [ -e "$commit_message" ]; then
printf 'Error: first argument looks like a file path ("%s"); provide the commit message first\n' "$commit_message" >&2
exit 1
fi
if [ "$#" -eq 0 ]; then
usage
fi
files=("$@")
# Disallow "." because it stages the entire repository and defeats the helper's safety guardrails.
for file in "${files[@]}"; do
if [ "$file" = "." ]; then
printf 'Error: "." is not allowed; list specific paths instead\n' >&2
exit 1
fi
done
# Prevent staging node_modules even if a path is forced.
for file in "${files[@]}"; do
case "$file" in
*node_modules* | */node_modules | */node_modules/* | node_modules)
printf 'Error: node_modules paths are not allowed: %s\n' "$file" >&2
exit 1
;;
esac
done
last_commit_error=''
run_git_command() {
local stderr_log
stderr_log=$(mktemp)
if "$@" 2> >(tee "$stderr_log" >&2); then
rm -f "$stderr_log"
last_commit_error=''
return 0
fi
last_commit_error=$(cat "$stderr_log")
rm -f "$stderr_log"
return 1
}
is_git_lock_error() {
printf '%s\n' "$last_commit_error" | grep -Eq \
"Another git process seems to be running|Unable to create '.*\\.git/[^']+\\.lock'"
}
extract_git_lock_path() {
printf '%s\n' "$last_commit_error" |
sed -n "s/.*'\(.*\.git\/[^']*\.lock\)'.*/\1/p" |
head -n 1
}
run_git_with_lock_retry() {
local label=$1
shift
local deadline=$((SECONDS + 5))
local announced_retry=false
while true; do
if run_git_command "$@"; then
return 0
fi
if ! is_git_lock_error; then
return 1
fi
if [ "$SECONDS" -ge "$deadline" ]; then
break
fi
if [ "$announced_retry" = false ]; then
printf 'Git lock during %s; retrying for up to 5 seconds...\n' "$label" >&2
announced_retry=true
fi
sleep 0.5
done
if [ "$force_delete_lock" = true ]; then
local lock_path
lock_path=$(extract_git_lock_path)
if [ -n "$lock_path" ] && [ -e "$lock_path" ]; then
rm -f "$lock_path"
printf 'Removed stale git lock: %s\n' "$lock_path" >&2
run_git_command "$@"
return $?
fi
fi
return 1
}
for file in "${files[@]}"; do
if [ ! -e "$file" ]; then
if ! git ls-files --error-unmatch -- "$file" >/dev/null 2>&1; then
printf 'Error: file not found: %s\n' "$file" >&2
exit 1
fi
fi
done
run_git_with_lock_retry "unstaging files" git restore --staged :/
run_git_with_lock_retry "staging files" git add --force -- "${files[@]}"
if git diff --staged --quiet; then
printf 'Warning: no staged changes detected for: %s\n' "${files[*]}" >&2
exit 1
fi
committed=false
if run_git_with_lock_retry "commit" git commit -m "$commit_message" -- "${files[@]}"; then
committed=true
fi
if [ "$committed" = false ]; then
exit 1
fi
printf 'Committed "%s" with %d files\n' "$commit_message" "${#files[@]}"
|