[PATCH] scripts: Add git-resolve tool for full SHA-1 resolution

Sasha Levin
2025-03-11T16:53:47Z

Introduce git-resolve.sh, a tool that resolves short git commit IDs to their
full SHA-1 hash. This is particularly useful for navigating references in commit
messages and verifying Fixes tags.

When faced with ambiguous commit IDs or imprecise references in messages,
this tool can help by resolving commit hashes based on not just the ID
itself but also the commit subject, making it more robust than standard
git rev-parse.

This is especially valuable for maintainers who need to verify Fixes tags
or cross-reference commits. Unlike proposals to add dates to Fixes tags
(which would break existing tooling), this script provides a way to
disambiguate commits without changing the established tag format.

The script includes several features:
- Resolves short commit IDs to full SHA-1 hashes
- Uses commit subjects to disambiguate between multiple potential matches
- Supports wildcard patterns in subjects with ellipsis (...)
- Provides a force mode to attempt resolution by subject when ID lookup fails
- Includes comprehensive self-tests

Signed-off-by: Sasha Levin <sashal@kernel.org>
---
scripts/git-resolve.sh | 199 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 199 insertions(+)
create mode 100755 scripts/git-resolve.sh

diff --git a/scripts/git-resolve.sh b/scripts/git-resolve.sh
new file mode 100755
index 0000000000000..204a8c9bd4ea8
--- /dev/null
+++ b/scripts/git-resolve.sh
@@ -0,0 +1,199 @@
+#!/bin/bash
+
+usage() {
+ echo "Usage: $(basename "$0") [--selftest] [--force] <commit-id> [commit-subject]"
+ echo "Resolves a short git commit ID to its full SHA-1 hash, particularly useful for fixing references in commit messages."
+ echo ""
+ echo "Arguments:"
+ echo " --selftest Run self-tests"
+ echo " --force Try to find commit by subject if ID lookup fails"
+ echo " commit-id Short git commit ID to resolve"
+ echo " commit-subject Optional commit subject to help resolve between multiple matches"
+ exit 1
+}
+
+# Convert subject with ellipsis to grep pattern
+convert_to_grep_pattern() {
+ local subject="$1"
+ # First escape ALL regex special characters
+ local escaped_subject
+ escaped_subject=$(printf '%s\n' "$subject" | sed 's/[[\.*^$()+?{}|]/\\&/g')
+ # Also escape colons, parentheses, and hyphens as they are special in our context
+ escaped_subject=$(echo "$escaped_subject" | sed 's/[:-]/\\&/g')
+ # Then convert escaped ... sequence to .*?
+ escaped_subject=$(echo "$escaped_subject" | sed 's/\\\.\\\.\\\./.*?/g')
+ echo "^${escaped_subject}$"
+}
+
+git_resolve_commit() {
+ local force=0
+ if [ "$1" = "--force" ]; then
+ force=1
+ shift
+ fi
+
+ # Split input into commit ID and subject
+ local input="$*"
+ local commit_id="${input%% *}"
+ local subject=""
+
+ # Extract subject if present (everything after the first space)
+ if [[ "$input" == *" "* ]]; then
+ subject="${input#* }"
+ # Strip the ("...") quotes if present
+ subject="${subject#*(\"}"
+ subject="${subject%\")*}"
+ fi
+
+ # Get all possible matching commit IDs
+ local matches
+ readarray -t matches < <(git rev-parse --disambiguate="$commit_id" 2>/dev/null)
+
+ # Return immediately if we have exactly one match
+ if [ ${#matches[@]} -eq 1 ]; then
+ echo "${matches[0]}"
+ return 0
+ fi
+
+ # If no matches and not in force mode, return failure
+ if [ ${#matches[@]} -eq 0 ] && [ $force -eq 0 ]; then
+ return 1
+ fi
+
+ # If we have a subject, try to find a match with that subject
+ if [ -n "$subject" ]; then
+ # Convert subject with possible ellipsis to grep pattern
+ local grep_pattern
+ grep_pattern=$(convert_to_grep_pattern "$subject")
+
+ # In force mode with no ID matches, use git log --grep directly
+ if [ ${#matches[@]} -eq 0 ] && [ $force -eq 1 ]; then
+ # Use git log to search, but filter to ensure subject matches exactly
+ local match
+ match=$(git log --format="%H %s" --grep="$grep_pattern" --perl-regexp -10 | \
+ while read -r hash subject; do
+ if echo "$subject" | grep -qP "$grep_pattern"; then
+ echo "$hash"
+ break
+ fi
+ done)
+ if [ -n "$match" ]; then
+ echo "$match"
+ return 0
+ fi
+ else
+ # Normal subject matching for existing matches
+ for match in "${matches[@]}"; do
+ if git log -1 --format="%s" "$match" | grep -qP "$grep_pattern"; then
+ echo "$match"
+ return 0
+ fi
+ done
+ fi
+ fi
+
+ # No match found
+ return 1
+}
+
+run_selftest() {
+ local test_cases=(
+ '00250b5 ("MAINTAINERS: add new Rockchip SoC list")'
+ '0037727 ("KVM: selftests: Convert xen_shinfo_test away from VCPU_ID")'
+ 'ffef737 ("net/tls: Fix skb memory leak when running kTLS traffic")'
+ 'd3d7 ("cifs: Improve guard for excluding $LXDEV xattr")'
+ 'dbef ("Rename .data.once to .data..once to fix resetting WARN*_ONCE")'
+ '12345678' # Non-existent commit
+ '12345 ("I'\''m a dummy commit")' # Valid prefix but wrong subject
+ '--force 99999999 ("net/tls: Fix skb memory leak when running kTLS traffic")' # Force mode with non-existent ID but valid subject
+ '83be ("firmware: ... auto-update: fix poll_complete() ... errors")' # Wildcard test
+ '--force 999999999999 ("firmware: ... auto-update: fix poll_complete() ... errors")' # Force mode wildcard test
+ )
+
+ local expected=(
+ "00250b529313d6262bb0ebbd6bdf0a88c809f6f0"
+ "0037727b3989c3fe1929c89a9a1dfe289ad86f58"
+ "ffef737fd0372ca462b5be3e7a592a8929a82752"
+ "d3d797e326533794c3f707ce1761da7a8895458c"
+ "dbefa1f31a91670c9e7dac9b559625336206466f"
+ "" # Expect empty output for non-existent commit
+ "" # Expect empty output for wrong subject
+ "ffef737fd0372ca462b5be3e7a592a8929a82752" # Should find commit by subject in force mode
+ "83beece5aff75879bdfc6df8ba84ea88fd93050e" # Wildcard test
+ "83beece5aff75879bdfc6df8ba84ea88fd93050e" # Force mode wildcard test
+ )
+
+ local expected_exit_codes=(
+ 0
+ 0
+ 0
+ 0
+ 0
+ 1 # Expect failure for non-existent commit
+ 1 # Expect failure for wrong subject
+ 0 # Should succeed in force mode
+ 0 # Should succeed with wildcard
+ 0 # Should succeed with force mode and wildcard
+ )
+
+ local failed=0
+
+ echo "Running self-tests..."
+ for i in "${!test_cases[@]}"; do
+ # Capture both output and exit code
+ local result
+ result=$(git_resolve_commit ${test_cases[$i]}) # Removed quotes to allow --force to be parsed
+ local exit_code=$?
+
+ # Check both output and exit code
+ if [ "$result" != "${expected[$i]}" ] || [ $exit_code != ${expected_exit_codes[$i]} ]; then
+ echo "Test case $((i+1)) FAILED"
+ echo "Input: ${test_cases[$i]}"
+ echo "Expected output: '${expected[$i]}'"
+ echo "Got output: '$result'"
+ echo "Expected exit code: ${expected_exit_codes[$i]}"
+ echo "Got exit code: $exit_code"
+ failed=1
+ else
+ echo "Test case $((i+1)) PASSED"
+ fi
+ done
+
+ if [ $failed -eq 0 ]; then
+ echo "All tests passed!"
+ exit 0
+ else
+ echo "Some tests failed!"
+ exit 1
+ fi
+}
+
+# Check for selftest
+if [ "$1" = "--selftest" ]; then
+ run_selftest
+ exit $?
+fi
+
+# Handle --force flag
+force=""
+if [ "$1" = "--force" ]; then
+ force="--force"
+ shift
+fi
+
+# Verify arguments
+if [ $# -eq 0 ]; then
+ usage
+fi
+
+# Skip validation in force mode
+if [ -z "$force" ]; then
+ # Validate that the first argument matches at least one git commit
+ if [ "$(git rev-parse --disambiguate="$1" 2>/dev/null | wc -l)" -eq 0 ]; then
+ echo "Error: '$1' does not match any git commit"
+ exit 1
+ fi
+fi
+
+git_resolve_commit $force "$@"
+exit $?
--
2.39.5