git-resolve.sh 5.8 KB

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