xref: /freebsd/tools/tools/git/git-arc.sh (revision 3a20f630a9fcf6a1267cd527464edf71d01c8771)
1#!/bin/sh
2#
3# SPDX-License-Identifier: BSD-2-Clause
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
35warn()
36{
37    echo "$(basename "$0"): $1" >&2
38}
39
40err()
41{
42    warn "$1"
43    exit 1
44}
45
46cleanup()
47{
48    rc=$?
49    rm -fr "$GITARC_TMPDIR"
50    trap - EXIT
51    exit $rc
52}
53
54err_usage()
55{
56    cat >&2 <<__EOF__
57Usage: git arc [-vy] <command> <arguments>
58
59Commands:
60  create [-l] [-r <reviewer1>[,<reviewer2>...]] [-s subscriber[,...]] [<commit>|<commit range>]
61  list <commit>|<commit range>
62  patch [-c] <diff1> [<diff2> ...]
63  stage [-b branch] [<commit>|<commit range>]
64  update [-l] [-m message] [<commit>|<commit range>]
65
66Description:
67  Create or manage FreeBSD Phabricator reviews based on git commits.  There
68  is a one-to one relationship between git commits and Differential revisions,
69  and the Differential revision title must match the summary line of the
70  corresponding commit.  In particular, commit summaries must be unique across
71  all open Differential revisions authored by you.
72
73  The first parameter must be a verb.  The available verbs are:
74
75    create -- Create new Differential revisions from the specified commits.
76    list   -- Print the associated Differential revisions for the specified
77              commits.
78    patch  -- Try to apply a patch from a Differential revision to the
79              currently checked out tree.
80    stage  -- Prepare a series of commits to be pushed to the upstream FreeBSD
81              repository.  The commits are cherry-picked to a branch (main by
82              default), review tags are added to the commit log message, and
83              the log message is opened in an editor for any last-minute
84              updates.  The commits need not have associated Differential
85              revisions.
86    update -- Synchronize the Differential revisions associated with the
87              specified commits.  Currently only the diff is updated; the
88              review description and other metadata is not synchronized.
89
90  The typical end-to-end usage looks something like this:
91
92    $ git commit -m "kern: Rewrite in Rust"
93    $ git arc create HEAD
94    <Make changes to the diff based on reviewer feedback.>
95    $ git commit --amend
96    $ git arc update HEAD
97    <Now that all reviewers are happy, it's time to push.>
98    $ git arc stage HEAD
99    $ git push freebsd HEAD:main
100
101Config Variables:
102  These are manipulated by git-config(1).
103
104    arc.assume_yes [bool]
105                       -- Assume a "yes" answer to all prompts instead of
106                          prompting the user.  Equivalent to the -y flag.
107
108    arc.browse [bool]  -- Try to open newly created reviews in a browser tab.
109                          Defaults to false.
110
111    arc.list [bool]    -- Always use "list mode" (-l) with create and update.
112                          In this mode, the list of git revisions to use
113                          is listed with a single prompt before creating or
114                          updating reviews.  The diffs for individual commits
115                          are not shown.
116
117    arc.verbose [bool] -- Verbose output.  Equivalent to the -v flag.
118
119Examples:
120  Create a Phabricator review using the contents of the most recent commit in
121  your git checkout.  The commit title is used as the review title, the commit
122  log message is used as the review description, markj@FreeBSD.org is added as
123  a reviewer. Also, the "Jails" reviewer group is added using its hashtag.
124
125  $ git arc create -r markj,#jails HEAD
126
127  Create a series of Phabricator reviews for each of HEAD~2, HEAD~ and HEAD.
128  Pairs of consecutive commits are linked into a patch stack.  Note that the
129  first commit in the specified range is excluded.
130
131  $ git arc create HEAD~3..HEAD
132
133  Update the review corresponding to commit b409afcfedcdda.  The title of the
134  commit must be the same as it was when the review was created.  The review
135  description is not automatically updated.
136
137  $ git arc update b409afcfedcdda
138
139  Apply the patch in review D12345 to the currently checked-out tree, and stage
140  it.
141
142  $ git arc patch D12345
143
144  Apply the patch in review D12345 to the currently checked-out tree, and
145  commit it using the review's title, summary and author.
146
147  $ git arc patch -c D12345
148
149  List the status of reviews for all the commits in the branch "feature":
150
151  $ git arc list main..feature
152
153__EOF__
154
155    exit 1
156}
157
158# Use xmktemp instead of mktemp when creating temporary files.
159xmktemp()
160{
161    mktemp "${GITARC_TMPDIR:?}/tmp.XXXXXXXXXX" || exit 1
162}
163
164#
165# Fetch the value of a boolean config variable ($1) and return true
166# (0) if the variable is true.  The default value to use if the
167# variable is not set is passed in $2.
168#
169get_bool_config()
170{
171    test "$(git config --bool --get $1 2>/dev/null || echo $2)" != "false"
172}
173
174#
175# Filter the output of call-conduit to remove the warnings that are generated
176# for some installations where openssl module is mysteriously installed twice so
177# a warning is generated. It's likely a local config error, but we should work
178# in the face of that.
179#
180arc_call_conduit()
181{
182    arc call-conduit "$@" | grep -v '^Warning: '
183}
184
185#
186# Filter the output of arc list to remove the warnings as above, as well as
187# the bolding sequence (the color sequence remains intact).
188#
189arc_list()
190{
191    arc list "$@" | grep -v '^Warning: ' | sed -E 's/\x1b\[1m//g;s/\x1b\[m//g'
192}
193
194diff2phid()
195{
196    local diff
197
198    diff=$1
199    if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then
200        err "invalid diff ID $diff"
201    fi
202
203    echo '{"names":["'"$diff"'"]}' |
204        arc_call_conduit -- phid.lookup |
205        jq -r "select(.response != []) | .response.${diff}.phid"
206}
207
208diff2status()
209{
210    local diff tmp status summary
211
212    diff=$1
213    if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then
214        err "invalid diff ID $diff"
215    fi
216
217    tmp=$(xmktemp)
218    echo '{"names":["'"$diff"'"]}' |
219        arc_call_conduit -- phid.lookup > "$tmp"
220    status=$(jq -r "select(.response != []) | .response.${diff}.status" < "$tmp")
221    summary=$(jq -r "select(.response != []) |
222        .response.${diff}.fullName" < "$tmp")
223    printf "%-14s %s\n" "${status}" "${summary}"
224}
225
226log2diff()
227{
228    local diff
229
230    diff=$(git show -s --format=%B "$commit" |
231        sed -nE '/^Differential Revision:[[:space:]]+(https:\/\/reviews.freebsd.org\/)?(D[0-9]+)$/{s//\2/;p;}')
232    if [ -n "$diff" ] && [ "$(echo "$diff" | wc -l)" -eq 1 ]; then
233        echo "$diff"
234    else
235        echo
236    fi
237}
238
239# Look for an open revision with a title equal to the input string.  Return
240# a possibly empty list of Differential revision IDs.
241title2diff()
242{
243    local title
244
245    title=$(echo $1 | sed 's/"/\\"/g')
246    arc_list --no-ansi |
247        awk -F': ' '{
248            if (substr($0, index($0, FS) + length(FS)) == "'"$title"'") {
249                print substr($1, match($1, "D[1-9][0-9]*"))
250            }
251        }'
252}
253
254commit2diff()
255{
256    local commit diff title
257
258    commit=$1
259
260    # First, look for a valid differential reference in the commit
261    # log.
262    diff=$(log2diff "$commit")
263    if [ -n "$diff" ]; then
264        echo "$diff"
265        return
266    fi
267
268    # Second, search the open reviews returned by 'arc list' looking
269    # for a subject match.
270    title=$(git show -s --format=%s "$commit")
271    diff=$(title2diff "$title")
272    if [ -z "$diff" ]; then
273        err "could not find review for '${title}'"
274    elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
275        err "found multiple reviews with the same title"
276    fi
277
278    echo "$diff"
279}
280
281create_one_review()
282{
283    local childphid commit doprompt msg parent parentphid reviewers
284    local subscribers
285
286    commit=$1
287    reviewers=$2
288    subscribers=$3
289    parent=$4
290    doprompt=$5
291
292    if [ "$doprompt" ] && ! show_and_prompt "$commit"; then
293        return 1
294    fi
295
296    msg=$(xmktemp)
297    git show -s --format='%B' "$commit" > "$msg"
298    printf "\nTest Plan:\n" >> "$msg"
299    printf "\nReviewers:\n" >> "$msg"
300    printf "%s\n" "${reviewers}" >> "$msg"
301    printf "\nSubscribers:\n" >> "$msg"
302    printf "%s\n" "${subscribers}" >> "$msg"
303
304    yes | env EDITOR=true \
305        arc diff --message-file "$msg" --never-apply-patches --create \
306        --allow-untracked $BROWSE --head "$commit" "${commit}~"
307    [ $? -eq 0 ] || err "could not create Phabricator diff"
308
309    if [ -n "$parent" ]; then
310        diff=$(commit2diff "$commit")
311        [ -n "$diff" ] || err "failed to look up review ID for $commit"
312
313        childphid=$(diff2phid "$diff")
314        parentphid=$(diff2phid "$parent")
315        echo '{
316            "objectIdentifier": "'"${childphid}"'",
317            "transactions": [
318                {
319                    "type": "parents.add",
320                    "value": ["'"${parentphid}"'"]
321                }
322            ]}' |
323            arc_call_conduit -- differential.revision.edit >&3
324    fi
325    return 0
326}
327
328# Get a list of reviewers who accepted the specified diff.
329diff2reviewers()
330{
331    local diff reviewid userids
332
333    diff=$1
334    reviewid=$(diff2phid "$diff")
335    userids=$( \
336        echo '{
337        "constraints": {"phids": ["'"$reviewid"'"]},
338        "attachments": {"reviewers": true}
339        }' |
340        arc_call_conduit -- differential.revision.search |
341        jq '.response.data[0].attachments.reviewers.reviewers[] | select(.status == "accepted").reviewerPHID')
342    if [ -n "$userids" ]; then
343        echo '{
344        "constraints": {"phids": ['"$(echo $userids | tr '[:blank:]' ',')"']}
345        }' |
346        arc_call_conduit -- user.search |
347        jq -r '.response.data[].fields.username'
348    fi
349}
350
351prompt()
352{
353    local resp
354
355    if [ "$ASSUME_YES" ]; then
356        return 0
357    fi
358
359    printf "\nDoes this look OK? [y/N] "
360    read -r resp
361
362    case $resp in
363    [Yy])
364        return 0
365        ;;
366    *)
367        return 1
368        ;;
369    esac
370}
371
372show_and_prompt()
373{
374    local commit
375
376    commit=$1
377
378    git show "$commit"
379    prompt
380}
381
382build_commit_list()
383{
384    local chash _commits commits
385
386    for chash in "$@"; do
387        _commits=$(git rev-parse "${chash}")
388        if ! git cat-file -e "${chash}"'^{commit}' >/dev/null 2>&1; then
389            # shellcheck disable=SC2086
390            _commits=$(git rev-list --reverse $_commits)
391        fi
392        [ -n "$_commits" ] || err "invalid commit ID ${chash}"
393        commits="$commits $_commits"
394    done
395    echo "$commits"
396}
397
398gitarc__create()
399{
400    local commit commits doprompt list o prev reviewers subscribers
401
402    list=
403    prev=""
404    if get_bool_config arc.list false; then
405        list=1
406    fi
407    doprompt=1
408    while getopts lp:r:s: o; do
409        case "$o" in
410        l)
411            list=1
412            ;;
413        p)
414            prev="$OPTARG"
415            ;;
416        r)
417            reviewers="$OPTARG"
418            ;;
419        s)
420            subscribers="$OPTARG"
421            ;;
422        *)
423            err_usage
424            ;;
425        esac
426    done
427    shift $((OPTIND-1))
428
429    commits=$(build_commit_list "$@")
430
431    if [ "$list" ]; then
432        for commit in ${commits}; do
433            git --no-pager show --oneline --no-patch "$commit"
434        done | git_pager
435        if ! prompt; then
436            return
437        fi
438        doprompt=
439    fi
440
441    for commit in ${commits}; do
442        if create_one_review "$commit" "$reviewers" "$subscribers" "$prev" \
443            "$doprompt"; then
444            prev=$(commit2diff "$commit")
445        else
446            prev=""
447        fi
448    done
449}
450
451gitarc__list()
452{
453    local chash commit commits diff openrevs title
454
455    commits=$(build_commit_list "$@")
456    openrevs=$(arc_list --ansi)
457
458    for commit in $commits; do
459        chash=$(git show -s --format='%C(auto)%h' "$commit")
460        printf "%s" "${chash} "
461
462        diff=$(log2diff "$commit")
463        if [ -n "$diff" ]; then
464            diff2status "$diff"
465            continue
466        fi
467
468        # This does not use commit2diff as it needs to handle errors
469        # differently and keep the entire status.
470        title=$(git show -s --format=%s "$commit")
471        diff=$(echo "$openrevs" | \
472            awk -F'D[1-9][0-9]*: ' \
473            '{if ($2 == "'"$(echo $title | sed 's/"/\\"/g')"'") print $0}')
474        if [ -z "$diff" ]; then
475            echo "No Review            : $title"
476        elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
477            printf "%s" "Ambiguous Reviews: "
478            echo "$diff" | grep -E -o 'D[1-9][0-9]*:' | tr -d ':' \
479                | paste -sd ',' - | sed 's/,/, /g'
480        else
481            echo "$diff" | sed -e 's/^[^ ]* *//'
482        fi
483    done
484}
485
486# Try to guess our way to a good author name. The DWIM is strong in this
487# function, but these heuristics seem to generally produce the right results, in
488# the sample of src commits I checked out.
489find_author()
490{
491    local addr name email author_addr author_name
492
493    addr="$1"
494    name="$2"
495    author_addr="$3"
496    author_name="$4"
497
498    # The Phabricator interface doesn't have a simple way to get author name and
499    # address, so we have to try a number of heuristics to get the right result.
500
501    # Choice 1: It's a FreeBSD committer. These folks have no '.' in their phab
502    # username/addr. Sampled data in phab suggests that there's a high rate of
503    # these people having their local config pointing at something other than
504    # freebsd.org (which isn't surprising for ports committers getting src
505    # commits reviewed).
506    case "${addr}" in
507    *.*) ;;             # external user
508    *)
509        echo "${name} <${addr}@FreeBSD.org>"
510        return
511        ;;
512    esac
513
514    # Choice 2: author_addr and author_name were set in the bundle, so use
515    # that. We may need to filter some known bogus ones, should they crop up.
516    if [ -n "$author_name" -a -n "$author_addr" ]; then
517        echo "${author_name} <${author_addr}>"
518        return
519    fi
520
521    # Choice 3: We can find this user in the FreeBSD repo. They've submited
522    # something before, and they happened to use an email that's somewhat
523    # similar to their phab username.
524    email=$(git log -1 --author "$(echo ${addr} | tr _ .)" --pretty="%aN <%aE>")
525    if [ -n "${email}" ]; then
526        echo "${email}"
527        return
528    fi
529
530    # Choice 4: We know this user. They've committed before, and they happened
531    # to use the same name, unless the name has the word 'user' in it. This
532    # might not be a good idea, since names can be somewhat common (there
533    # are two Andrew Turners that have contributed to FreeBSD, for example).
534    if ! (echo "${name}" | grep -w "[Uu]ser" -q); then
535        email=$(git log -1 --author "${name}" --pretty="%aN <%aE>")
536        if [ -n "$email" ]; then
537            echo "$email"
538            return
539        fi
540    fi
541
542    # Choice 5: Wing it as best we can. In this scenario, we replace the last _
543    # with a @, and call it the email address...
544    # Annoying fun fact: Phab replaces all non alpha-numerics with _, so we
545    # don't know if the prior _ are _ or + or any number of other characters.
546    # Since there's issues here, prompt
547    a=$(printf "%s <%s>\n" "${name}" $(echo "$addr" | sed -e 's/\(.*\)_/\1@/'))
548    echo "Making best guess: Turning ${addr} to ${a}" >&2
549    if ! prompt; then
550        echo "ABORT"
551        return
552    fi
553    echo "${a}"
554}
555
556patch_commit()
557{
558    local diff reviewid review_data authorid user_data user_addr user_name
559    local diff_data author_addr author_name author tmp
560
561    diff=$1
562    reviewid=$(diff2phid "$diff")
563    # Get the author phid for this patch
564    review_data=$(xmktemp)
565    echo '{"constraints": {"phids": ["'"$reviewid"'"]}}' | \
566        arc_call_conduit -- differential.revision.search > "$review_data"
567    authorid=$(jq -r '.response.data[].fields.authorPHID' "$review_data")
568    # Get metadata about the user that submitted this patch
569    user_data=$(xmktemp)
570    echo '{"constraints": {"phids": ["'"$authorid"'"]}}' | \
571        arc_call_conduit -- user.search | \
572        jq -r '.response.data[].fields' > "$user_data"
573    user_addr=$(jq -r '.username' "$user_data")
574    user_name=$(jq -r '.realName' "$user_data")
575    # Dig the data out of querydiffs api endpoint, although it's deprecated,
576    # since it's one of the few places we can get email addresses. It's unclear
577    # if we can expect multiple difference ones of these. Some records don't
578    # have this data, so we remove all the 'null's. We sort the results and
579    # remove duplicates 'just to be sure' since we've not seen multiple
580    # records that match.
581    diff_data=$(xmktemp)
582    echo '{"revisionIDs": [ '"${diff#D}"' ]}' | \
583        arc_call_conduit -- differential.querydiffs |
584        jq -r '.response | flatten | .[]' > "$diff_data"
585    # If the differential revision has multiple revisions, just take the first
586    # non-null value we get.
587    author_addr=$(jq -r ".authorEmail?" "$diff_data" | grep -v '^null$' | head -n 1)
588    author_name=$(jq -r ".authorName?" "$diff_data" | grep -v '^null$' | head -n 1)
589
590    author=$(find_author "$user_addr" "$user_name" "$author_addr" "$author_name")
591
592    # If we had to guess, and the user didn't want to guess, abort
593    if [ "${author}" = "ABORT" ]; then
594        warn "Not committing due to uncertainty over author name"
595        exit 1
596    fi
597
598    tmp=$(xmktemp)
599    jq -r '.response.data[].fields.title' "$review_data" > "$tmp"
600    echo >> "$tmp"
601    jq -r '.response.data[].fields.summary' "$review_data" >> "$tmp"
602    echo >> "$tmp"
603    # XXX this leaves an extra newline in some cases.
604    reviewers=$(diff2reviewers "$diff" | sed '/^$/d' | paste -sd ',' - | sed 's/,/, /g')
605    if [ -n "$reviewers" ]; then
606        printf "Reviewed by:\t%s\n" "${reviewers}" >> "$tmp"
607    fi
608    # XXX TODO refactor with gitarc__stage maybe?
609    printf "Differential Revision:\thttps://reviews.freebsd.org/%s\n" "${diff}" >> "$tmp"
610    git commit --author "${author}" --file "$tmp"
611}
612
613gitarc__patch()
614{
615    local rev commit
616
617    if [ $# -eq 0 ]; then
618        err_usage
619    fi
620
621    commit=false
622    while getopts c o; do
623        case "$o" in
624        c)
625            require_clean_work_tree "patch -c"
626            commit=true
627            ;;
628        *)
629            err_usage
630            ;;
631        esac
632    done
633    shift $((OPTIND-1))
634
635    for rev in "$@"; do
636        arc patch --skip-dependencies --nocommit --nobranch --force "$rev"
637        echo "Applying ${rev}..."
638        [ $? -eq 0 ] || break
639        if ${commit}; then
640            patch_commit $rev
641        fi
642    done
643}
644
645gitarc__stage()
646{
647    local author branch commit commits diff reviewers title tmp
648
649    branch=main
650    while getopts b: o; do
651        case "$o" in
652        b)
653            branch="$OPTARG"
654            ;;
655        *)
656            err_usage
657            ;;
658        esac
659    done
660    shift $((OPTIND-1))
661
662    commits=$(build_commit_list "$@")
663
664    if [ "$branch" = "main" ]; then
665        git checkout -q main
666    else
667        git checkout -q -b "${branch}" main
668    fi
669
670    tmp=$(xmktemp)
671    for commit in $commits; do
672        git show -s --format=%B "$commit" > "$tmp"
673        title=$(git show -s --format=%s "$commit")
674        diff=$(title2diff "$title")
675        if [ -n "$diff" ]; then
676            # XXX this leaves an extra newline in some cases.
677            reviewers=$(diff2reviewers "$diff" | sed '/^$/d' | paste -sd ',' - | sed 's/,/, /g')
678            if [ -n "$reviewers" ]; then
679                printf "Reviewed by:\t%s\n" "${reviewers}" >> "$tmp"
680            fi
681            printf "Differential Revision:\thttps://reviews.freebsd.org/%s" "${diff}" >> "$tmp"
682        fi
683        author=$(git show -s --format='%an <%ae>' "${commit}")
684        if ! git cherry-pick --no-commit "${commit}"; then
685            warn "Failed to apply $(git rev-parse --short "${commit}").  Are you staging patches in the wrong order?"
686            git checkout -f
687            break
688        fi
689        git commit --edit --file "$tmp" --author "${author}"
690    done
691}
692
693gitarc__update()
694{
695    local commit commits diff doprompt have_msg list o msg
696
697    list=
698    if get_bool_config arc.list false; then
699        list=1
700    fi
701    doprompt=1
702    while getopts lm: o; do
703        case "$o" in
704        l)
705            list=1
706            ;;
707        m)
708            msg="$OPTARG"
709            have_msg=1
710            ;;
711        *)
712            err_usage
713            ;;
714        esac
715    done
716    shift $((OPTIND-1))
717
718    commits=$(build_commit_list "$@")
719
720    if [ "$list" ]; then
721        for commit in ${commits}; do
722            git --no-pager show --oneline --no-patch "$commit"
723        done | git_pager
724        if ! prompt; then
725            return
726        fi
727        doprompt=
728    fi
729
730    for commit in ${commits}; do
731        diff=$(commit2diff "$commit")
732
733        if [ "$doprompt" ] && ! show_and_prompt "$commit"; then
734            break
735        fi
736
737        # The linter is stupid and applies patches to the working copy.
738        # This would be tolerable if it didn't try to correct "misspelled" variable
739        # names.
740        if [ -n "$have_msg" ]; then
741            arc diff --message "$msg" --allow-untracked --never-apply-patches \
742                --update "$diff" --head "$commit" "${commit}~"
743        else
744            arc diff --allow-untracked --never-apply-patches --update "$diff" \
745                --head "$commit" "${commit}~"
746        fi
747    done
748}
749
750set -e
751
752ASSUME_YES=
753if get_bool_config arc.assume-yes false; then
754    ASSUME_YES=1
755fi
756
757VERBOSE=
758while getopts vy o; do
759    case "$o" in
760    v)
761        VERBOSE=1
762        ;;
763    y)
764        ASSUME_YES=1
765        ;;
766    *)
767        err_usage
768        ;;
769    esac
770done
771shift $((OPTIND-1))
772
773[ $# -ge 1 ] || err_usage
774
775which arc >/dev/null 2>&1 || err "arc is required, install devel/arcanist"
776which jq >/dev/null 2>&1 || err "jq is required, install textproc/jq"
777
778if [ "$VERBOSE" ]; then
779    exec 3>&1
780else
781    exec 3> /dev/null
782fi
783
784case "$1" in
785create|list|patch|stage|update)
786    ;;
787*)
788    err_usage
789    ;;
790esac
791verb=$1
792shift
793
794# All subcommands require at least one parameter.
795if [ $# -eq 0 ]; then
796    err_usage
797fi
798
799# Pull in some git helper functions.
800git_sh_setup=$(git --exec-path)/git-sh-setup
801[ -f "$git_sh_setup" ] || err "cannot find git-sh-setup"
802SUBDIRECTORY_OK=y
803USAGE=
804# shellcheck disable=SC1090
805. "$git_sh_setup"
806
807# git commands use GIT_EDITOR instead of EDITOR, so try to provide consistent
808# behaviour.  Ditto for PAGER.  This makes git-arc play nicer with editor
809# plugins like vim-fugitive.
810if [ -n "$GIT_EDITOR" ]; then
811    EDITOR=$GIT_EDITOR
812fi
813if [ -n "$GIT_PAGER" ]; then
814    PAGER=$GIT_PAGER
815fi
816
817# Bail if the working tree is unclean, except for "list" and "patch"
818# operations.
819case $verb in
820list|patch)
821    ;;
822*)
823    require_clean_work_tree "$verb"
824    ;;
825esac
826
827if get_bool_config arc.browse false; then
828    BROWSE=--browse
829fi
830
831GITARC_TMPDIR=$(mktemp -d) || exit 1
832trap cleanup EXIT HUP INT QUIT TRAP USR1 TERM
833
834gitarc__"${verb}" "$@"
835