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