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