summaryrefslogtreecommitdiff
path: root/scripts/git-resolve.sh
blob: e9b5940c0f2837895ffd88b0f540031c538d5436 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# (c) 2025, Sasha Levin <sashal@kernel.org>

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 $?