xref: /linux/scripts/git-resolve.sh (revision c26f4fbd58375bd6ef74f95eb73d61762ad97c59)
17ae52a3dSSasha Levin#!/bin/bash
2*d0fd663aSSasha Levin# SPDX-License-Identifier: GPL-2.0
3*d0fd663aSSasha Levin# (c) 2025, Sasha Levin <sashal@kernel.org>
47ae52a3dSSasha Levin
57ae52a3dSSasha Levinusage() {
67ae52a3dSSasha Levin	echo "Usage: $(basename "$0") [--selftest] [--force] <commit-id> [commit-subject]"
77ae52a3dSSasha Levin	echo "Resolves a short git commit ID to its full SHA-1 hash, particularly useful for fixing references in commit messages."
87ae52a3dSSasha Levin	echo ""
97ae52a3dSSasha Levin	echo "Arguments:"
107ae52a3dSSasha Levin	echo "  --selftest      Run self-tests"
117ae52a3dSSasha Levin	echo "  --force         Try to find commit by subject if ID lookup fails"
127ae52a3dSSasha Levin	echo "  commit-id       Short git commit ID to resolve"
137ae52a3dSSasha Levin	echo "  commit-subject  Optional commit subject to help resolve between multiple matches"
147ae52a3dSSasha Levin	exit 1
157ae52a3dSSasha Levin}
167ae52a3dSSasha Levin
177ae52a3dSSasha Levin# Convert subject with ellipsis to grep pattern
187ae52a3dSSasha Levinconvert_to_grep_pattern() {
197ae52a3dSSasha Levin	local subject="$1"
207ae52a3dSSasha Levin	# First escape ALL regex special characters
217ae52a3dSSasha Levin	local escaped_subject
227ae52a3dSSasha Levin	escaped_subject=$(printf '%s\n' "$subject" | sed 's/[[\.*^$()+?{}|]/\\&/g')
237ae52a3dSSasha Levin	# Also escape colons, parentheses, and hyphens as they are special in our context
247ae52a3dSSasha Levin	escaped_subject=$(echo "$escaped_subject" | sed 's/[:-]/\\&/g')
257ae52a3dSSasha Levin	# Then convert escaped ... sequence to .*?
267ae52a3dSSasha Levin	escaped_subject=$(echo "$escaped_subject" | sed 's/\\\.\\\.\\\./.*?/g')
277ae52a3dSSasha Levin	echo "^${escaped_subject}$"
287ae52a3dSSasha Levin}
297ae52a3dSSasha Levin
307ae52a3dSSasha Levingit_resolve_commit() {
317ae52a3dSSasha Levin	local force=0
327ae52a3dSSasha Levin	if [ "$1" = "--force" ]; then
337ae52a3dSSasha Levin		force=1
347ae52a3dSSasha Levin		shift
357ae52a3dSSasha Levin	fi
367ae52a3dSSasha Levin
377ae52a3dSSasha Levin	# Split input into commit ID and subject
387ae52a3dSSasha Levin	local input="$*"
397ae52a3dSSasha Levin	local commit_id="${input%% *}"
407ae52a3dSSasha Levin	local subject=""
417ae52a3dSSasha Levin
427ae52a3dSSasha Levin	# Extract subject if present (everything after the first space)
437ae52a3dSSasha Levin	if [[ "$input" == *" "* ]]; then
447ae52a3dSSasha Levin		subject="${input#* }"
457ae52a3dSSasha Levin		# Strip the ("...") quotes if present
467ae52a3dSSasha Levin		subject="${subject#*(\"}"
477ae52a3dSSasha Levin		subject="${subject%\")*}"
487ae52a3dSSasha Levin	fi
497ae52a3dSSasha Levin
507ae52a3dSSasha Levin	# Get all possible matching commit IDs
517ae52a3dSSasha Levin	local matches
527ae52a3dSSasha Levin	readarray -t matches < <(git rev-parse --disambiguate="$commit_id" 2>/dev/null)
537ae52a3dSSasha Levin
547ae52a3dSSasha Levin	# Return immediately if we have exactly one match
557ae52a3dSSasha Levin	if [ ${#matches[@]} -eq 1 ]; then
567ae52a3dSSasha Levin		echo "${matches[0]}"
577ae52a3dSSasha Levin		return 0
587ae52a3dSSasha Levin	fi
597ae52a3dSSasha Levin
607ae52a3dSSasha Levin	# If no matches and not in force mode, return failure
617ae52a3dSSasha Levin	if [ ${#matches[@]} -eq 0 ] && [ $force -eq 0 ]; then
627ae52a3dSSasha Levin		return 1
637ae52a3dSSasha Levin	fi
647ae52a3dSSasha Levin
657ae52a3dSSasha Levin	# If we have a subject, try to find a match with that subject
667ae52a3dSSasha Levin	if [ -n "$subject" ]; then
677ae52a3dSSasha Levin		# Convert subject with possible ellipsis to grep pattern
687ae52a3dSSasha Levin		local grep_pattern
697ae52a3dSSasha Levin		grep_pattern=$(convert_to_grep_pattern "$subject")
707ae52a3dSSasha Levin
717ae52a3dSSasha Levin		# In force mode with no ID matches, use git log --grep directly
727ae52a3dSSasha Levin		if [ ${#matches[@]} -eq 0 ] && [ $force -eq 1 ]; then
737ae52a3dSSasha Levin			# Use git log to search, but filter to ensure subject matches exactly
747ae52a3dSSasha Levin			local match
757ae52a3dSSasha Levin			match=$(git log --format="%H %s" --grep="$grep_pattern" --perl-regexp -10 | \
767ae52a3dSSasha Levin					while read -r hash subject; do
777ae52a3dSSasha Levin						if echo "$subject" | grep -qP "$grep_pattern"; then
787ae52a3dSSasha Levin							echo "$hash"
797ae52a3dSSasha Levin							break
807ae52a3dSSasha Levin						fi
817ae52a3dSSasha Levin					done)
827ae52a3dSSasha Levin			if [ -n "$match" ]; then
837ae52a3dSSasha Levin				echo "$match"
847ae52a3dSSasha Levin				return 0
857ae52a3dSSasha Levin			fi
867ae52a3dSSasha Levin		else
877ae52a3dSSasha Levin			# Normal subject matching for existing matches
887ae52a3dSSasha Levin			for match in "${matches[@]}"; do
897ae52a3dSSasha Levin				if git log -1 --format="%s" "$match" | grep -qP "$grep_pattern"; then
907ae52a3dSSasha Levin					echo "$match"
917ae52a3dSSasha Levin					return 0
927ae52a3dSSasha Levin				fi
937ae52a3dSSasha Levin			done
947ae52a3dSSasha Levin		fi
957ae52a3dSSasha Levin	fi
967ae52a3dSSasha Levin
977ae52a3dSSasha Levin	# No match found
987ae52a3dSSasha Levin	return 1
997ae52a3dSSasha Levin}
1007ae52a3dSSasha Levin
1017ae52a3dSSasha Levinrun_selftest() {
1027ae52a3dSSasha Levin	local test_cases=(
1037ae52a3dSSasha Levin		'00250b5 ("MAINTAINERS: add new Rockchip SoC list")'
1047ae52a3dSSasha Levin		'0037727 ("KVM: selftests: Convert xen_shinfo_test away from VCPU_ID")'
1057ae52a3dSSasha Levin		'ffef737 ("net/tls: Fix skb memory leak when running kTLS traffic")'
1067ae52a3dSSasha Levin		'd3d7 ("cifs: Improve guard for excluding $LXDEV xattr")'
1077ae52a3dSSasha Levin		'dbef ("Rename .data.once to .data..once to fix resetting WARN*_ONCE")'
1087ae52a3dSSasha Levin		'12345678'  # Non-existent commit
1097ae52a3dSSasha Levin		'12345 ("I'\''m a dummy commit")'  # Valid prefix but wrong subject
1107ae52a3dSSasha Levin		'--force 99999999 ("net/tls: Fix skb memory leak when running kTLS traffic")'  # Force mode with non-existent ID but valid subject
1117ae52a3dSSasha Levin		'83be ("firmware: ... auto-update: fix poll_complete() ... errors")'  # Wildcard test
1127ae52a3dSSasha Levin		'--force 999999999999 ("firmware: ... auto-update: fix poll_complete() ... errors")'  # Force mode wildcard test
1137ae52a3dSSasha Levin	)
1147ae52a3dSSasha Levin
1157ae52a3dSSasha Levin	local expected=(
1167ae52a3dSSasha Levin		"00250b529313d6262bb0ebbd6bdf0a88c809f6f0"
1177ae52a3dSSasha Levin		"0037727b3989c3fe1929c89a9a1dfe289ad86f58"
1187ae52a3dSSasha Levin		"ffef737fd0372ca462b5be3e7a592a8929a82752"
1197ae52a3dSSasha Levin		"d3d797e326533794c3f707ce1761da7a8895458c"
1207ae52a3dSSasha Levin		"dbefa1f31a91670c9e7dac9b559625336206466f"
1217ae52a3dSSasha Levin		""  # Expect empty output for non-existent commit
1227ae52a3dSSasha Levin		""  # Expect empty output for wrong subject
1237ae52a3dSSasha Levin		"ffef737fd0372ca462b5be3e7a592a8929a82752"  # Should find commit by subject in force mode
1247ae52a3dSSasha Levin		"83beece5aff75879bdfc6df8ba84ea88fd93050e"  # Wildcard test
1257ae52a3dSSasha Levin		"83beece5aff75879bdfc6df8ba84ea88fd93050e"  # Force mode wildcard test
1267ae52a3dSSasha Levin	)
1277ae52a3dSSasha Levin
1287ae52a3dSSasha Levin	local expected_exit_codes=(
1297ae52a3dSSasha Levin		0
1307ae52a3dSSasha Levin		0
1317ae52a3dSSasha Levin		0
1327ae52a3dSSasha Levin		0
1337ae52a3dSSasha Levin		0
1347ae52a3dSSasha Levin		1  # Expect failure for non-existent commit
1357ae52a3dSSasha Levin		1  # Expect failure for wrong subject
1367ae52a3dSSasha Levin		0  # Should succeed in force mode
1377ae52a3dSSasha Levin		0  # Should succeed with wildcard
1387ae52a3dSSasha Levin		0  # Should succeed with force mode and wildcard
1397ae52a3dSSasha Levin	)
1407ae52a3dSSasha Levin
1417ae52a3dSSasha Levin	local failed=0
1427ae52a3dSSasha Levin
1437ae52a3dSSasha Levin	echo "Running self-tests..."
1447ae52a3dSSasha Levin	for i in "${!test_cases[@]}"; do
1457ae52a3dSSasha Levin		# Capture both output and exit code
1467ae52a3dSSasha Levin		local result
1477ae52a3dSSasha Levin		result=$(git_resolve_commit ${test_cases[$i]})  # Removed quotes to allow --force to be parsed
1487ae52a3dSSasha Levin		local exit_code=$?
1497ae52a3dSSasha Levin
1507ae52a3dSSasha Levin		# Check both output and exit code
1517ae52a3dSSasha Levin		if [ "$result" != "${expected[$i]}" ] || [ $exit_code != ${expected_exit_codes[$i]} ]; then
1527ae52a3dSSasha Levin			echo "Test case $((i+1)) FAILED"
1537ae52a3dSSasha Levin			echo "Input: ${test_cases[$i]}"
1547ae52a3dSSasha Levin			echo "Expected output: '${expected[$i]}'"
1557ae52a3dSSasha Levin			echo "Got output: '$result'"
1567ae52a3dSSasha Levin			echo "Expected exit code: ${expected_exit_codes[$i]}"
1577ae52a3dSSasha Levin			echo "Got exit code: $exit_code"
1587ae52a3dSSasha Levin			failed=1
1597ae52a3dSSasha Levin		else
1607ae52a3dSSasha Levin			echo "Test case $((i+1)) PASSED"
1617ae52a3dSSasha Levin		fi
1627ae52a3dSSasha Levin	done
1637ae52a3dSSasha Levin
1647ae52a3dSSasha Levin	if [ $failed -eq 0 ]; then
1657ae52a3dSSasha Levin		echo "All tests passed!"
1667ae52a3dSSasha Levin		exit 0
1677ae52a3dSSasha Levin	else
1687ae52a3dSSasha Levin		echo "Some tests failed!"
1697ae52a3dSSasha Levin		exit 1
1707ae52a3dSSasha Levin	fi
1717ae52a3dSSasha Levin}
1727ae52a3dSSasha Levin
1737ae52a3dSSasha Levin# Check for selftest
1747ae52a3dSSasha Levinif [ "$1" = "--selftest" ]; then
1757ae52a3dSSasha Levin	run_selftest
1767ae52a3dSSasha Levin	exit $?
1777ae52a3dSSasha Levinfi
1787ae52a3dSSasha Levin
1797ae52a3dSSasha Levin# Handle --force flag
1807ae52a3dSSasha Levinforce=""
1817ae52a3dSSasha Levinif [ "$1" = "--force" ]; then
1827ae52a3dSSasha Levin	force="--force"
1837ae52a3dSSasha Levin	shift
1847ae52a3dSSasha Levinfi
1857ae52a3dSSasha Levin
1867ae52a3dSSasha Levin# Verify arguments
1877ae52a3dSSasha Levinif [ $# -eq 0 ]; then
1887ae52a3dSSasha Levin	usage
1897ae52a3dSSasha Levinfi
1907ae52a3dSSasha Levin
1917ae52a3dSSasha Levin# Skip validation in force mode
1927ae52a3dSSasha Levinif [ -z "$force" ]; then
1937ae52a3dSSasha Levin	# Validate that the first argument matches at least one git commit
1947ae52a3dSSasha Levin	if [ "$(git rev-parse --disambiguate="$1" 2>/dev/null | wc -l)" -eq 0 ]; then
1957ae52a3dSSasha Levin		echo "Error: '$1' does not match any git commit"
1967ae52a3dSSasha Levin		exit 1
1977ae52a3dSSasha Levin	fi
1987ae52a3dSSasha Levinfi
1997ae52a3dSSasha Levin
2007ae52a3dSSasha Levingit_resolve_commit $force "$@"
2017ae52a3dSSasha Levinexit $?
202