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