blob: 0f4382bec4c22d2314ff910af30fc86c3032bdc7 [file] [log] [blame]
Willy Tarreau62b71ee2016-05-10 12:04:13 +02001#!/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] [-m] [-r reference] [-l logexpr] [-s subject] [-b base] branch [...]"
32BRANCHES=( )
33REF=master
34BASE=
35QUIET=
36LOGEXPR=
37SUBJECT=
38MISSING=
39
40die() {
41 [ "$#" -eq 0 ] || echo "$*" >&2
42 exit 1
43}
44
45err() {
46 echo "$*" >&2
47}
48
49quit() {
50 [ "$#" -eq 0 ] || echo "$*"
51 exit 0
52}
53
54short() {
55 # git rev-parse --short $1
56 echo "${1::8}"
57}
58
59dump_commit_matrix() {
60 title=":$REF:"
61 for branch in "${BRANCHES[@]}"; do
62 #echo -n " $branch"
63 title="$title :${branch}:"
64 done
65 title="$title |"
66
67 count=0
68 # now look up commits
69 while read ref subject; do
70 if [ -n "$MISSING" -a "${subject:0:9}" = "[RELEASE]" ]; then
71 continue
72 fi
73
74 upstream="none"
75 missing=0
76 line="$(short $ref)"
77 for branch in "${BRANCHES[@]}"; do
78 set -- $(grep -m 1 $ref "$WORK/${branch//\//_}")
79 newhash=$1 ; shift
80 # count the number of cherry-picks after this one. Since we shift,
81 # the result is in "$#"
82 while [ -n "$1" -a "$1" != "$ref" ]; do
83 shift
84 done
85 if [ -n "$newhash" ]; then
86 line="${line} $(short $newhash)-$#"
87 else
88 # before giving up we can check if our current commit was
89 # itself cherry-picked and check this again. In order not
90 # to have to do it all the time, we can cache the result
91 # for the current line. If a match is found we report it
92 # with the '+' delimiter instead of '-'.
93 if [ "$upstream" = "none" ]; then
94 upstream=( $(git log -1 --pretty --format=%B "$ref" | \
95 sed -n 's/^commit \([^)]*\) upstream\.$/\1/p;s/^(cherry picked from commit \([^)]*\))/\1/p') )
96 fi
97 newhash=""
98 for h in ${upstream[@]}; do
99 set -- $(grep -m 1 $h "$WORK/${branch//\//_}")
100 newhash=$1 ; shift
101 while [ -n "$1" -a "$1" != "$h" ]; do
102 shift
103 done
104 if [ -n "$newhash" ]; then
105 line="${line} $(short $newhash)+$#"
106 break
107 fi
108 done
109 if [ -z "$newhash" ]; then
110 line="${line} -"
111 missing=1
112 fi
113 fi
114 done
115 line="${line} |"
116 if [ -z "$MISSING" -o $missing -gt 0 ]; then
117 [ $((count++)) -gt 0 ] || echo $title
118 [ "$QUIET" != "" -o $count -lt 20 ] || count=0
119 echo "$line"
120 fi
121 done < "$WORK/${REF//\//_}"
122}
123
124while [ -n "$1" -a -z "${1##-*}" ]; do
125 case "$1" in
126 -b) BASE="$2" ; shift 2 ;;
127 -r) REF="$2" ; shift 2 ;;
128 -l) LOGEXPR="$2" ; shift 2 ;;
129 -s) SUBJECT="$2" ; shift 2 ;;
130 -q) QUIET=1 ; shift ;;
131 -m) MISSING=1 ; shift ;;
132 -h|--help) quit "$USAGE" ;;
133 *) die "$USAGE" ;;
134 esac
135done
136
137BRANCHES=( "$@" )
138if [ ${#BRANCHES[@]} = 0 ]; then
139 die "$USAGE"
140fi
141
142for branch in "$REF" "${BRANCHES[@]}"; do
143 if ! git rev-parse --verify -q "$branch" >/dev/null; then
144 die "Failed to check git branch $branch."
145 fi
146done
147
148if [ -z "$BASE" ]; then
149 err "Warning! No base specified, looking for common ancestor."
150 BASE=$(git merge-base --all "$REF" "${BRANCHES[@]}")
151 if [ -z "$BASE" ]; then
152 die "Couldn't find a common ancestor between these branches"
153 fi
154fi
155
156# we want to go to the git root dir
157DIR="$PWD"
158cd $(git rev-parse --show-toplevel)
159
160mkdir -p .git/.show-backports #|| die "Can't create .git/.show-backports"
161WORK=.git/.show-backports
162
163rm -f "$WORK/${REF//\//_}"
164git log --reverse ${LOGEXPR:+--grep $LOGEXPR} --pretty="%H %s" "$BASE".."$REF" | grep "${SUBJECT}" > "$WORK/${branch//\//_}" > "$WORK/${REF//\//_}"
165
166# for each branch, enumerate all commits and their ancestry
167for branch in "${BRANCHES[@]}"; do
168 rm -f "$WORK/${branch//\//_}"
169 git log --reverse --pretty="%H %s" "$BASE".."$branch" | grep "${SUBJECT}" | while read h subject; do
170 echo "$h" $(git log -1 --pretty --format=%B "$h" | \
171 sed -n 's/^commit \([^)]*\) upstream\.$/\1/p;s/^(cherry picked from commit \([^)]*\))/\1/p')
172 done > "$WORK/${branch//\//_}"
173done
174
175count=0
176dump_commit_matrix | column -t | \
177(
178 left_commits=( )
179 right_commits=( )
180 while read line; do
181 # append the subject at the end of the line
182 set -- $line
183 echo -n "$line "
184 if [ "${line::1}" = ":" ]; then
185 echo "---- Subject ----"
186 else
187 # doing it this way prevents git from abusing the terminal
188 echo $(git log -1 --pretty="%s" "$1")
189 left_commits[${#left_commits[@]}]="$1"
190 comm=""
191 while [ -n "$1" -a "$1" != "-" -a "$1" != "|" ]; do
192 comm="${1%-*}"
193 shift
194 done
195 right_commits[${#right_commits[@]}]="$comm"
196 fi
197 done
198 if [ -n "$MISSING" -a ${#left_commits[@]} -eq 0 ]; then
199 echo "No missing commit to apply."
200 elif [ -n "$MISSING" ]; then
201 echo
202 echo "In order to apply all leftmost commits to current branch :"
203 echo " git cherry-pick -x ${left_commits[@]}"
204 echo
205 echo "In order to apply all rightmost commits to current branch :"
206 echo " git cherry-pick -x ${right_commits[@]}"
207 fi
208)