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