1#!/bin/bash
2#
3# Compares multiple branches against a reference and shows which ones contain
4# each commit, and the level of backports since the origin or its own ancestors.
5#
6# Copyright (c) 2016 Willy Tarreau <w@1wt.eu>
7#
8# The purpose is to make it easy to visualize what backports might be missing
9# in a maintenance branch, and to easily spot the ones that are needed and the
10# ones that are not. It solely relies on the "cherry-picked from" tags in the
11# commit messages to find what commit is available where, and can even find a
12# reference commit's ancestor in another branch's commit ancestors as well to
13# detect that the patch is present. When done with the proper references and
14# a correct ordering of the branches, it can be used to quickly apply a set of
15# fixes to a branch since it dumps suggested commands at the end. When doing
16# so it is a good idea to use "HEAD" as the last branch to avoid doing mistakes.
17#
18# Examples :
19# - find what's in master and not in current branch :
20#   show-backports -q -m -r master HEAD
21# - find what's in 1.6/master and in hapee-maint-1.5r2 but not in current branch :
22#   show-backports -q -m -r 1.6/master hapee-maint-1.5r2 HEAD | grep ' [a-f0-9]\{8\}[-+][0-9] '
23# - check that no recent fix from master is missing in any maintenance branch :
24#   show-backports -r master hapee-maint-1.5r2 aloha-7.5 hapee-maint-1.5r1 aloha-7.0
25# - see what was recently merged into 1.6 and has no equivalent in local master :
26#   show-backports -q -m -r 1.6/master -b "1.6/master@{1 week ago}" master
27# - check what extra backports are present in hapee-r2 compared to hapee-r1 :
28#   show-backports -q -m -r hapee-r2 hapee-r1
29
30
31USAGE="Usage: ${0##*/} [-q] [-H] [-m] [-u] [-r reference] [-l logexpr] [-s subject] [-b base] {branch|range} [...]"
32BASES=( )
33BRANCHES=( )
34REF=master
35BASE=
36QUIET=
37LOGEXPR=
38SUBJECT=
39MISSING=
40UPSTREAM=
41BODYHASH=
42
43die() {
44	[ "$#" -eq 0 ] || echo "$*" >&2
45	exit 1
46}
47
48err() {
49	echo "$*" >&2
50}
51
52quit() {
53	[ "$#" -eq 0 ] || echo "$*"
54	exit 0
55}
56
57short() {
58	# git rev-parse --short $1
59	echo "${1::8}"
60}
61
62dump_commit_matrix() {
63	title=":$REF:"
64	for branch in "${BRANCHES[@]}"; do
65		#echo -n " $branch"
66		title="$title :${branch}:"
67	done
68	title="$title |"
69
70	count=0
71	# now look up commits
72	while read ref subject; do
73		if [ -n "$MISSING" -a "${subject:0:9}" = "[RELEASE]" ]; then
74			continue
75		fi
76
77		upstream="none"
78		missing=0
79		refbhash=""
80		line=""
81		for branch in "${BRANCHES[@]}"; do
82			set -- $(grep -m 1 $ref "$WORK/${branch//\//_}")
83			newhash=$1 ; shift
84			bhash=""
85			# count the number of cherry-picks after this one. Since we shift,
86			# the result is in "$#"
87			while [ -n "$1" -a "$1" != "$ref" ]; do
88				shift
89			done
90			if [ -n "$newhash" ]; then
91				line="${line} $(short $newhash)-$#"
92			else
93				# before giving up we can check if our current commit was
94				# itself cherry-picked and check this again. In order not
95				# to have to do it all the time, we can cache the result
96				# for the current line. If a match is found we report it
97				# with the '+' delimiter instead of '-'.
98				if [ "$upstream" = "none" ]; then
99					upstream=( $(git log -1 --pretty --format=%B "$ref" | \
100						sed -n 's/^commit \([^)]*\) upstream\.$/\1/p;s/^(cherry picked from commit \([^)]*\))/\1/p') )
101				fi
102				newhash=""
103				for h in ${upstream[@]}; do
104					set -- $(grep -m 1 $h "$WORK/${branch//\//_}")
105					newhash=$1 ; shift
106					while [ -n "$1" -a "$1" != "$h" ]; do
107						shift
108					done
109					if [ -n "$newhash" ]; then
110						line="${line} $(short $newhash)+$#"
111						break
112					fi
113				done
114				if [ -z "$newhash" -a -n "$BODYHASH" ]; then
115					if [ -z "$refbhash" ]; then
116						refbhash=$(git log -1 --pretty="%an|%ae|%at|%B" "$ref" | sed -n '/^\(Signed-off-by\|(cherry picked\)/q;p' | md5sum)
117					fi
118
119
120					set -- $(grep -m 1 "H$refbhash\$" "$WORK/${branch//\//_}")
121					newhash=$1 ; shift
122					if [ -n "$newhash" ]; then
123						line="${line} $(short $newhash)+?"
124						break
125					fi
126				fi
127				if [ -z "$newhash" ]; then
128					line="${line} -"
129					missing=1
130				fi
131			fi
132		done
133		line="${line} |"
134		if [ -z "$MISSING" -o $missing -gt 0 ]; then
135			[ $((count++)) -gt 0 ] || echo "$title"
136			[ "$QUIET" != "" -o $count -lt 20 ] || count=0
137			if [ -z "$UPSTREAM" -o "$upstream" = "none" -o -z "$upstream" ]; then
138				echo "$(short $ref) $line"
139			else
140				echo "$(short $upstream) $line"
141			fi
142		fi
143	done < "$WORK/${REF//\//_}"
144}
145
146while [ -n "$1" -a -z "${1##-*}" ]; do
147	case "$1" in
148		-b)        BASE="$2"      ; shift 2 ;;
149		-r)        REF="$2"       ; shift 2 ;;
150		-l)        LOGEXPR="$2"   ; shift 2 ;;
151		-s)        SUBJECT="$2"   ; shift 2 ;;
152		-q)        QUIET=1        ; shift   ;;
153		-m)        MISSING=1      ; shift   ;;
154		-u)        UPSTREAM=1     ; shift   ;;
155		-H)        BODYHASH=1     ; shift   ;;
156		-h|--help) quit "$USAGE" ;;
157		*)         die  "$USAGE" ;;
158	esac
159done
160
161# branches may also appear as id1..id2 to limit the history instead of looking
162# back to the common base. The field is left empty if not set.
163BRANCHES=( )
164BASES=( )
165while [ $# -gt 0 ]; do
166	branch="${1##*..}"
167	if [ "$branch" == "$1" ]; then
168		base=""
169	else
170		base="${1%%..*}"
171	fi
172	BASES[${#BRANCHES[@]}]="$base"
173	BRANCHES[${#BRANCHES[@]}]="$branch"
174	shift
175done
176
177if [ ${#BRANCHES[@]} = 0 ]; then
178	die "$USAGE"
179fi
180
181for branch in "$REF" "${BRANCHES[@]}"; do
182	if ! git rev-parse --verify -q "$branch" >/dev/null; then
183		die "Failed to check git branch $branch."
184	fi
185done
186
187if [ -z "$BASE" ]; then
188	err "Warning! No base specified, looking for common ancestor."
189	BASE=$(git merge-base --all "$REF" "${BRANCHES[@]}")
190	if [ -z "$BASE" ]; then
191		die "Couldn't find a common ancestor between these branches"
192	fi
193fi
194
195# we want to go to the git root dir
196DIR="$PWD"
197cd $(git rev-parse --show-toplevel)
198
199mkdir -p .git/.show-backports #|| die "Can't create .git/.show-backports"
200WORK=.git/.show-backports
201
202rm -f "$WORK/${REF//\//_}"
203git log --reverse ${LOGEXPR:+--grep $LOGEXPR} --pretty="%H %s" "$BASE".."$REF" | grep "${SUBJECT}" > "$WORK/${REF//\//_}"
204
205# for each branch, enumerate all commits and their ancestry
206
207branch_num=0;
208while [ $branch_num -lt "${#BRANCHES[@]}" ]; do
209	branch="${BRANCHES[$branch_num]}"
210	base="${BASES[$branch_num]}"
211	base="${base:-$BASE}"
212	rm -f "$WORK/${branch//\//_}"
213	git log --reverse --pretty="%H %s" "$base".."$branch" | grep "${SUBJECT}" | while read h subject; do
214		echo -n "$h" $(git log -1 --pretty --format=%B "$h" | \
215			sed -n 's/^commit \([^)]*\) upstream\.$/\1/p;s/^(cherry picked from commit \([^)]*\))/\1/p')
216		if [ -n "$BODYHASH" ]; then
217			echo " H$(git log -1 --pretty="%an|%ae|%at|%B" "$h" | sed -n '/^\(Signed-off-by\|(cherry picked\)/q;p' | md5sum)"
218		else
219			echo
220		fi
221	done > "$WORK/${branch//\//_}"
222	(( branch_num++ ))
223done
224
225count=0
226dump_commit_matrix | column -t | \
227(
228	left_commits=( )
229	right_commits=( )
230	while read line; do
231		# append the subject at the end of the line
232		set -- $line
233		echo -n "$line "
234		if [ "${line::1}" = ":" ]; then
235			echo "---- Subject ----"
236		else
237			# doing it this way prevents git from abusing the terminal
238			echo "$(git log -1 --pretty="%s" "$1")"
239			left_commits[${#left_commits[@]}]="$1"
240			comm=""
241			while [ -n "$1" -a "$1" != "-" -a "$1" != "|" ]; do
242				comm="${1%-*}"
243				shift
244			done
245			right_commits[${#right_commits[@]}]="$comm"
246		fi
247	done
248	if [ -n "$MISSING" -a ${#left_commits[@]} -eq 0 ]; then
249		echo "No missing commit to apply."
250	elif [ -n "$MISSING" ]; then
251		echo
252		echo "In order to show and/or apply all leftmost commits to current branch :"
253		echo "   git show ${left_commits[@]}"
254		echo "   git cherry-pick -sx ${left_commits[@]}"
255		echo
256		echo "In order to show and/or apply all rightmost commits to current branch :"
257		echo "   git show ${right_commits[@]}"
258		echo "   git cherry-pick -sx ${right_commits[@]}"
259	fi
260)
261