]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - tools/tools/git/git-arc.sh
zfs: merge openzfs/zfs@c629f0bf6
[FreeBSD/FreeBSD.git] / tools / tools / git / git-arc.sh
1 #!/bin/sh
2 #
3 # SPDX-License-Identifier: BSD-2-Clause-FreeBSD
4 #
5 # Copyright (c) 2019-2021 Mark Johnston <markj@FreeBSD.org>
6 # Copyright (c) 2021 John Baldwin <jhb@FreeBSD.org>
7 #
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions are
10 # met:
11 # 1. Redistributions of source code must retain the above copyright
12 #    notice, this list of conditions and the following disclaimer.
13 # 2. Redistributions in binary form must reproduce the above copyright
14 #    notice, this list of conditions and the following disclaimer in
15 #    the documentation and/or other materials provided with the distribution.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 # SUCH DAMAGE.
28 #
29
30 # TODO:
31 # - roll back after errors or SIGINT
32 #   - created revs
33 #   - main (for git arc stage)
34
35 warn()
36 {
37     echo "$(basename "$0"): $1" >&2
38 }
39
40 err()
41 {
42     warn "$1"
43     exit 1
44 }
45
46 err_usage()
47 {
48     cat >&2 <<__EOF__
49 Usage: git arc [-vy] <command> <arguments>
50
51 Commands:
52   create [-l] [-r <reviewer1>[,<reviewer2>...]] [-s subscriber[,...]] [<commit>|<commit range>]
53   list <commit>|<commit range>
54   patch <diff1> [<diff2> ...]
55   stage [-b branch] [<commit>|<commit range>]
56   update [<commit>|<commit range>]
57
58 Description:
59   Create or manage FreeBSD Phabricator reviews based on git commits.  There
60   is a one-to one relationship between git commits and Differential revisions,
61   and the Differential revision title must match the summary line of the
62   corresponding commit.  In particular, commit summaries must be unique across
63   all open Differential revisions authored by you.
64
65   The first parameter must be a verb.  The available verbs are:
66
67     create -- Create new Differential revisions from the specified commits.
68     list   -- Print the associated Differential revisions for the specified
69               commits.
70     patch  -- Try to apply a patch from a Differential revision to the
71               currently checked out tree.
72     stage  -- Prepare a series of commits to be pushed to the upstream FreeBSD
73               repository.  The commits are cherry-picked to a branch (main by
74               default), review tags are added to the commit log message, and
75               the log message is opened in an editor for any last-minute
76               updates.  The commits need not have associated Differential
77               revisions.
78     update -- Synchronize the Differential revisions associated with the
79               specified commits.  Currently only the diff is updated; the
80               review description and other metadata is not synchronized.
81
82   The typical end-to-end usage looks something like this:
83
84     $ git commit -m "kern: Rewrite in Rust"
85     $ git arc create HEAD
86     <Make changes to the diff based on reviewer feedback.>
87     $ git commit --amend
88     $ git arc update HEAD
89     <Now that all reviewers are happy, it's time to push.>
90     $ git arc stage HEAD
91     $ git push freebsd HEAD:main
92
93 Config Variables:
94   These are manipulated by git-config(1).
95
96     arc.assume_yes [bool]
97                        -- Assume a "yes" answer to all prompts instead of
98                           prompting the user.  Equivalent to the -y flag.
99
100     arc.browse [bool]  -- Try to open newly created reviews in a browser tab.
101                           Defaults to false.
102
103     arc.list [bool]    -- Always use "list mode" (-l) with create.  In this
104                           mode, the list of git revisions to create reviews for
105                           is listed with a single prompt before creating
106                           reviews.  The diffs for individual commits are not
107                           shown.
108
109     arc.verbose [bool] -- Verbose output.  Equivalent to the -v flag.
110
111 Examples:
112   Create a Phabricator review using the contents of the most recent commit in
113   your git checkout.  The commit title is used as the review title, the commit
114   log message is used as the review description, markj@FreeBSD.org is added as
115   a reviewer.
116
117   $ git arc create -r markj HEAD
118
119   Create a series of Phabricator reviews for each of HEAD~2, HEAD~ and HEAD.
120   Pairs of consecutive commits are linked into a patch stack.  Note that the
121   first commit in the specified range is excluded.
122
123   $ git arc create HEAD~3..HEAD
124
125   Update the review corresponding to commit b409afcfedcdda.  The title of the
126   commit must be the same as it was when the review was created.  The review
127   description is not automatically updated.
128
129   $ git arc update b409afcfedcdda
130
131   Apply the patch in review D12345 to the currently checked-out tree, and stage
132   it.
133
134   $ git arc patch D12345
135
136   List the status of reviews for all the commits in the branch "feature":
137
138   $ git arc list main..feature
139
140 __EOF__
141
142     exit 1
143 }
144
145 diff2phid()
146 {
147     local diff
148
149     diff=$1
150     if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then
151         err "invalid diff ID $diff"
152     fi
153
154     echo '{"names":["'"$diff"'"]}' |
155         arc call-conduit -- phid.lookup |
156         jq -r "select(.response != []) | .response.${diff}.phid"
157 }
158
159 diff2status()
160 {
161     local diff tmp status summary
162
163     diff=$1
164     if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then
165         err "invalid diff ID $diff"
166     fi
167
168     tmp=$(mktemp)
169     echo '{"names":["'"$diff"'"]}' |
170         arc call-conduit -- phid.lookup > "$tmp"
171     status=$(jq -r "select(.response != []) | .response.${diff}.status" < "$tmp")
172     summary=$(jq -r "select(.response != []) |
173          .response.${diff}.fullName" < "$tmp")
174     printf "%-14s %s\n" "${status}" "${summary}"
175 }
176
177 log2diff()
178 {
179     local diff
180
181     diff=$(git show -s --format=%B "$commit" |
182         sed -nE '/^Differential Revision:[[:space:]]+(https:\/\/reviews.freebsd.org\/)?(D[0-9]+)$/{s//\2/;p;}')
183     if [ -n "$diff" ] && [ "$(echo "$diff" | wc -l)" -eq 1 ]; then
184         echo "$diff"
185     else
186         echo
187     fi
188 }
189
190 # Look for an open revision with a title equal to the input string.  Return
191 # a possibly empty list of Differential revision IDs.
192 title2diff()
193 {
194     local title
195
196     title=$1
197     # arc list output always includes ANSI escape sequences, strip them.
198     arc list | sed 's/\x1b\[[0-9;]*m//g' | \
199         awk -F': ' '{
200             if (substr($0, index($0, FS) + length(FS)) == "'"$title"'") {
201                 print substr($1, match($1, "D[1-9][0-9]*"))
202             }
203         }'
204 }
205
206 commit2diff()
207 {
208     local commit diff title
209
210     commit=$1
211
212     # First, look for a valid differential reference in the commit
213     # log.
214     diff=$(log2diff "$commit")
215     if [ -n "$diff" ]; then
216         echo "$diff"
217         return
218     fi
219
220     # Second, search the open reviews returned by 'arc list' looking
221     # for a subject match.
222     title=$(git show -s --format=%s "$commit")
223     diff=$(title2diff "$title")
224     if [ -z "$diff" ]; then
225         err "could not find review for '${title}'"
226     elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
227         err "found multiple reviews with the same title"
228     fi
229
230     echo "$diff"
231 }
232
233 create_one_review()
234 {
235     local childphid commit doprompt msg parent parentphid reviewers
236     local subscribers
237
238     commit=$1
239     reviewers=$2
240     subscribers=$3
241     parent=$4
242     doprompt=$5
243
244     if [ "$doprompt" ] && ! show_and_prompt "$commit"; then
245         return 1
246     fi
247
248     msg=$(mktemp)
249     git show -s --format='%B' "$commit" > "$msg"
250     printf "\nTest Plan:\n" >> "$msg"
251     printf "\nReviewers:\n" >> "$msg"
252     printf "%s\n" "${reviewers}" >> "$msg"
253     printf "\nSubscribers:\n" >> "$msg"
254     printf "%s\n" "${subscribers}" >> "$msg"
255
256     yes | env EDITOR=true \
257         arc diff --message-file "$msg" --never-apply-patches --create \
258         --allow-untracked $BROWSE --head "$commit" "${commit}~"
259     [ $? -eq 0 ] || err "could not create Phabricator diff"
260
261     if [ -n "$parent" ]; then
262         diff=$(commit2diff "$commit")
263         [ -n "$diff" ] || err "failed to look up review ID for $commit"
264
265         childphid=$(diff2phid "$diff")
266         parentphid=$(diff2phid "$parent")
267         echo '{
268             "objectIdentifier": "'"${childphid}"'",
269             "transactions": [
270                 {
271                     "type": "parents.add",
272                     "value": ["'"${parentphid}"'"]
273                 }
274              ]}' |
275             arc call-conduit -- differential.revision.edit >&3
276     fi
277     rm -f "$msg"
278     return 0
279 }
280
281 # Get a list of reviewers who accepted the specified diff.
282 diff2reviewers()
283 {
284     local diff reviewid userids
285
286     diff=$1
287     reviewid=$(diff2phid "$diff")
288     userids=$( \
289         echo '{
290                   "constraints": {"phids": ["'"$reviewid"'"]},
291                   "attachments": {"reviewers": true}
292               }' |
293         arc call-conduit -- differential.revision.search |
294         jq '.response.data[0].attachments.reviewers.reviewers[] | select(.status == "accepted").reviewerPHID')
295     if [ -n "$userids" ]; then
296         echo '{
297                   "constraints": {"phids": ['"$(echo -n "$userids" | tr '[:space:]' ',')"']}
298               }' |
299             arc call-conduit -- user.search |
300             jq -r '.response.data[].fields.username'
301     fi
302 }
303
304 prompt()
305 {
306     local resp
307
308     if [ "$ASSUME_YES" ]; then
309         return 0
310     fi
311
312     printf "\nDoes this look OK? [y/N] "
313     read -r resp
314
315     case $resp in
316     [Yy])
317         return 0
318         ;;
319     *)
320         return 1
321         ;;
322     esac
323 }
324
325 show_and_prompt()
326 {
327     local commit
328
329     commit=$1
330
331     git show "$commit"
332     prompt
333 }
334
335 build_commit_list()
336 {
337     local chash _commits commits
338
339     for chash in "$@"; do
340         _commits=$(git rev-parse "${chash}")
341         if ! git cat-file -e "${chash}"'^{commit}' >/dev/null 2>&1; then
342             # shellcheck disable=SC2086
343             _commits=$(git rev-list $_commits | tail -r)
344         fi
345         [ -n "$_commits" ] || err "invalid commit ID ${chash}"
346         commits="$commits $_commits"
347     done
348     echo "$commits"
349 }
350
351 gitarc__create()
352 {
353     local commit commits doprompt list o prev reviewers subscribers
354
355     list=
356     prev=""
357     if [ "$(git config --bool --get arc.list 2>/dev/null || echo false)" != "false" ]; then
358         list=1
359     fi
360     doprompt=1
361     while getopts lp:r:s: o; do
362         case "$o" in
363         l)
364             list=1
365             ;;
366         p)
367             prev="$OPTARG"
368             ;;
369         r)
370             reviewers="$OPTARG"
371             ;;
372         s)
373             subscribers="$OPTARG"
374             ;;
375         *)
376             err_usage
377             ;;
378         esac
379     done
380     shift $((OPTIND-1))
381
382     commits=$(build_commit_list "$@")
383
384     if [ "$list" ]; then
385         for commit in ${commits}; do
386             git --no-pager show --oneline --no-patch "$commit"
387         done | git_pager
388         if ! prompt; then
389             return
390         fi
391         doprompt=
392     fi
393
394     for commit in ${commits}; do
395         if create_one_review "$commit" "$reviewers" "$subscribers" "$prev" \
396                              "$doprompt"; then
397             prev=$(commit2diff "$commit")
398         else
399             prev=""
400         fi
401     done
402 }
403
404 gitarc__list()
405 {
406     local chash commit commits diff openrevs title
407
408     commits=$(build_commit_list "$@")
409     openrevs=$(arc list)
410
411     for commit in $commits; do
412         chash=$(git show -s --format='%C(auto)%h' "$commit")
413         echo -n "${chash} "
414
415         diff=$(log2diff "$commit")
416         if [ -n "$diff" ]; then
417                 diff2status "$diff"
418                 continue
419         fi
420
421         # This does not use commit2diff as it needs to handle errors
422         # differently and keep the entire status.
423         title=$(git show -s --format=%s "$commit")
424         diff=$(echo "$openrevs" | \
425             awk -F'D[1-9][0-9]*:\.\\[m ' '{if ($2 == "'"$title"'") print $0}')
426         if [ -z "$diff" ]; then
427             echo "No Review      : $title"
428         elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
429             echo -n "Ambiguous Reviews: "
430             echo "$diff" | grep -E -o 'D[1-9][0-9]*:' | tr -d ':' \
431                 | paste -sd ',' - | sed 's/,/, /g'
432         else
433             echo "$diff" | sed -e 's/^[^ ]* *//'
434         fi
435     done
436 }
437
438 gitarc__patch()
439 {
440     local rev
441
442     if [ $# -eq 0 ]; then
443         err_usage
444     fi
445
446     for rev in "$@"; do
447         arc patch --skip-dependencies --nocommit --nobranch --force "$rev"
448         echo "Applying ${rev}..."
449         [ $? -eq 0 ] || break
450     done
451 }
452
453 gitarc__stage()
454 {
455     local author branch commit commits diff reviewers title tmp
456
457     branch=main
458     while getopts b: o; do
459         case "$o" in
460         b)
461             branch="$OPTARG"
462             ;;
463         *)
464             err_usage
465             ;;
466         esac
467     done
468     shift $((OPTIND-1))
469
470     commits=$(build_commit_list "$@")
471
472     if [ "$branch" = "main" ]; then
473         git checkout -q main
474     else
475         git checkout -q -b "${branch}" main
476     fi
477
478     tmp=$(mktemp)
479     for commit in $commits; do
480         git show -s --format=%B "$commit" > "$tmp"
481         title=$(git show -s --format=%s "$commit")
482         diff=$(title2diff "$title")
483         if [ -n "$diff" ]; then
484             # XXX this leaves an extra newline in some cases.
485             reviewers=$(diff2reviewers "$diff" | sed '/^$/d' | paste -sd ',' - | sed 's/,/, /g')
486             if [ -n "$reviewers" ]; then
487                 printf "Reviewed by:\t%s\n" "${reviewers}" >> "$tmp"
488             fi
489             printf "Differential Revision:\thttps://reviews.freebsd.org/%s" "${diff}" >> "$tmp"
490         fi
491         author=$(git show -s --format='%an <%ae>' "${commit}")
492         if ! git cherry-pick --no-commit "${commit}"; then
493             warn "Failed to apply $(git rev-parse --short "${commit}").  Are you staging patches in the wrong order?"
494             git checkout -f
495             break
496         fi
497         git commit --edit --file "$tmp" --author "${author}"
498     done
499 }
500
501 gitarc__update()
502 {
503     local commit commits diff
504
505     commits=$(build_commit_list "$@")
506     for commit in ${commits}; do
507         diff=$(commit2diff "$commit")
508
509         if ! show_and_prompt "$commit"; then
510             break
511         fi
512
513         # The linter is stupid and applies patches to the working copy.
514         # This would be tolerable if it didn't try to correct "misspelled" variable
515         # names.
516         arc diff --allow-untracked --never-apply-patches --update "$diff" \
517             --head "$commit" "${commit}~"
518     done
519 }
520
521 set -e
522
523 ASSUME_YES=
524 if [ "$(git config --bool --get arc.assume-yes 2>/dev/null || echo false)" != "false" ]; then
525     ASSUME_YES=1
526 fi
527
528 VERBOSE=
529 while getopts vy o; do
530     case "$o" in
531     v)
532         VERBOSE=1
533         ;;
534     y)
535         ASSUME_YES=1
536         ;;
537     *)
538         err_usage
539         ;;
540     esac
541 done
542 shift $((OPTIND-1))
543
544 [ $# -ge 1 ] || err_usage
545
546 which arc >/dev/null 2>&1 || err "arc is required, install devel/arcanist"
547 which jq >/dev/null 2>&1 || err "jq is required, install textproc/jq"
548
549 if [ "$VERBOSE" ]; then
550     exec 3>&1
551 else
552     exec 3> /dev/null
553 fi
554
555 case "$1" in
556 create|list|patch|stage|update)
557     ;;
558 *)
559     err_usage
560     ;;
561 esac
562 verb=$1
563 shift
564
565 # All subcommands require at least one parameter.
566 if [ $# -eq 0 ]; then
567     err_usage
568 fi
569
570 # Pull in some git helper functions.
571 git_sh_setup=$(git --exec-path)/git-sh-setup
572 [ -f "$git_sh_setup" ] || err "cannot find git-sh-setup"
573 SUBDIRECTORY_OK=y
574 USAGE=
575 # shellcheck disable=SC1090
576 . "$git_sh_setup"
577
578 # git commands use GIT_EDITOR instead of EDITOR, so try to provide consistent
579 # behaviour.  Ditto for PAGER.  This makes git-arc play nicer with editor
580 # plugins like vim-fugitive.
581 if [ -n "$GIT_EDITOR" ]; then
582     EDITOR=$GIT_EDITOR
583 fi
584 if [ -n "$GIT_PAGER" ]; then
585     PAGER=$GIT_PAGER
586 fi
587
588 # Bail if the working tree is unclean, except for "list" and "patch"
589 # operations.
590 case $verb in
591 list|patch)
592     ;;
593 *)
594     require_clean_work_tree "$verb"
595     ;;
596 esac
597
598 if [ "$(git config --bool --get arc.browse 2>/dev/null || echo false)" != "false" ]; then
599     BROWSE=--browse
600 fi
601
602 gitarc__"${verb}" "$@"