]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - tools/tools/git/git-arc.sh
tools/git: ensure git-arc is more platform indepdendent
[FreeBSD/FreeBSD.git] / tools / tools / git / git-arc.sh
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
35 warn()
36 {
37     echo "$(basename "$0"): $1" >&2
38 }
39
40 err()
41 {
42     warn "$1"
43     exit 1
44 }
45
46 err_usage()
47 {
48     cat >&2 <<__EOF__
49 Usage: git arc [-vy] <command> <arguments>
50
51 Commands:
52   create [-l] [-r <reviewer1>[,<reviewer2>...]] [-s subscriber[,...]] [<commit>|<commit range>]
53   list <commit>|<commit range>
54   patch [-c] <diff1> [<diff2> ...]
55   stage [-b branch] [<commit>|<commit range>]
56   update [-m message] [<commit>|<commit range>]
57
58 Description:
59   Create or manage FreeBSD Phabricator reviews based on git commits.  There
60   is a one-to one relationship between git commits and Differential revisions,
61   and the Differential revision title must match the summary line of the
62   corresponding commit.  In particular, commit summaries must be unique across
63   all open Differential revisions authored by you.
64
65   The first parameter must be a verb.  The available verbs are:
66
67     create -- Create new Differential revisions from the specified commits.
68     list   -- Print the associated Differential revisions for the specified
69               commits.
70     patch  -- Try to apply a patch from a Differential revision to the
71               currently checked out tree.
72     stage  -- Prepare a series of commits to be pushed to the upstream FreeBSD
73               repository.  The commits are cherry-picked to a branch (main by
74               default), review tags are added to the commit log message, and
75               the log message is opened in an editor for any last-minute
76               updates.  The commits need not have associated Differential
77               revisions.
78     update -- Synchronize the Differential revisions associated with the
79               specified commits.  Currently only the diff is updated; the
80               review description and other metadata is not synchronized.
81
82   The typical end-to-end usage looks something like this:
83
84     $ git commit -m "kern: Rewrite in Rust"
85     $ git arc create HEAD
86     <Make changes to the diff based on reviewer feedback.>
87     $ git commit --amend
88     $ git arc update HEAD
89     <Now that all reviewers are happy, it's time to push.>
90     $ git arc stage HEAD
91     $ git push freebsd HEAD:main
92
93 Config Variables:
94   These are manipulated by git-config(1).
95
96     arc.assume_yes [bool]
97                        -- Assume a "yes" answer to all prompts instead of
98                           prompting the user.  Equivalent to the -y flag.
99
100     arc.browse [bool]  -- Try to open newly created reviews in a browser tab.
101                           Defaults to false.
102
103     arc.list [bool]    -- Always use "list mode" (-l) with create.  In this
104                           mode, the list of git revisions to create reviews for
105                           is listed with a single prompt before creating
106                           reviews.  The diffs for individual commits are not
107                           shown.
108
109     arc.verbose [bool] -- Verbose output.  Equivalent to the -v flag.
110
111 Examples:
112   Create a Phabricator review using the contents of the most recent commit in
113   your git checkout.  The commit title is used as the review title, the commit
114   log message is used as the review description, markj@FreeBSD.org is added as
115   a reviewer.
116
117   $ git arc create -r markj HEAD
118
119   Create a series of Phabricator reviews for each of HEAD~2, HEAD~ and HEAD.
120   Pairs of consecutive commits are linked into a patch stack.  Note that the
121   first commit in the specified range is excluded.
122
123   $ git arc create HEAD~3..HEAD
124
125   Update the review corresponding to commit b409afcfedcdda.  The title of the
126   commit must be the same as it was when the review was created.  The review
127   description is not automatically updated.
128
129   $ git arc update b409afcfedcdda
130
131   Apply the patch in review D12345 to the currently checked-out tree, and stage
132   it.
133
134   $ git arc patch D12345
135
136   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 #
156 arc_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 #
165 arc_list()
166 {
167     arc list "$@" | grep -v '^Warning: ' | sed -E 's/\x1b\[1m//g;s/\x1b\[m//g'
168 }
169
170 diff2phid()
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
184 diff2status()
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
202 log2diff()
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.
217 title2diff()
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
230 commit2diff()
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
257 create_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.
306 diff2reviewers()
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
328 prompt()
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
349 show_and_prompt()
350 {
351     local commit
352
353     commit=$1
354
355     git show "$commit"
356     prompt
357 }
358
359 build_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
375 gitarc__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
428 gitarc__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.
466 find_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
533 patch_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
590 gitarc__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
622 gitarc__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
670 gitarc__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
708 set -e
709
710 ASSUME_YES=
711 if [ "$(git config --bool --get arc.assume-yes 2>/dev/null || echo false)" != "false" ]; then
712     ASSUME_YES=1
713 fi
714
715 VERBOSE=
716 while 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
728 done
729 shift $((OPTIND-1))
730
731 [ $# -ge 1 ] || err_usage
732
733 which arc >/dev/null 2>&1 || err "arc is required, install devel/arcanist"
734 which jq >/dev/null 2>&1 || err "jq is required, install textproc/jq"
735
736 if [ "$VERBOSE" ]; then
737     exec 3>&1
738 else
739     exec 3> /dev/null
740 fi
741
742 case "$1" in
743 create|list|patch|stage|update)
744     ;;
745 *)
746     err_usage
747     ;;
748 esac
749 verb=$1
750 shift
751
752 # All subcommands require at least one parameter.
753 if [ $# -eq 0 ]; then
754     err_usage
755 fi
756
757 # Pull in some git helper functions.
758 git_sh_setup=$(git --exec-path)/git-sh-setup
759 [ -f "$git_sh_setup" ] || err "cannot find git-sh-setup"
760 SUBDIRECTORY_OK=y
761 USAGE=
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.
768 if [ -n "$GIT_EDITOR" ]; then
769     EDITOR=$GIT_EDITOR
770 fi
771 if [ -n "$GIT_PAGER" ]; then
772     PAGER=$GIT_PAGER
773 fi
774
775 # Bail if the working tree is unclean, except for "list" and "patch"
776 # operations.
777 case $verb in
778 list|patch)
779     ;;
780 *)
781     require_clean_work_tree "$verb"
782     ;;
783 esac
784
785 if [ "$(git config --bool --get arc.browse 2>/dev/null || echo false)" != "false" ]; then
786     BROWSE=--browse
787 fi
788
789 gitarc__"${verb}" "$@"