xref: /linux/scripts/git-resolve.sh (revision 7ae52a3d7f511c95dad414441db7cfdd6356d88a)
1*7ae52a3dSSasha Levin#!/bin/bash
2*7ae52a3dSSasha Levin
3*7ae52a3dSSasha Levinusage() {
4*7ae52a3dSSasha Levin	echo "Usage: $(basename "$0") [--selftest] [--force] <commit-id> [commit-subject]"
5*7ae52a3dSSasha Levin	echo "Resolves a short git commit ID to its full SHA-1 hash, particularly useful for fixing references in commit messages."
6*7ae52a3dSSasha Levin	echo ""
7*7ae52a3dSSasha Levin	echo "Arguments:"
8*7ae52a3dSSasha Levin	echo "  --selftest      Run self-tests"
9*7ae52a3dSSasha Levin	echo "  --force         Try to find commit by subject if ID lookup fails"
10*7ae52a3dSSasha Levin	echo "  commit-id       Short git commit ID to resolve"
11*7ae52a3dSSasha Levin	echo "  commit-subject  Optional commit subject to help resolve between multiple matches"
12*7ae52a3dSSasha Levin	exit 1
13*7ae52a3dSSasha Levin}
14*7ae52a3dSSasha Levin
15*7ae52a3dSSasha Levin# Convert subject with ellipsis to grep pattern
16*7ae52a3dSSasha Levinconvert_to_grep_pattern() {
17*7ae52a3dSSasha Levin	local subject="$1"
18*7ae52a3dSSasha Levin	# First escape ALL regex special characters
19*7ae52a3dSSasha Levin	local escaped_subject
20*7ae52a3dSSasha Levin	escaped_subject=$(printf '%s\n' "$subject" | sed 's/[[\.*^$()+?{}|]/\\&/g')
21*7ae52a3dSSasha Levin	# Also escape colons, parentheses, and hyphens as they are special in our context
22*7ae52a3dSSasha Levin	escaped_subject=$(echo "$escaped_subject" | sed 's/[:-]/\\&/g')
23*7ae52a3dSSasha Levin	# Then convert escaped ... sequence to .*?
24*7ae52a3dSSasha Levin	escaped_subject=$(echo "$escaped_subject" | sed 's/\\\.\\\.\\\./.*?/g')
25*7ae52a3dSSasha Levin	echo "^${escaped_subject}$"
26*7ae52a3dSSasha Levin}
27*7ae52a3dSSasha Levin
28*7ae52a3dSSasha Levingit_resolve_commit() {
29*7ae52a3dSSasha Levin	local force=0
30*7ae52a3dSSasha Levin	if [ "$1" = "--force" ]; then
31*7ae52a3dSSasha Levin		force=1
32*7ae52a3dSSasha Levin		shift
33*7ae52a3dSSasha Levin	fi
34*7ae52a3dSSasha Levin
35*7ae52a3dSSasha Levin	# Split input into commit ID and subject
36*7ae52a3dSSasha Levin	local input="$*"
37*7ae52a3dSSasha Levin	local commit_id="${input%% *}"
38*7ae52a3dSSasha Levin	local subject=""
39*7ae52a3dSSasha Levin
40*7ae52a3dSSasha Levin	# Extract subject if present (everything after the first space)
41*7ae52a3dSSasha Levin	if [[ "$input" == *" "* ]]; then
42*7ae52a3dSSasha Levin		subject="${input#* }"
43*7ae52a3dSSasha Levin		# Strip the ("...") quotes if present
44*7ae52a3dSSasha Levin		subject="${subject#*(\"}"
45*7ae52a3dSSasha Levin		subject="${subject%\")*}"
46*7ae52a3dSSasha Levin	fi
47*7ae52a3dSSasha Levin
48*7ae52a3dSSasha Levin	# Get all possible matching commit IDs
49*7ae52a3dSSasha Levin	local matches
50*7ae52a3dSSasha Levin	readarray -t matches < <(git rev-parse --disambiguate="$commit_id" 2>/dev/null)
51*7ae52a3dSSasha Levin
52*7ae52a3dSSasha Levin	# Return immediately if we have exactly one match
53*7ae52a3dSSasha Levin	if [ ${#matches[@]} -eq 1 ]; then
54*7ae52a3dSSasha Levin		echo "${matches[0]}"
55*7ae52a3dSSasha Levin		return 0
56*7ae52a3dSSasha Levin	fi
57*7ae52a3dSSasha Levin
58*7ae52a3dSSasha Levin	# If no matches and not in force mode, return failure
59*7ae52a3dSSasha Levin	if [ ${#matches[@]} -eq 0 ] && [ $force -eq 0 ]; then
60*7ae52a3dSSasha Levin		return 1
61*7ae52a3dSSasha Levin	fi
62*7ae52a3dSSasha Levin
63*7ae52a3dSSasha Levin	# If we have a subject, try to find a match with that subject
64*7ae52a3dSSasha Levin	if [ -n "$subject" ]; then
65*7ae52a3dSSasha Levin		# Convert subject with possible ellipsis to grep pattern
66*7ae52a3dSSasha Levin		local grep_pattern
67*7ae52a3dSSasha Levin		grep_pattern=$(convert_to_grep_pattern "$subject")
68*7ae52a3dSSasha Levin
69*7ae52a3dSSasha Levin		# In force mode with no ID matches, use git log --grep directly
70*7ae52a3dSSasha Levin		if [ ${#matches[@]} -eq 0 ] && [ $force -eq 1 ]; then
71*7ae52a3dSSasha Levin			# Use git log to search, but filter to ensure subject matches exactly
72*7ae52a3dSSasha Levin			local match
73*7ae52a3dSSasha Levin			match=$(git log --format="%H %s" --grep="$grep_pattern" --perl-regexp -10 | \
74*7ae52a3dSSasha Levin					while read -r hash subject; do
75*7ae52a3dSSasha Levin						if echo "$subject" | grep -qP "$grep_pattern"; then
76*7ae52a3dSSasha Levin							echo "$hash"
77*7ae52a3dSSasha Levin							break
78*7ae52a3dSSasha Levin						fi
79*7ae52a3dSSasha Levin					done)
80*7ae52a3dSSasha Levin			if [ -n "$match" ]; then
81*7ae52a3dSSasha Levin				echo "$match"
82*7ae52a3dSSasha Levin				return 0
83*7ae52a3dSSasha Levin			fi
84*7ae52a3dSSasha Levin		else
85*7ae52a3dSSasha Levin			# Normal subject matching for existing matches
86*7ae52a3dSSasha Levin			for match in "${matches[@]}"; do
87*7ae52a3dSSasha Levin				if git log -1 --format="%s" "$match" | grep -qP "$grep_pattern"; then
88*7ae52a3dSSasha Levin					echo "$match"
89*7ae52a3dSSasha Levin					return 0
90*7ae52a3dSSasha Levin				fi
91*7ae52a3dSSasha Levin			done
92*7ae52a3dSSasha Levin		fi
93*7ae52a3dSSasha Levin	fi
94*7ae52a3dSSasha Levin
95*7ae52a3dSSasha Levin	# No match found
96*7ae52a3dSSasha Levin	return 1
97*7ae52a3dSSasha Levin}
98*7ae52a3dSSasha Levin
99*7ae52a3dSSasha Levinrun_selftest() {
100*7ae52a3dSSasha Levin	local test_cases=(
101*7ae52a3dSSasha Levin		'00250b5 ("MAINTAINERS: add new Rockchip SoC list")'
102*7ae52a3dSSasha Levin		'0037727 ("KVM: selftests: Convert xen_shinfo_test away from VCPU_ID")'
103*7ae52a3dSSasha Levin		'ffef737 ("net/tls: Fix skb memory leak when running kTLS traffic")'
104*7ae52a3dSSasha Levin		'd3d7 ("cifs: Improve guard for excluding $LXDEV xattr")'
105*7ae52a3dSSasha Levin		'dbef ("Rename .data.once to .data..once to fix resetting WARN*_ONCE")'
106*7ae52a3dSSasha Levin		'12345678'  # Non-existent commit
107*7ae52a3dSSasha Levin		'12345 ("I'\''m a dummy commit")'  # Valid prefix but wrong subject
108*7ae52a3dSSasha Levin		'--force 99999999 ("net/tls: Fix skb memory leak when running kTLS traffic")'  # Force mode with non-existent ID but valid subject
109*7ae52a3dSSasha Levin		'83be ("firmware: ... auto-update: fix poll_complete() ... errors")'  # Wildcard test
110*7ae52a3dSSasha Levin		'--force 999999999999 ("firmware: ... auto-update: fix poll_complete() ... errors")'  # Force mode wildcard test
111*7ae52a3dSSasha Levin	)
112*7ae52a3dSSasha Levin
113*7ae52a3dSSasha Levin	local expected=(
114*7ae52a3dSSasha Levin		"00250b529313d6262bb0ebbd6bdf0a88c809f6f0"
115*7ae52a3dSSasha Levin		"0037727b3989c3fe1929c89a9a1dfe289ad86f58"
116*7ae52a3dSSasha Levin		"ffef737fd0372ca462b5be3e7a592a8929a82752"
117*7ae52a3dSSasha Levin		"d3d797e326533794c3f707ce1761da7a8895458c"
118*7ae52a3dSSasha Levin		"dbefa1f31a91670c9e7dac9b559625336206466f"
119*7ae52a3dSSasha Levin		""  # Expect empty output for non-existent commit
120*7ae52a3dSSasha Levin		""  # Expect empty output for wrong subject
121*7ae52a3dSSasha Levin		"ffef737fd0372ca462b5be3e7a592a8929a82752"  # Should find commit by subject in force mode
122*7ae52a3dSSasha Levin		"83beece5aff75879bdfc6df8ba84ea88fd93050e"  # Wildcard test
123*7ae52a3dSSasha Levin		"83beece5aff75879bdfc6df8ba84ea88fd93050e"  # Force mode wildcard test
124*7ae52a3dSSasha Levin	)
125*7ae52a3dSSasha Levin
126*7ae52a3dSSasha Levin	local expected_exit_codes=(
127*7ae52a3dSSasha Levin		0
128*7ae52a3dSSasha Levin		0
129*7ae52a3dSSasha Levin		0
130*7ae52a3dSSasha Levin		0
131*7ae52a3dSSasha Levin		0
132*7ae52a3dSSasha Levin		1  # Expect failure for non-existent commit
133*7ae52a3dSSasha Levin		1  # Expect failure for wrong subject
134*7ae52a3dSSasha Levin		0  # Should succeed in force mode
135*7ae52a3dSSasha Levin		0  # Should succeed with wildcard
136*7ae52a3dSSasha Levin		0  # Should succeed with force mode and wildcard
137*7ae52a3dSSasha Levin	)
138*7ae52a3dSSasha Levin
139*7ae52a3dSSasha Levin	local failed=0
140*7ae52a3dSSasha Levin
141*7ae52a3dSSasha Levin	echo "Running self-tests..."
142*7ae52a3dSSasha Levin	for i in "${!test_cases[@]}"; do
143*7ae52a3dSSasha Levin		# Capture both output and exit code
144*7ae52a3dSSasha Levin		local result
145*7ae52a3dSSasha Levin		result=$(git_resolve_commit ${test_cases[$i]})  # Removed quotes to allow --force to be parsed
146*7ae52a3dSSasha Levin		local exit_code=$?
147*7ae52a3dSSasha Levin
148*7ae52a3dSSasha Levin		# Check both output and exit code
149*7ae52a3dSSasha Levin		if [ "$result" != "${expected[$i]}" ] || [ $exit_code != ${expected_exit_codes[$i]} ]; then
150*7ae52a3dSSasha Levin			echo "Test case $((i+1)) FAILED"
151*7ae52a3dSSasha Levin			echo "Input: ${test_cases[$i]}"
152*7ae52a3dSSasha Levin			echo "Expected output: '${expected[$i]}'"
153*7ae52a3dSSasha Levin			echo "Got output: '$result'"
154*7ae52a3dSSasha Levin			echo "Expected exit code: ${expected_exit_codes[$i]}"
155*7ae52a3dSSasha Levin			echo "Got exit code: $exit_code"
156*7ae52a3dSSasha Levin			failed=1
157*7ae52a3dSSasha Levin		else
158*7ae52a3dSSasha Levin			echo "Test case $((i+1)) PASSED"
159*7ae52a3dSSasha Levin		fi
160*7ae52a3dSSasha Levin	done
161*7ae52a3dSSasha Levin
162*7ae52a3dSSasha Levin	if [ $failed -eq 0 ]; then
163*7ae52a3dSSasha Levin		echo "All tests passed!"
164*7ae52a3dSSasha Levin		exit 0
165*7ae52a3dSSasha Levin	else
166*7ae52a3dSSasha Levin		echo "Some tests failed!"
167*7ae52a3dSSasha Levin		exit 1
168*7ae52a3dSSasha Levin	fi
169*7ae52a3dSSasha Levin}
170*7ae52a3dSSasha Levin
171*7ae52a3dSSasha Levin# Check for selftest
172*7ae52a3dSSasha Levinif [ "$1" = "--selftest" ]; then
173*7ae52a3dSSasha Levin	run_selftest
174*7ae52a3dSSasha Levin	exit $?
175*7ae52a3dSSasha Levinfi
176*7ae52a3dSSasha Levin
177*7ae52a3dSSasha Levin# Handle --force flag
178*7ae52a3dSSasha Levinforce=""
179*7ae52a3dSSasha Levinif [ "$1" = "--force" ]; then
180*7ae52a3dSSasha Levin	force="--force"
181*7ae52a3dSSasha Levin	shift
182*7ae52a3dSSasha Levinfi
183*7ae52a3dSSasha Levin
184*7ae52a3dSSasha Levin# Verify arguments
185*7ae52a3dSSasha Levinif [ $# -eq 0 ]; then
186*7ae52a3dSSasha Levin	usage
187*7ae52a3dSSasha Levinfi
188*7ae52a3dSSasha Levin
189*7ae52a3dSSasha Levin# Skip validation in force mode
190*7ae52a3dSSasha Levinif [ -z "$force" ]; then
191*7ae52a3dSSasha Levin	# Validate that the first argument matches at least one git commit
192*7ae52a3dSSasha Levin	if [ "$(git rev-parse --disambiguate="$1" 2>/dev/null | wc -l)" -eq 0 ]; then
193*7ae52a3dSSasha Levin		echo "Error: '$1' does not match any git commit"
194*7ae52a3dSSasha Levin		exit 1
195*7ae52a3dSSasha Levin	fi
196*7ae52a3dSSasha Levinfi
197*7ae52a3dSSasha Levin
198*7ae52a3dSSasha Levingit_resolve_commit $force "$@"
199*7ae52a3dSSasha Levinexit $?
200