3 # SPDX-License-Identifier: BSD-2-Clause
5 # Copyright (c) 2019-2021 Mark Johnston <markj@FreeBSD.org>
6 # Copyright (c) 2021 John Baldwin <jhb@FreeBSD.org>
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions are
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.
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
31 # - roll back after errors or SIGINT
33 # - main (for git arc stage)
37 echo "$(basename "$0"): $1" >&2
49 Usage: git arc [-vy] <command> <arguments>
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>]
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.
65 The first parameter must be a verb. The available verbs are:
67 create -- Create new Differential revisions from the specified commits.
68 list -- Print the associated Differential revisions for the specified
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
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.
82 The typical end-to-end usage looks something like this:
84 $ git commit -m "kern: Rewrite in Rust"
86 <Make changes to the diff based on reviewer feedback.>
89 <Now that all reviewers are happy, it's time to push.>
91 $ git push freebsd HEAD:main
94 These are manipulated by git-config(1).
97 -- Assume a "yes" answer to all prompts instead of
98 prompting the user. Equivalent to the -y flag.
100 arc.browse [bool] -- Try to open newly created reviews in a browser tab.
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
109 arc.verbose [bool] -- Verbose output. Equivalent to the -v flag.
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
117 $ git arc create -r markj HEAD
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.
123 $ git arc create HEAD~3..HEAD
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.
129 $ git arc update b409afcfedcdda
131 Apply the patch in review D12345 to the currently checked-out tree, and stage
134 $ git arc patch D12345
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.
139 $ git arc patch -c D12345
141 List the status of reviews for all the commits in the branch "feature":
143 $ git arc list main..feature
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.
158 arc call-conduit "$@" | grep -v '^Warning: '
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).
167 arc list "$@" | grep -v '^Warning: ' | sed -E 's/\x1b\[1m//g;s/\x1b\[m//g'
175 if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then
176 err "invalid diff ID $diff"
179 echo '{"names":["'"$diff"'"]}' |
180 arc_call_conduit -- phid.lookup |
181 jq -r "select(.response != []) | .response.${diff}.phid"
186 local diff tmp status summary
189 if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then
190 err "invalid diff ID $diff"
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}"
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
215 # Look for an open revision with a title equal to the input string. Return
216 # a possibly empty list of Differential revision IDs.
221 title=$(echo $1 | sed 's/"/\\"/g')
224 if (substr($0, index($0, FS) + length(FS)) == "'"$title"'") {
225 print substr($1, match($1, "D[1-9][0-9]*"))
232 local commit diff title
236 # First, look for a valid differential reference in the commit
238 diff=$(log2diff "$commit")
239 if [ -n "$diff" ]; then
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"
259 local childphid commit doprompt msg parent parentphid reviewers
268 if [ "$doprompt" ] && ! show_and_prompt "$commit"; then
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"
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"
285 if [ -n "$parent" ]; then
286 diff=$(commit2diff "$commit")
287 [ -n "$diff" ] || err "failed to look up review ID for $commit"
289 childphid=$(diff2phid "$diff")
290 parentphid=$(diff2phid "$parent")
292 "objectIdentifier": "'"${childphid}"'",
295 "type": "parents.add",
296 "value": ["'"${parentphid}"'"]
299 arc_call_conduit -- differential.revision.edit >&3
305 # Get a list of reviewers who accepted the specified diff.
308 local diff reviewid userids
311 reviewid=$(diff2phid "$diff")
314 "constraints": {"phids": ["'"$reviewid"'"]},
315 "attachments": {"reviewers": true}
317 arc_call_conduit -- differential.revision.search |
318 jq '.response.data[0].attachments.reviewers.reviewers[] | select(.status == "accepted").reviewerPHID')
319 if [ -n "$userids" ]; then
321 "constraints": {"phids": ['"$(echo -n "$userids" | tr '[:space:]' ',')"']}
323 arc_call_conduit -- user.search |
324 jq -r '.response.data[].fields.username'
332 if [ "$ASSUME_YES" ]; then
336 printf "\nDoes this look OK? [y/N] "
361 local chash _commits commits
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)
369 [ -n "$_commits" ] || err "invalid commit ID ${chash}"
370 commits="$commits $_commits"
377 local commit commits doprompt list o prev reviewers subscribers
381 if [ "$(git config --bool --get arc.list 2>/dev/null || echo false)" != "false" ]; then
385 while getopts lp:r:s: o; do
397 subscribers="$OPTARG"
406 commits=$(build_commit_list "$@")
409 for commit in ${commits}; do
410 git --no-pager show --oneline --no-patch "$commit"
418 for commit in ${commits}; do
419 if create_one_review "$commit" "$reviewers" "$subscribers" "$prev" \
421 prev=$(commit2diff "$commit")
430 local chash commit commits diff openrevs title
432 commits=$(build_commit_list "$@")
433 openrevs=$(arc_list --ansi)
435 for commit in $commits; do
436 chash=$(git show -s --format='%C(auto)%h' "$commit")
439 diff=$(log2diff "$commit")
440 if [ -n "$diff" ]; then
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'
458 echo "$diff" | sed -e 's/^[^ ]* *//'
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.
468 local addr name email author_addr author_name
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.
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
484 *.*) ;; # external user
486 echo "${name} <${addr}@FreeBSD.org>"
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}>"
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
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
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}"
535 local diff reviewid review_data authorid user_data user_addr user_name author
536 local tmp author_addr author_name
539 reviewid=$(diff2phid "$diff")
540 # Get the author phid for this patch
541 review_data=$(echo '{
542 "constraints": {"phids": ["'"$reviewid"'"]}
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
548 "constraints": {"phids": ["'"$authorid"'"]}
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.
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")
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"
575 echo "$review_data" | jq -r '.response.data[].fields.title' > $tmp
577 echo "$review_data" | jq -r '.response.data[].fields.summary' >> $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"
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"
594 if [ $# -eq 0 ]; then
599 while getopts c o; do
602 require_clean_work_tree "patch -c"
613 arc patch --skip-dependencies --nocommit --nobranch --force "$rev"
614 echo "Applying ${rev}..."
615 [ $? -eq 0 ] || break
624 local author branch commit commits diff reviewers title tmp
627 while getopts b: o; do
639 commits=$(build_commit_list "$@")
641 if [ "$branch" = "main" ]; then
644 git checkout -q -b "${branch}" main
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"
658 printf "Differential Revision:\thttps://reviews.freebsd.org/%s" "${diff}" >> "$tmp"
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?"
666 git commit --edit --file "$tmp" --author "${author}"
672 local commit commits diff have_msg msg
674 while getopts m: o; do
687 commits=$(build_commit_list "$@")
688 for commit in ${commits}; do
689 diff=$(commit2diff "$commit")
691 if ! show_and_prompt "$commit"; then
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
698 if [ -n "$have_msg" ]; then
699 arc diff --message "$msg" --allow-untracked --never-apply-patches \
700 --update "$diff" --head "$commit" "${commit}~"
702 arc diff --allow-untracked --never-apply-patches --update "$diff" \
703 --head "$commit" "${commit}~"
711 if [ "$(git config --bool --get arc.assume-yes 2>/dev/null || echo false)" != "false" ]; then
716 while getopts vy o; do
731 [ $# -ge 1 ] || err_usage
733 which arc >/dev/null 2>&1 || err "arc is required, install devel/arcanist"
734 which jq >/dev/null 2>&1 || err "jq is required, install textproc/jq"
736 if [ "$VERBOSE" ]; then
743 create|list|patch|stage|update)
752 # All subcommands require at least one parameter.
753 if [ $# -eq 0 ]; then
757 # Pull in some git helper functions.
758 git_sh_setup=$(git --exec-path)/git-sh-setup
759 [ -f "$git_sh_setup" ] || err "cannot find git-sh-setup"
762 # shellcheck disable=SC1090
765 # git commands use GIT_EDITOR instead of EDITOR, so try to provide consistent
766 # behaviour. Ditto for PAGER. This makes git-arc play nicer with editor
767 # plugins like vim-fugitive.
768 if [ -n "$GIT_EDITOR" ]; then
771 if [ -n "$GIT_PAGER" ]; then
775 # Bail if the working tree is unclean, except for "list" and "patch"
781 require_clean_work_tree "$verb"
785 if [ "$(git config --bool --get arc.browse 2>/dev/null || echo false)" != "false" ]; then
789 gitarc__"${verb}" "$@"