1#!/usr/bin/env 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} [...] [-- file*]" 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 if [ "$1" = "--" ]; then 167 shift 168 break 169 fi 170 branch="${1##*..}" 171 if [ "$branch" == "$1" ]; then 172 base="" 173 else 174 base="${1%%..*}" 175 fi 176 BASES[${#BRANCHES[@]}]="$base" 177 BRANCHES[${#BRANCHES[@]}]="$branch" 178 shift 179done 180 181# args left for git-log 182ARGS=( "$@" ) 183 184if [ ${#BRANCHES[@]} = 0 ]; then 185 die "$USAGE" 186fi 187 188for branch in "$REF" "${BRANCHES[@]}"; do 189 if ! git rev-parse --verify -q "$branch" >/dev/null; then 190 die "Failed to check git branch $branch." 191 fi 192done 193 194if [ -z "$BASE" ]; then 195 err "Warning! No base specified, looking for common ancestor." 196 BASE=$(git merge-base --all "$REF" "${BRANCHES[@]}") 197 if [ -z "$BASE" ]; then 198 die "Couldn't find a common ancestor between these branches" 199 fi 200fi 201 202# we want to go to the git root dir 203DIR="$PWD" 204cd $(git rev-parse --show-toplevel) 205 206mkdir -p .git/.show-backports #|| die "Can't create .git/.show-backports" 207WORK=.git/.show-backports 208 209rm -f "$WORK/${REF//\//_}" 210git log --reverse ${LOGEXPR:+--grep $LOGEXPR} --pretty="%H %s" "$BASE".."$REF" -- "${ARGS[@]}" | grep "${SUBJECT}" > "$WORK/${REF//\//_}" 211 212# for each branch, enumerate all commits and their ancestry 213 214branch_num=0; 215while [ $branch_num -lt "${#BRANCHES[@]}" ]; do 216 branch="${BRANCHES[$branch_num]}" 217 base="${BASES[$branch_num]}" 218 base="${base:-$BASE}" 219 rm -f "$WORK/${branch//\//_}" 220 git log --reverse --pretty="%H %s" "$base".."$branch" -- "${ARGS[@]}" | grep "${SUBJECT}" | while read h subject; do 221 echo -n "$h" $(git log -1 --pretty --format=%B "$h" | \ 222 sed -n 's/^commit \([^)]*\) upstream\.$/\1/p;s/^(cherry picked from commit \([^)]*\))/\1/p') 223 if [ -n "$BODYHASH" ]; then 224 echo " H$(git log -1 --pretty="%an|%ae|%at|%B" "$h" | sed -n '/^\(Signed-off-by\|(cherry picked\)/q;p' | md5sum)" 225 else 226 echo 227 fi 228 done > "$WORK/${branch//\//_}" 229 (( branch_num++ )) 230done 231 232count=0 233dump_commit_matrix | column -t | \ 234( 235 left_commits=( ) 236 right_commits=( ) 237 while read line; do 238 # append the subject at the end of the line 239 set -- $line 240 echo -n "$line " 241 if [ "${line::1}" = ":" ]; then 242 echo "---- Subject ----" 243 else 244 # doing it this way prevents git from abusing the terminal 245 echo "$(git log -1 --pretty="%s" "$1")" 246 left_commits[${#left_commits[@]}]="$1" 247 comm="" 248 while [ -n "$1" -a "$1" != "-" -a "$1" != "|" ]; do 249 comm="${1%-*}" 250 shift 251 done 252 right_commits[${#right_commits[@]}]="$comm" 253 fi 254 done 255 if [ -n "$MISSING" -a ${#left_commits[@]} -eq 0 ]; then 256 echo "No missing commit to apply." 257 elif [ -n "$MISSING" ]; then 258 echo 259 echo 260 echo "In order to show and/or apply all leftmost commits to current branch :" 261 echo " git show --pretty=format:'%C(yellow)commit %H%C(normal)%nAuthor: %an <%ae>%nDate: %aD%n%n%C(green)%C(bold)git cherry-pick -sx %h%n%n%w(72,4,4)%B%N' ${left_commits[@]}" 262 echo 263 echo " git cherry-pick -sx ${left_commits[@]}" 264 echo 265 if [ "${left_commits[*]}" != "${right_commits[*]}" ]; then 266 echo "In order to show and/or apply all rightmost commits to current branch :" 267 echo " git show --pretty=format:'%C(yellow)commit %H%C(normal)%nAuthor: %an <%ae>%nDate: %aD%n%n%C(green)%C(bold)git cherry-pick -sx %h%n%n%w(72,4,4)%B%N' ${right_commits[@]}" 268 echo 269 echo " git cherry-pick -sx ${right_commits[@]}" 270 echo 271 fi 272 fi 273) 274