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