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