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