pick-to-branch: Improve fix of behavior on failed cherry-pick
[tools.git] / review-tools / ghmerge
1 #! /bin/bash
2
3 function usage_exit {
4     >&2 echo "Usage: ghmerge <options including prnum and reviewer(s)>
5     or ghmerge [<options>] -- <prnum> <reviewer>...
6 Options may include addrev options and gitaddrev filter args.
7
8 Option style arguments:
9
10 --help              Print this help and exit
11 --tools             Merge a tools PR (rather than openssl PR)
12 --web               Merge a web PR (rather than openssl PR)
13 --remote <remote>   Repo to merge with (rather than git.openssl.org), usually 'upstream'
14 --target <branch>   Merge target (rather than current branch), usually 'master'
15 --ref <branch>      A synonym for --target
16 --cherry-pick [<n>] Cherry-pick the last n (1 <= n <= 99, default: 1) commits, rather than rebasing
17 --squash            Squash new commits non-interactively (allows editing msg)
18 --noautosquash      Do not automatically squash fixups in interactive rebase
19 --nobuild           Do not call 'openssbuild' before merging
20
21 Examples:
22
23   ghmerge 12345 mattcaswell
24   ghmerge 12345 paulidale t8m --nobuild --myemail=dev@ddvo.net
25   ghmerge edd05b7^^^^..19692bb2c32 --squash -- 12345 levitte
26   ghmerge 12345 slontis --target openssl-3.0
27   ghmerge --nobuild --target openssl-3.0 --cherry-pick 3 16051 paulidale"
28     exit 9
29 }
30
31 set -o errexit
32
33 WHAT=""
34 PICK=no
35 INTERACTIVE=yes
36 AUTOSQUASH="--autosquash"
37 REMOTE=""
38 TARGET=""
39 BUILD=yes
40 [ -z ${CC+x} ] && CC="ccache gcc" # opensslbuild will otherwise use "ccache clang-3.6"
41
42 if [ ! -d .git ] ; then
43     echo Not at a top-level git directory
44     exit 1
45 fi
46
47 PRNUM=
48 TEAM=""
49 ADDREVOPTS=""
50 # Parse JCL.
51 shopt -s extglob
52 while [ $# -ne 0 ]; do
53     case "$1" in
54     --help)
55         usage_exit
56         ;;
57     --tools)
58         WHAT=tools ; BUILD=no ; shift
59         ;;
60     --web)
61         WHAT=web ; BUILD=no ; shift
62         ;;
63     --cherry-pick)
64         shift;
65         PICK=1;
66         if [ "$1" != "" ] && [ $1 -ge 1 ] 2>/dev/null && [ $1 -le 99 ]; then
67             PICK=$1 ; shift
68         fi
69         ;;
70     --noautosquash)
71         AUTOSQUASH="" ; shift
72         ;;
73     --squash)
74         INTERACTIVE=no ; shift
75         ;;
76     --nobuild)
77         BUILD=no ; shift
78         ;;
79     --remote)
80         if [ $# -lt 2 ] ; then
81             echo "Missing argument of '$1'"
82             usage_exit
83         fi
84         shift; REMOTE=$1; shift
85         ;;
86     --target|--ref)
87         if [ $# -lt 2 ] ; then
88             echo "Missing argument of '$1'"
89             usage_exit
90         fi
91         shift; TARGET=$1; shift
92         ;;
93     --)
94         if [ $# -lt 3 ] ; then
95             echo "Missing <prnum> <reviewer>... after '--'"
96             usage_exit
97         fi
98         shift; PRNUM=$1 ; shift
99         TEAM="$TEAM $*"
100         break
101         ;;
102     -*) # e.g., --verbose, --trivial, --myemail=...
103         ADDREVOPTS="$ADDREVOPTS $1"
104         shift
105         ;;
106     +([[:digit:]]) ) # e.g., 1453
107         PRNUM=$1; shift
108         ;;
109     @*) # e.g., @t8m
110         TEAM="$TEAM $1"; shift
111         ;;
112     +([[:alnum:]-]) ) # e.g., levitte
113         if [[ $1 =~ ^[0-9a-f]{7,}+$ ]]; then # e.g., edd05b7
114             ADDREVOPTS="$ADDREVOPTS $1"
115         else
116             TEAM="$TEAM $1"
117         fi
118         shift
119         ;;
120     *) # e.g., edd05b7^^^^..19692bb2c32
121         ADDREVOPTS="$ADDREVOPTS $1"; shift
122         ;;
123     esac
124 done
125
126 if [ "$WHAT" = "" ] ; then
127     WHAT="openssl"
128 else
129     ADDREVOPTS="$ADDREVOPTS --$WHAT"
130 fi
131 ADDREVOPTS=${ADDREVOPTS# } # chop any leading ' '
132
133 [ "$REMOTE" = "" ] && REMOTE=`git remote -v | awk '/git.openssl.org.*(push)/{ print $1; }' | head -n 1` # usually this will be 'upstream'
134 if [ "$REMOTE" = "" ] ; then
135     echo Cannot find git remote with URL including 'git.openssl.org'
136     exit 1
137 fi
138
139 if [ "$PRNUM" = "" -o "$TEAM" = "" ] ; then
140     usage_exit
141 fi
142
143 PR_URL=https://api.github.com/repos/openssl/$WHAT/pulls/$PRNUM
144 if ! wget --quiet $PR_URL -O /tmp/gh$$; then
145     echo "Error getting $PR_URL"
146     exit 1
147 fi
148 set -- `python -c '
149 from __future__ import print_function
150 import json, sys;
151 input = json.load(sys.stdin)
152 print(str(input["head"]["label"]).replace(":", " "),
153       str(input["head"]["repo"]["ssh_url"]))'        </tmp/gh$$`
154 WHO=$1
155 BRANCH=$2
156 REPO=$3
157 rm /tmp/gh$$
158
159 if [ -z "$WHO" -o -z "$BRANCH" -o -z "$REPO" ]; then
160     echo "Could not determine from $PR_URL which branch of whom to fetch from where"
161     exit 1
162 fi
163
164 ORIG_REF=`git rev-parse --abbrev-ref HEAD` # usually this will be 'master'
165 STASH_OUT=`git stash`
166 WORK="copy-of-${WHO}-${BRANCH}"
167
168 (git branch | grep -q "$WORK") && (echo "Branch already exists: $WORK"; exit 1)
169
170 function cleanup {
171     rv=$?
172     echo # make sure to enter new line, needed, e.g., after Ctrl-C
173     [ $rv -ne 0 ] && echo -e "\nghmerge failed"
174     if [ "$REBASING" == 1 ] ; then
175         git rebase --abort 2>/dev/null || true
176     fi
177     if [ "$CHERRYPICKING" == 1 ] ; then
178         echo "Hint: maybe --cherry-pick was not given a suitable <n> parameter."
179         git cherry-pick --abort 2>/dev/null || true
180     fi
181     if [ "$ORIG_TARGET_HEAD" != "" ]; then
182         echo Restoring original commit HEAD of $TARGET
183         git reset --hard "$ORIG_TARGET_HEAD"
184     fi
185     if [ "$TARGET" != "$ORIG_REF" ] || [ "$WORK_USED" != "" ]; then
186         echo Returning to previous branch $ORIG_REF
187         git checkout -q $ORIG_REF
188     fi
189     if [ "$WORK_USED" != "" ]; then
190         git branch -qD $WORK_USED
191     fi
192     if [ "$STASH_OUT" != "No local changes to save" ]; then
193         git stash pop -q # restore original state of $ORIG_REF, pruning any leftover commits added locally
194     fi
195 }
196 trap 'cleanup' EXIT
197
198 [ "$TARGET" = "" ] && TARGET=$ORIG_REF
199 if [ "$TARGET" != "$ORIG_REF" ]; then
200     echo -n "Press Enter to checkout $TARGET: "; read foo
201     git checkout $TARGET
202 fi
203
204 echo -n "Press Enter to pull the latest $REMOTE/$TARGET: "; read foo
205 REBASING=1
206 git pull $REMOTE $TARGET || exit 1
207 REBASING=
208
209 # append new commits from $REPO/$BRANCH
210 if [ "$PICK" == "no" ]; then
211     echo Rebasing $REPO/$BRANCH on $TARGET...
212     git fetch $REPO $BRANCH && git checkout -b $WORK FETCH_HEAD
213     WORK_USED=$WORK
214     REBASING=1
215     git rebase $TARGET || (echo 'Fix or Ctrl-d to abort' ; read || exit 1)
216     REBASING=
217 else
218     echo Cherry-picking $REPO/$BRANCH to $TARGET...
219     git checkout -b $WORK $TARGET
220     WORK_USED=$WORK
221     CHERRYPICKING=1
222     git fetch $REPO $BRANCH && (git cherry-pick FETCH_HEAD~$PICK..FETCH_HEAD || exit 1)
223     CHERRYPICKING=
224 fi
225
226 echo Diff against $REMOTE/$TARGET
227 git diff $REMOTE/$TARGET
228
229 if [ "$INTERACTIVE" == "yes" ] ; then
230     echo -n "Press Enter to interactively rebase $AUTOSQUASH on $REMOTE/$TARGET: "; read foo
231     REBASING=1
232     git rebase -i $AUTOSQUASH $REMOTE/$TARGET || exit 1
233     REBASING=
234     echo "Calling addrev $ADDREVOPTS --prnum=$PRNUM $TEAM $REMOTE/$TARGET.."
235     addrev $ADDREVOPTS --prnum=$PRNUM $TEAM $REMOTE/$TARGET..
236 fi
237
238 echo
239 echo Log since $REMOTE/$TARGET
240 git log $REMOTE/$TARGET..
241
242 git checkout $TARGET
243 if [ "$INTERACTIVE" != "yes" ] ; then
244     echo -n "Press Enter to non-interactively merge --squash $BRANCH to $REMOTE/$TARGET: "; read foo
245     ORIG_TARGET_HEAD=`git show -s --format="%H"`
246     git merge --ff-only --no-commit --squash $WORK
247     AUTHOR=`git show --no-patch --pretty="format:%an <%ae>" $WORK`
248     git commit --author="$AUTHOR"
249     addrev $ADDREVOPTS --prnum=$PRNUM $TEAM $REMOTE/${TARGET}..
250 else
251     # echo -n "Press Enter to merge to $TARGET: "; read foo
252     echo
253     echo "Merging to $TARGET"
254     ORIG_TARGET_HEAD=`git show -s --format="%H"`
255     git merge --ff-only $WORK
256 fi
257
258 echo New log since $REMOTE/$TARGET
259 git log $REMOTE/$TARGET..
260
261 if [ "$BUILD" == "yes" ] ; then
262     echo Rebuilding...
263     CC="$CC" opensslbuild >/dev/null # any STDERR output will be shown
264 fi
265
266 while true ; do
267     echo -n "Enter 'y'/'yes' to push to $REMOTE/$TARGET or 'n'/'no' to abort: "
268     read x
269     x="`echo $x | tr A-Z a-z`"
270     if [ "$x" = "y" -o "$x" = "yes" -o "$x" = "n" -o "$x" = "no" ] ; then
271         break
272     fi
273 done
274
275 if [ "$x" = "y" -o "$x" = "yes" ] ; then
276     git push -v $REMOTE $TARGET
277     ORIG_TARGET_HEAD=
278 fi