Commit 7ae52a3d authored by Sasha Levin's avatar Sasha Levin Committed by Greg Kroah-Hartman
Browse files

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



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: default avatarSasha Levin <sashal@kernel.org>
Reviewed-by: default avatarKees Cook <kees@kernel.org>
Link: https://lore.kernel.org/r/20250311165336.248120-1-sashal@kernel.org


Signed-off-by: default avatarGreg Kroah-Hartman <gregkh@linuxfoundation.org>
parent d062463e
Loading
Loading
Loading
Loading

scripts/git-resolve.sh

0 → 100755
+199 −0
Original line number Diff line number Diff line
#!/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 $?