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