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