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