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