]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - tools/tools/git/git-arc.sh
zfs: merge openzfs/zfs@17b2ae0b2 (master) into main
[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 commit2diff()
191 {
192     local commit diff title
193
194     commit=$1
195
196     # First, look for a valid differential reference in the commit
197     # log.
198     diff=$(log2diff "$commit")
199     if [ -n "$diff" ]; then
200         echo "$diff"
201         return
202     fi
203
204     # Second, search the open reviews returned by 'arc list' looking
205     # for a subject match.
206     title=$(git show -s --format=%s "$commit")
207     diff=$(arc list | grep -F "$title" | grep -E -o 'D[1-9][0-9]*:' | tr -d ':')
208     if [ -z "$diff" ]; then
209         err "could not find review for '${title}'"
210     elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
211         err "found multiple reviews with the same title"
212     fi
213
214     echo "$diff"
215 }
216
217 create_one_review()
218 {
219     local childphid commit doprompt msg parent parentphid reviewers
220     local subscribers
221
222     commit=$1
223     reviewers=$2
224     subscribers=$3
225     parent=$4
226     doprompt=$5
227
228     if [ "$doprompt" ] && ! show_and_prompt "$commit"; then
229         return 1
230     fi
231
232     git checkout -q "$commit"
233
234     msg=$(mktemp)
235     git show -s --format='%B' "$commit" > "$msg"
236     printf "\nTest Plan:\n" >> "$msg"
237     printf "\nReviewers:\n" >> "$msg"
238     printf "%s\n" "${reviewers}" >> "$msg"
239     printf "\nSubscribers:\n" >> "$msg"
240     printf "%s\n" "${subscribers}" >> "$msg"
241
242     yes | env EDITOR=true \
243         arc diff --message-file "$msg" --never-apply-patches --create --allow-untracked $BROWSE HEAD~
244     [ $? -eq 0 ] || err "could not create Phabricator diff"
245
246     if [ -n "$parent" ]; then
247         diff=$(commit2diff "$commit")
248         [ -n "$diff" ] || err "failed to look up review ID for $commit"
249
250         childphid=$(diff2phid "$diff")
251         parentphid=$(diff2phid "$parent")
252         echo '{
253             "objectIdentifier": "'"${childphid}"'",
254             "transactions": [
255                 {
256                     "type": "parents.add",
257                     "value": ["'"${parentphid}"'"]
258                 }
259              ]}' |
260             arc call-conduit -- differential.revision.edit >&3
261     fi
262     rm -f "$msg"
263     return 0
264 }
265
266 # Get a list of reviewers who accepted the specified diff.
267 diff2reviewers()
268 {
269     local diff reviewid userids
270
271     diff=$1
272     reviewid=$(diff2phid "$diff")
273     userids=$( \
274         echo '{
275                   "constraints": {"phids": ["'"$reviewid"'"]},
276                   "attachments": {"reviewers": true}
277               }' |
278         arc call-conduit -- differential.revision.search |
279         jq '.response.data[0].attachments.reviewers.reviewers[] | select(.status == "accepted").reviewerPHID')
280     if [ -n "$userids" ]; then
281         echo '{
282                   "constraints": {"phids": ['"$(echo -n "$userids" | tr '[:space:]' ',')"']}
283               }' |
284             arc call-conduit -- user.search |
285             jq -r '.response.data[].fields.username'
286     fi
287 }
288
289 prompt()
290 {
291     local resp
292
293     if [ "$ASSUME_YES" ]; then
294         return 1
295     fi
296
297     printf "\nDoes this look OK? [y/N] "
298     read -r resp
299
300     case $resp in
301     [Yy])
302         return 0
303         ;;
304     *)
305         return 1
306         ;;
307     esac
308 }
309
310 show_and_prompt()
311 {
312     local commit
313
314     commit=$1
315
316     git show "$commit"
317     prompt
318 }
319
320 save_head()
321 {
322     local orig
323
324     if ! orig=$(git symbolic-ref --short -q HEAD); then
325         orig=$(git show -s --pretty=%H HEAD)
326     fi
327     SAVED_HEAD=$orig
328 }
329
330 restore_head()
331 {
332     if [ -n "$SAVED_HEAD" ]; then
333         git checkout -q "$SAVED_HEAD"
334         SAVED_HEAD=
335     fi
336 }
337
338 build_commit_list()
339 {
340     local chash _commits commits
341
342     for chash in "$@"; do
343         _commits=$(git rev-parse "${chash}")
344         if ! git cat-file -e "${chash}"'^{commit}' >/dev/null 2>&1; then
345             # shellcheck disable=SC2086
346             _commits=$(git rev-list $_commits | tail -r)
347         fi
348         [ -n "$_commits" ] || err "invalid commit ID ${chash}"
349         commits="$commits $_commits"
350     done
351     echo "$commits"
352 }
353
354 gitarc__create()
355 {
356     local commit commits doprompt list o prev reviewers subscribers
357
358     list=
359     prev=""
360     if [ "$(git config --bool --get arc.list 2>/dev/null || echo false)" != "false" ]; then
361         list=1
362     fi
363     doprompt=1
364     while getopts lp:r:s: o; do
365         case "$o" in
366         l)
367             list=1
368             ;;
369         p)
370             prev="$OPTARG"
371             ;;
372         r)
373             reviewers="$OPTARG"
374             ;;
375         s)
376             subscribers="$OPTARG"
377             ;;
378         *)
379             err_usage
380             ;;
381         esac
382     done
383     shift $((OPTIND-1))
384
385     commits=$(build_commit_list "$@")
386
387     if [ "$list" ]; then
388         for commit in ${commits}; do
389             git --no-pager show --oneline --no-patch "$commit"
390         done | git_pager
391         if ! prompt; then
392             return
393         fi
394         doprompt=
395     fi
396
397     save_head
398     for commit in ${commits}; do
399         if create_one_review "$commit" "$reviewers" "$subscribers" "$prev" \
400                              "$doprompt"; then
401             prev=$(commit2diff "$commit")
402         else
403             prev=""
404         fi
405     done
406     restore_head
407 }
408
409 gitarc__list()
410 {
411     local chash commit commits diff title
412
413     commits=$(build_commit_list "$@")
414
415     for commit in $commits; do
416         chash=$(git show -s --format='%C(auto)%h' "$commit")
417         echo -n "${chash} "
418
419         diff=$(log2diff "$commit")
420         if [ -n "$diff" ]; then
421                 diff2status "$diff"
422                 continue
423         fi
424
425         # This does not use commit2diff as it needs to handle errors
426         # differently and keep the entire status.  The extra 'cat'
427         # after 'fgrep' avoids erroring due to -e.
428         title=$(git show -s --format=%s "$commit")
429         diff=$(arc list | grep -F "$title" | cat)
430         if [ -z "$diff" ]; then
431             echo "No Review      : $title"
432         elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
433             echo -n "Ambiguous Reviews: "
434             echo "$diff" | grep -E -o 'D[1-9][0-9]*:' | tr -d ':' \
435                 | paste -sd ',' - | sed 's/,/, /g'
436         else
437             echo "$diff" | sed -e 's/^[^ ]* *//'
438         fi
439     done
440 }
441
442 gitarc__patch()
443 {
444     local rev
445
446     if [ $# -eq 0 ]; then
447         err_usage
448     fi
449
450     for rev in "$@"; do
451         arc patch --skip-dependencies --nocommit --nobranch --force "$rev"
452         echo "Applying ${rev}..."
453         [ $? -eq 0 ] || break
454     done
455 }
456
457 gitarc__stage()
458 {
459     local author branch commit commits diff reviewers tmp
460
461     branch=main
462     while getopts b: o; do
463         case "$o" in
464         b)
465             branch="$OPTARG"
466             ;;
467         *)
468             err_usage
469             ;;
470         esac
471     done
472     shift $((OPTIND-1))
473
474     commits=$(build_commit_list "$@")
475
476     if [ "$branch" = "main" ]; then
477         git checkout -q main
478     else
479         git checkout -q -b "${branch}" main
480     fi
481
482     tmp=$(mktemp)
483     for commit in $commits; do
484         git show -s --format=%B "$commit" > "$tmp"
485         diff=$(arc list | grep -F "$(git show -s --format=%s "$commit")" |
486             grep -E -o 'D[1-9][0-9]*:' | tr -d ':')
487         if [ -n "$diff" ]; then
488             # XXX this leaves an extra newline in some cases.
489             reviewers=$(diff2reviewers "$diff" | sed '/^$/d' | paste -sd ',' - | sed 's/,/, /g')
490             if [ -n "$reviewers" ]; then
491                 printf "Reviewed by:\t%s\n" "${reviewers}" >> "$tmp"
492             fi
493             printf "Differential Revision:\thttps://reviews.freebsd.org/%s" "${diff}" >> "$tmp"
494         fi
495         author=$(git show -s --format='%an <%ae>' "${commit}")
496         if ! git cherry-pick --no-commit "${commit}"; then
497             warn "Failed to apply $(git rev-parse --short "${commit}").  Are you staging patches in the wrong order?"
498             git checkout -f
499             break
500         fi
501         git commit --edit --file "$tmp" --author "${author}"
502     done
503 }
504
505 gitarc__update()
506 {
507     local commit commits diff
508
509     commits=$(build_commit_list "$@")
510     save_head
511     for commit in ${commits}; do
512         diff=$(commit2diff "$commit")
513
514         if ! show_and_prompt "$commit"; then
515             break
516         fi
517
518         git checkout -q "$commit"
519
520         # The linter is stupid and applies patches to the working copy.
521         # This would be tolerable if it didn't try to correct "misspelled" variable
522         # names.
523         arc diff --allow-untracked --never-apply-patches --update "$diff" HEAD~
524     done
525     restore_head
526 }
527
528 set -e
529
530 ASSUME_YES=
531 if [ "$(git config --bool --get arc.assume-yes 2>/dev/null || echo false)" != "false" ]; then
532     ASSUME_YES=1
533 fi
534
535 VERBOSE=
536 while getopts vy o; do
537     case "$o" in
538     v)
539         VERBOSE=1
540         ;;
541     y)
542         ASSUME_YES=1
543         ;;
544     *)
545         err_usage
546         ;;
547     esac
548 done
549 shift $((OPTIND-1))
550
551 [ $# -ge 1 ] || err_usage
552
553 which arc >/dev/null 2>&1 || err "arc is required, install devel/arcanist"
554 which jq >/dev/null 2>&1 || err "jq is required, install textproc/jq"
555
556 if [ "$VERBOSE" ]; then
557     exec 3>&1
558 else
559     exec 3> /dev/null
560 fi
561
562 case "$1" in
563 create|list|patch|stage|update)
564     ;;
565 *)
566     err_usage
567     ;;
568 esac
569 verb=$1
570 shift
571
572 # All subcommands require at least one parameter.
573 if [ $# -eq 0 ]; then
574     err_usage
575 fi
576
577 # Pull in some git helper functions.
578 git_sh_setup=$(git --exec-path)/git-sh-setup
579 [ -f "$git_sh_setup" ] || err "cannot find git-sh-setup"
580 SUBDIRECTORY_OK=y
581 USAGE=
582 # shellcheck disable=SC1090
583 . "$git_sh_setup"
584
585 # Bail if the working tree is unclean, except for "list" and "patch"
586 # operations.
587 case $verb in
588 list|patch)
589     ;;
590 *)
591     require_clean_work_tree "$verb"
592     ;;
593 esac
594
595 if [ "$(git config --bool --get arc.browse 2>/dev/null || echo false)" != "false" ]; then
596     BROWSE=--browse
597 fi
598
599 trap restore_head EXIT INT
600
601 gitarc__"${verb}" "$@"