blob: e9b5940c0f2837895ffd88b0f540031c538d5436 [file] [log] [blame] [edit]
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# (c) 2025, Sasha Levin <sashal@kernel.org>
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 $?