1#!/bin/bash 2 3usage() { 4 echo "Usage: $(basename "$0") [--selftest] [--force] <commit-id> [commit-subject]" 5 echo "Resolves a short git commit ID to its full SHA-1 hash, particularly useful for fixing references in commit messages." 6 echo "" 7 echo "Arguments:" 8 echo " --selftest Run self-tests" 9 echo " --force Try to find commit by subject if ID lookup fails" 10 echo " commit-id Short git commit ID to resolve" 11 echo " commit-subject Optional commit subject to help resolve between multiple matches" 12 exit 1 13} 14 15# Convert subject with ellipsis to grep pattern 16convert_to_grep_pattern() { 17 local subject="$1" 18 # First escape ALL regex special characters 19 local escaped_subject 20 escaped_subject=$(printf '%s\n' "$subject" | sed 's/[[\.*^$()+?{}|]/\\&/g') 21 # Also escape colons, parentheses, and hyphens as they are special in our context 22 escaped_subject=$(echo "$escaped_subject" | sed 's/[:-]/\\&/g') 23 # Then convert escaped ... sequence to .*? 24 escaped_subject=$(echo "$escaped_subject" | sed 's/\\\.\\\.\\\./.*?/g') 25 echo "^${escaped_subject}$" 26} 27 28git_resolve_commit() { 29 local force=0 30 if [ "$1" = "--force" ]; then 31 force=1 32 shift 33 fi 34 35 # Split input into commit ID and subject 36 local input="$*" 37 local commit_id="${input%% *}" 38 local subject="" 39 40 # Extract subject if present (everything after the first space) 41 if [[ "$input" == *" "* ]]; then 42 subject="${input#* }" 43 # Strip the ("...") quotes if present 44 subject="${subject#*(\"}" 45 subject="${subject%\")*}" 46 fi 47 48 # Get all possible matching commit IDs 49 local matches 50 readarray -t matches < <(git rev-parse --disambiguate="$commit_id" 2>/dev/null) 51 52 # Return immediately if we have exactly one match 53 if [ ${#matches[@]} -eq 1 ]; then 54 echo "${matches[0]}" 55 return 0 56 fi 57 58 # If no matches and not in force mode, return failure 59 if [ ${#matches[@]} -eq 0 ] && [ $force -eq 0 ]; then 60 return 1 61 fi 62 63 # If we have a subject, try to find a match with that subject 64 if [ -n "$subject" ]; then 65 # Convert subject with possible ellipsis to grep pattern 66 local grep_pattern 67 grep_pattern=$(convert_to_grep_pattern "$subject") 68 69 # In force mode with no ID matches, use git log --grep directly 70 if [ ${#matches[@]} -eq 0 ] && [ $force -eq 1 ]; then 71 # Use git log to search, but filter to ensure subject matches exactly 72 local match 73 match=$(git log --format="%H %s" --grep="$grep_pattern" --perl-regexp -10 | \ 74 while read -r hash subject; do 75 if echo "$subject" | grep -qP "$grep_pattern"; then 76 echo "$hash" 77 break 78 fi 79 done) 80 if [ -n "$match" ]; then 81 echo "$match" 82 return 0 83 fi 84 else 85 # Normal subject matching for existing matches 86 for match in "${matches[@]}"; do 87 if git log -1 --format="%s" "$match" | grep -qP "$grep_pattern"; then 88 echo "$match" 89 return 0 90 fi 91 done 92 fi 93 fi 94 95 # No match found 96 return 1 97} 98 99run_selftest() { 100 local test_cases=( 101 '00250b5 ("MAINTAINERS: add new Rockchip SoC list")' 102 '0037727 ("KVM: selftests: Convert xen_shinfo_test away from VCPU_ID")' 103 'ffef737 ("net/tls: Fix skb memory leak when running kTLS traffic")' 104 'd3d7 ("cifs: Improve guard for excluding $LXDEV xattr")' 105 'dbef ("Rename .data.once to .data..once to fix resetting WARN*_ONCE")' 106 '12345678' # Non-existent commit 107 '12345 ("I'\''m a dummy commit")' # Valid prefix but wrong subject 108 '--force 99999999 ("net/tls: Fix skb memory leak when running kTLS traffic")' # Force mode with non-existent ID but valid subject 109 '83be ("firmware: ... auto-update: fix poll_complete() ... errors")' # Wildcard test 110 '--force 999999999999 ("firmware: ... auto-update: fix poll_complete() ... errors")' # Force mode wildcard test 111 ) 112 113 local expected=( 114 "00250b529313d6262bb0ebbd6bdf0a88c809f6f0" 115 "0037727b3989c3fe1929c89a9a1dfe289ad86f58" 116 "ffef737fd0372ca462b5be3e7a592a8929a82752" 117 "d3d797e326533794c3f707ce1761da7a8895458c" 118 "dbefa1f31a91670c9e7dac9b559625336206466f" 119 "" # Expect empty output for non-existent commit 120 "" # Expect empty output for wrong subject 121 "ffef737fd0372ca462b5be3e7a592a8929a82752" # Should find commit by subject in force mode 122 "83beece5aff75879bdfc6df8ba84ea88fd93050e" # Wildcard test 123 "83beece5aff75879bdfc6df8ba84ea88fd93050e" # Force mode wildcard test 124 ) 125 126 local expected_exit_codes=( 127 0 128 0 129 0 130 0 131 0 132 1 # Expect failure for non-existent commit 133 1 # Expect failure for wrong subject 134 0 # Should succeed in force mode 135 0 # Should succeed with wildcard 136 0 # Should succeed with force mode and wildcard 137 ) 138 139 local failed=0 140 141 echo "Running self-tests..." 142 for i in "${!test_cases[@]}"; do 143 # Capture both output and exit code 144 local result 145 result=$(git_resolve_commit ${test_cases[$i]}) # Removed quotes to allow --force to be parsed 146 local exit_code=$? 147 148 # Check both output and exit code 149 if [ "$result" != "${expected[$i]}" ] || [ $exit_code != ${expected_exit_codes[$i]} ]; then 150 echo "Test case $((i+1)) FAILED" 151 echo "Input: ${test_cases[$i]}" 152 echo "Expected output: '${expected[$i]}'" 153 echo "Got output: '$result'" 154 echo "Expected exit code: ${expected_exit_codes[$i]}" 155 echo "Got exit code: $exit_code" 156 failed=1 157 else 158 echo "Test case $((i+1)) PASSED" 159 fi 160 done 161 162 if [ $failed -eq 0 ]; then 163 echo "All tests passed!" 164 exit 0 165 else 166 echo "Some tests failed!" 167 exit 1 168 fi 169} 170 171# Check for selftest 172if [ "$1" = "--selftest" ]; then 173 run_selftest 174 exit $? 175fi 176 177# Handle --force flag 178force="" 179if [ "$1" = "--force" ]; then 180 force="--force" 181 shift 182fi 183 184# Verify arguments 185if [ $# -eq 0 ]; then 186 usage 187fi 188 189# Skip validation in force mode 190if [ -z "$force" ]; then 191 # Validate that the first argument matches at least one git commit 192 if [ "$(git rev-parse --disambiguate="$1" 2>/dev/null | wc -l)" -eq 0 ]; then 193 echo "Error: '$1' does not match any git commit" 194 exit 1 195 fi 196fi 197 198git_resolve_commit $force "$@" 199exit $? 200