| #!/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 $? |