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
190 # Look for an open revision with a title equal to the input string. Return
191 # a possibly empty list of Differential revision IDs.
197 # arc list output always includes ANSI escape sequences, strip them.
198 arc list | sed 's/\x1b\[[0-9;]*m//g' | \
200 if (substr($0, index($0, FS) + length(FS)) == "'"$title"'") {
201 print substr($1, match($1, "D[1-9][0-9]*"))
208 local commit diff title
212 # First, look for a valid differential reference in the commit
214 diff=$(log2diff "$commit")
215 if [ -n "$diff" ]; then
220 # Second, search the open reviews returned by 'arc list' looking
221 # for a subject match.
222 title=$(git show -s --format=%s "$commit")
223 diff=$(title2diff "$title")
224 if [ -z "$diff" ]; then
225 err "could not find review for '${title}'"
226 elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
227 err "found multiple reviews with the same title"
235 local childphid commit doprompt msg parent parentphid reviewers
244 if [ "$doprompt" ] && ! show_and_prompt "$commit"; then
249 git show -s --format='%B' "$commit" > "$msg"
250 printf "\nTest Plan:\n" >> "$msg"
251 printf "\nReviewers:\n" >> "$msg"
252 printf "%s\n" "${reviewers}" >> "$msg"
253 printf "\nSubscribers:\n" >> "$msg"
254 printf "%s\n" "${subscribers}" >> "$msg"
256 yes | env EDITOR=true \
257 arc diff --message-file "$msg" --never-apply-patches --create \
258 --allow-untracked $BROWSE --head "$commit" "${commit}~"
259 [ $? -eq 0 ] || err "could not create Phabricator diff"
261 if [ -n "$parent" ]; then
262 diff=$(commit2diff "$commit")
263 [ -n "$diff" ] || err "failed to look up review ID for $commit"
265 childphid=$(diff2phid "$diff")
266 parentphid=$(diff2phid "$parent")
268 "objectIdentifier": "'"${childphid}"'",
271 "type": "parents.add",
272 "value": ["'"${parentphid}"'"]
275 arc call-conduit -- differential.revision.edit >&3
281 # Get a list of reviewers who accepted the specified diff.
284 local diff reviewid userids
287 reviewid=$(diff2phid "$diff")
290 "constraints": {"phids": ["'"$reviewid"'"]},
291 "attachments": {"reviewers": true}
293 arc call-conduit -- differential.revision.search |
294 jq '.response.data[0].attachments.reviewers.reviewers[] | select(.status == "accepted").reviewerPHID')
295 if [ -n "$userids" ]; then
297 "constraints": {"phids": ['"$(echo -n "$userids" | tr '[:space:]' ',')"']}
299 arc call-conduit -- user.search |
300 jq -r '.response.data[].fields.username'
308 if [ "$ASSUME_YES" ]; then
312 printf "\nDoes this look OK? [y/N] "
337 local chash _commits commits
339 for chash in "$@"; do
340 _commits=$(git rev-parse "${chash}")
341 if ! git cat-file -e "${chash}"'^{commit}' >/dev/null 2>&1; then
342 # shellcheck disable=SC2086
343 _commits=$(git rev-list $_commits | tail -r)
345 [ -n "$_commits" ] || err "invalid commit ID ${chash}"
346 commits="$commits $_commits"
353 local commit commits doprompt list o prev reviewers subscribers
357 if [ "$(git config --bool --get arc.list 2>/dev/null || echo false)" != "false" ]; then
361 while getopts lp:r:s: o; do
373 subscribers="$OPTARG"
382 commits=$(build_commit_list "$@")
385 for commit in ${commits}; do
386 git --no-pager show --oneline --no-patch "$commit"
394 for commit in ${commits}; do
395 if create_one_review "$commit" "$reviewers" "$subscribers" "$prev" \
397 prev=$(commit2diff "$commit")
406 local chash commit commits diff openrevs title
408 commits=$(build_commit_list "$@")
411 for commit in $commits; do
412 chash=$(git show -s --format='%C(auto)%h' "$commit")
415 diff=$(log2diff "$commit")
416 if [ -n "$diff" ]; then
421 # This does not use commit2diff as it needs to handle errors
422 # differently and keep the entire status.
423 title=$(git show -s --format=%s "$commit")
424 diff=$(echo "$openrevs" | \
425 awk -F'D[1-9][0-9]*:\.\\[m ' '{if ($2 == "'"$title"'") print $0}')
426 if [ -z "$diff" ]; then
427 echo "No Review : $title"
428 elif [ "$(echo "$diff" | wc -l)" -ne 1 ]; then
429 echo -n "Ambiguous Reviews: "
430 echo "$diff" | grep -E -o 'D[1-9][0-9]*:' | tr -d ':' \
431 | paste -sd ',' - | sed 's/,/, /g'
433 echo "$diff" | sed -e 's/^[^ ]* *//'
442 if [ $# -eq 0 ]; then
447 arc patch --skip-dependencies --nocommit --nobranch --force "$rev"
448 echo "Applying ${rev}..."
449 [ $? -eq 0 ] || break
455 local author branch commit commits diff reviewers title tmp
458 while getopts b: o; do
470 commits=$(build_commit_list "$@")
472 if [ "$branch" = "main" ]; then
475 git checkout -q -b "${branch}" main
479 for commit in $commits; do
480 git show -s --format=%B "$commit" > "$tmp"
481 title=$(git show -s --format=%s "$commit")
482 diff=$(title2diff "$title")
483 if [ -n "$diff" ]; then
484 # XXX this leaves an extra newline in some cases.
485 reviewers=$(diff2reviewers "$diff" | sed '/^$/d' | paste -sd ',' - | sed 's/,/, /g')
486 if [ -n "$reviewers" ]; then
487 printf "Reviewed by:\t%s\n" "${reviewers}" >> "$tmp"
489 printf "Differential Revision:\thttps://reviews.freebsd.org/%s" "${diff}" >> "$tmp"
491 author=$(git show -s --format='%an <%ae>' "${commit}")
492 if ! git cherry-pick --no-commit "${commit}"; then
493 warn "Failed to apply $(git rev-parse --short "${commit}"). Are you staging patches in the wrong order?"
497 git commit --edit --file "$tmp" --author "${author}"
503 local commit commits diff
505 commits=$(build_commit_list "$@")
506 for commit in ${commits}; do
507 diff=$(commit2diff "$commit")
509 if ! show_and_prompt "$commit"; then
513 # The linter is stupid and applies patches to the working copy.
514 # This would be tolerable if it didn't try to correct "misspelled" variable
516 arc diff --allow-untracked --never-apply-patches --update "$diff" \
517 --head "$commit" "${commit}~"
524 if [ "$(git config --bool --get arc.assume-yes 2>/dev/null || echo false)" != "false" ]; then
529 while getopts vy o; do
544 [ $# -ge 1 ] || err_usage
546 which arc >/dev/null 2>&1 || err "arc is required, install devel/arcanist"
547 which jq >/dev/null 2>&1 || err "jq is required, install textproc/jq"
549 if [ "$VERBOSE" ]; then
556 create|list|patch|stage|update)
565 # All subcommands require at least one parameter.
566 if [ $# -eq 0 ]; then
570 # Pull in some git helper functions.
571 git_sh_setup=$(git --exec-path)/git-sh-setup
572 [ -f "$git_sh_setup" ] || err "cannot find git-sh-setup"
575 # shellcheck disable=SC1090
578 # git commands use GIT_EDITOR instead of EDITOR, so try to provide consistent
579 # behaviour. Ditto for PAGER. This makes git-arc play nicer with editor
580 # plugins like vim-fugitive.
581 if [ -n "$GIT_EDITOR" ]; then
584 if [ -n "$GIT_PAGER" ]; then
588 # Bail if the working tree is unclean, except for "list" and "patch"
594 require_clean_work_tree "$verb"
598 if [ "$(git config --bool --get arc.browse 2>/dev/null || echo false)" != "false" ]; then
602 gitarc__"${verb}" "$@"