3 # SPDX-License-Identifier: BSD-2-Clause-FreeBSD
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 <diff1> [<diff2> ...]
55 stage [-b branch] [<commit>|<commit range>]
56 update [<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 List the status of reviews for all the commits in the branch "feature":
138 $ git arc list main..feature
150 if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then
151 err "invalid diff ID $diff"
154 echo '{"names":["'"$diff"'"]}' |
155 arc call-conduit -- phid.lookup |
156 jq -r "select(.response != []) | .response.${diff}.phid"
161 local diff tmp status summary
164 if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then
165 err "invalid diff ID $diff"
169 echo '{"names":["'"$diff"'"]}' |
170 arc call-conduit -- phid.lookup > "$tmp"
171 status=$(jq -r "select(.response != []) | .response.${diff}.status" < "$tmp")
172 summary=$(jq -r "select(.response != []) |
173 .response.${diff}.fullName" < "$tmp")
174 printf "%-14s %s\n" "${status}" "${summary}"
181 diff=$(git show -s --format=%B "$commit" |
182 sed -nE '/^Differential Revision:[[:space:]]+(https:\/\/reviews.freebsd.org\/)?(D[0-9]+)$/{s//\2/;p;}')
183 if [ -n "$diff" ] && [ "$(echo "$diff" | wc -l)" -eq 1 ]; then
192 local commit diff title
196 # First, look for a valid differential reference in the commit
198 diff=$(log2diff "$commit")
199 if [ -n "$diff" ]; then
204 # Second, search the open reviews returned by 'arc list' looking
205 # for a subject match.
206 title=$(git show -s --format=%s "$commit")
207 diff=$(arc list | grep -F "$title" | grep -E -o 'D[1-9][0-9]*:' | tr -d ':')
208 if [ -z "$diff" ]; then
209 err "could not find review for '${title}'"
210 elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
211 err "found multiple reviews with the same title"
219 local childphid commit doprompt msg parent parentphid reviewers
228 if [ "$doprompt" ] && ! show_and_prompt "$commit"; then
232 git checkout -q "$commit"
235 git show -s --format='%B' "$commit" > "$msg"
236 printf "\nTest Plan:\n" >> "$msg"
237 printf "\nReviewers:\n" >> "$msg"
238 printf "%s\n" "${reviewers}" >> "$msg"
239 printf "\nSubscribers:\n" >> "$msg"
240 printf "%s\n" "${subscribers}" >> "$msg"
242 yes | env EDITOR=true \
243 arc diff --message-file "$msg" --never-apply-patches --create --allow-untracked $BROWSE HEAD~
244 [ $? -eq 0 ] || err "could not create Phabricator diff"
246 if [ -n "$parent" ]; then
247 diff=$(commit2diff "$commit")
248 [ -n "$diff" ] || err "failed to look up review ID for $commit"
250 childphid=$(diff2phid "$diff")
251 parentphid=$(diff2phid "$parent")
253 "objectIdentifier": "'"${childphid}"'",
256 "type": "parents.add",
257 "value": ["'"${parentphid}"'"]
260 arc call-conduit -- differential.revision.edit >&3
266 # Get a list of reviewers who accepted the specified diff.
269 local diff reviewid userids
272 reviewid=$(diff2phid "$diff")
275 "constraints": {"phids": ["'"$reviewid"'"]},
276 "attachments": {"reviewers": true}
278 arc call-conduit -- differential.revision.search |
279 jq '.response.data[0].attachments.reviewers.reviewers[] | select(.status == "accepted").reviewerPHID')
280 if [ -n "$userids" ]; then
282 "constraints": {"phids": ['"$(echo -n "$userids" | tr '[:space:]' ',')"']}
284 arc call-conduit -- user.search |
285 jq -r '.response.data[].fields.username'
293 if [ "$ASSUME_YES" ]; then
297 printf "\nDoes this look OK? [y/N] "
324 if ! orig=$(git symbolic-ref --short -q HEAD); then
325 orig=$(git show -s --pretty=%H HEAD)
332 if [ -n "$SAVED_HEAD" ]; then
333 git checkout -q "$SAVED_HEAD"
340 local chash _commits commits
342 for chash in "$@"; do
343 _commits=$(git rev-parse "${chash}")
344 if ! git cat-file -e "${chash}"'^{commit}' >/dev/null 2>&1; then
345 # shellcheck disable=SC2086
346 _commits=$(git rev-list $_commits | tail -r)
348 [ -n "$_commits" ] || err "invalid commit ID ${chash}"
349 commits="$commits $_commits"
356 local commit commits doprompt list o prev reviewers subscribers
360 if [ "$(git config --bool --get arc.list 2>/dev/null || echo false)" != "false" ]; then
364 while getopts lp:r:s: o; do
376 subscribers="$OPTARG"
385 commits=$(build_commit_list "$@")
388 for commit in ${commits}; do
389 git --no-pager show --oneline --no-patch "$commit"
398 for commit in ${commits}; do
399 if create_one_review "$commit" "$reviewers" "$subscribers" "$prev" \
401 prev=$(commit2diff "$commit")
411 local chash commit commits diff title
413 commits=$(build_commit_list "$@")
415 for commit in $commits; do
416 chash=$(git show -s --format='%C(auto)%h' "$commit")
419 diff=$(log2diff "$commit")
420 if [ -n "$diff" ]; then
425 # This does not use commit2diff as it needs to handle errors
426 # differently and keep the entire status. The extra 'cat'
427 # after 'fgrep' avoids erroring due to -e.
428 title=$(git show -s --format=%s "$commit")
429 diff=$(arc list | grep -F "$title" | cat)
430 if [ -z "$diff" ]; then
431 echo "No Review : $title"
432 elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
433 echo -n "Ambiguous Reviews: "
434 echo "$diff" | grep -E -o 'D[1-9][0-9]*:' | tr -d ':' \
435 | paste -sd ',' - | sed 's/,/, /g'
437 echo "$diff" | sed -e 's/^[^ ]* *//'
446 if [ $# -eq 0 ]; then
451 arc patch --skip-dependencies --nocommit --nobranch --force "$rev"
452 echo "Applying ${rev}..."
453 [ $? -eq 0 ] || break
459 local author branch commit commits diff reviewers tmp
462 while getopts b: o; do
474 commits=$(build_commit_list "$@")
476 if [ "$branch" = "main" ]; then
479 git checkout -q -b "${branch}" main
483 for commit in $commits; do
484 git show -s --format=%B "$commit" > "$tmp"
485 diff=$(arc list | grep -F "$(git show -s --format=%s "$commit")" |
486 grep -E -o 'D[1-9][0-9]*:' | tr -d ':')
487 if [ -n "$diff" ]; then
488 # XXX this leaves an extra newline in some cases.
489 reviewers=$(diff2reviewers "$diff" | sed '/^$/d' | paste -sd ',' - | sed 's/,/, /g')
490 if [ -n "$reviewers" ]; then
491 printf "Reviewed by:\t%s\n" "${reviewers}" >> "$tmp"
493 printf "Differential Revision:\thttps://reviews.freebsd.org/%s" "${diff}" >> "$tmp"
495 author=$(git show -s --format='%an <%ae>' "${commit}")
496 if ! git cherry-pick --no-commit "${commit}"; then
497 warn "Failed to apply $(git rev-parse --short "${commit}"). Are you staging patches in the wrong order?"
501 git commit --edit --file "$tmp" --author "${author}"
507 local commit commits diff
509 commits=$(build_commit_list "$@")
511 for commit in ${commits}; do
512 diff=$(commit2diff "$commit")
514 if ! show_and_prompt "$commit"; then
518 git checkout -q "$commit"
520 # The linter is stupid and applies patches to the working copy.
521 # This would be tolerable if it didn't try to correct "misspelled" variable
523 arc diff --allow-untracked --never-apply-patches --update "$diff" HEAD~
531 if [ "$(git config --bool --get arc.assume-yes 2>/dev/null || echo false)" != "false" ]; then
536 while getopts vy o; do
551 [ $# -ge 1 ] || err_usage
553 which arc >/dev/null 2>&1 || err "arc is required, install devel/arcanist"
554 which jq >/dev/null 2>&1 || err "jq is required, install textproc/jq"
556 if [ "$VERBOSE" ]; then
563 create|list|patch|stage|update)
572 # All subcommands require at least one parameter.
573 if [ $# -eq 0 ]; then
577 # Pull in some git helper functions.
578 git_sh_setup=$(git --exec-path)/git-sh-setup
579 [ -f "$git_sh_setup" ] || err "cannot find git-sh-setup"
582 # shellcheck disable=SC1090
585 # Bail if the working tree is unclean, except for "list" and "patch"
591 require_clean_work_tree "$verb"
595 if [ "$(git config --bool --get arc.browse 2>/dev/null || echo false)" != "false" ]; then
599 trap restore_head EXIT INT
601 gitarc__"${verb}" "$@"