diff --git a/tools/tools/git/git-arc.sh b/tools/tools/git/git-arc.sh index 2de7e1858c00..07b042ceb151 100644 --- a/tools/tools/git/git-arc.sh +++ b/tools/tools/git/git-arc.sh @@ -1,595 +1,596 @@ #!/bin/sh # # SPDX-License-Identifier: BSD-2-Clause-FreeBSD # # Copyright (c) 2019-2021 Mark Johnston # Copyright (c) 2021 John Baldwin # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # TODO: # - roll back after errors or SIGINT # - created revs # - main (for git arc stage) warn() { echo "$(basename $0): $1" >&2 } err() { warn "$1" exit 1 } err_usage() { cat >&2 <<__EOF__ Usage: git arc [-vy] Commands: create [-l] [-r [,...]] [-s subscriber[,...]] [|] list | patch [ ...] stage [-b branch] [|] update Description: Create or manage FreeBSD Phabricator reviews based on git commits. There is a one-to one relationship between git commits and Differential revisions, and the Differential revision title must match the summary line of the corresponding commit. In particular, commit summaries must be unique across all open Differential revisions authored by you. The first parameter must be a verb. The available verbs are: create -- Create new Differential revisions from the specified commits. list -- Print the associated Differential revisions for the specified commits. patch -- Try to apply a patch from a Differential revision to the currently checked out tree. stage -- Prepare a series of commits to be pushed to the upstream FreeBSD repository. The commits are cherry-picked to a branch (main by default), review tags are added to the commit log message, and the log message is opened in an editor for any last-minute updates. The commits need not have associated Differential revisions. update -- Synchronize the Differential revisions associated with the specified commits. Currently only the diff is updated; the review description and other metadata is not synchronized. The typical end-to-end usage looks something like this: $ git commit -m "kern: Rewrite in Rust" $ git arc create HEAD $ git commit --amend $ git arc update HEAD $ git arc stage HEAD $ git push freebsd HEAD:main Config Variables: These are manipulated by git-config(1). arc.assume_yes [bool] -- Assume a "yes" answer to all prompts instead of prompting the user. Equivalent to the -y flag. arc.browse [bool] -- Try to open newly created reviews in a browser tab. Defaults to false. arc.list [bool] -- Always use "list mode" (-l) with create. In this mode, the list of git revisions to create reviews for is listed with a single prompt before creating reviews. The diffs for individual commits are not shown. arc.verbose [bool] -- Verbose output. Equivalent to the -v flag. Examples: Create a Phabricator review using the contents of the most recent commit in your git checkout. The commit title is used as the review title, the commit log message is used as the review description, markj@FreeBSD.org is added as a reviewer. $ git arc create -r markj HEAD Create a series of Phabricator reviews for each of HEAD~2, HEAD~ and HEAD. Pairs of consecutive commits are linked into a patch stack. Note that the first commit in the specified range is excluded. $ git arc create HEAD~3..HEAD Update the review corresponding to commit b409afcfedcdda. The title of the commit must be the same as it was when the review was created. The review description is not automatically updated. $ git arc update b409afcfedcdda Apply the patch in review D12345 to the currently checked-out tree, and stage it. $ git arc patch D12345 List the status of reviews for all the commits in the branch "feature": $ git arc list main..feature __EOF__ exit 1 } diff2phid() { local diff diff=$1 if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then err "invalid diff ID $diff" fi echo '{"names":["'$diff'"]}' | arc call-conduit -- phid.lookup | jq -r "select(.response != []) | .response.${diff}.phid" } diff2status() { local diff tmp status summary diff=$1 if ! expr "$diff" : 'D[1-9][0-9]*$' >/dev/null; then err "invalid diff ID $diff" fi tmp=$(mktemp) echo '{"names":["'$diff'"]}' | arc call-conduit -- phid.lookup > $tmp status=$(jq -r "select(.response != []) | .response.${diff}.status" < $tmp) summary=$(jq -r "select(.response != []) | .response.${diff}.fullName" < $tmp) printf "%-14s %s\n" "${status}" "${summary}" } log2diff() { local diff diff=$(git show -s --format=%B $commit | sed -nE '/^Differential Revision:[[:space:]]+(https:\/\/reviews.freebsd.org\/)?(D[0-9]+)$/{s//\2/;p;}') if [ -n "$diff" ] && [ $(echo "$diff" | wc -l) -eq 1 ]; then echo $diff else echo fi } commit2diff() { local commit diff title commit=$1 # First, look for a valid differential reference in the commit # log. diff=$(log2diff $commit) if [ -n "$diff" ]; then echo $diff return fi # Second, search the open reviews returned by 'arc list' looking # for a subject match. title=$(git show -s --format=%s $commit) diff=$(arc list | fgrep "$title" | egrep -o 'D[1-9][0-9]*:' | tr -d ':') if [ -z "$diff" ]; then err "could not find review for '${title}'" elif [ $(echo "$diff" | wc -l) -ne 1 ]; then err "found multiple reviews with the same title" fi echo $diff } create_one_review() { local childphid commit doprompt msg parent parentphid reviewers local subscribers commit=$1 reviewers=$2 subscribers=$3 parent=$4 doprompt=$5 if [ "$doprompt" ] && ! show_and_prompt $commit; then return 1 fi git checkout -q $commit msg=$(mktemp) git show -s --format='%B' $commit > $msg printf "\nTest Plan:\n" >> $msg printf "\nReviewers:\n" >> $msg printf "${reviewers}\n" >> $msg printf "\nSubscribers:\n" >> $msg printf "${subscribers}\n" >> $msg yes | env EDITOR=true \ arc diff --message-file $msg --never-apply-patches --create --allow-untracked $BROWSE HEAD~ [ $? -eq 0 ] || err "could not create Phabricator diff" if [ -n "$parent" ]; then diff=$(commit2diff $commit) [ -n "$diff" ] || err "failed to look up review ID for $commit" childphid=$(diff2phid $diff) parentphid=$(diff2phid $parent) echo '{ "objectIdentifier": "'${childphid}'", "transactions": [ { "type": "parents.add", "value": ["'${parentphid}'"] } ]}' | arc call-conduit -- differential.revision.edit >&3 fi rm -f $msg return 0 } # Get a list of reviewers who accepted the specified diff. diff2reviewers() { local diff phid reviewid userids diff=$1 reviewid=$(diff2phid $diff) userids=$( \ echo '{ "constraints": {"phids": ["'$reviewid'"]}, "attachments": {"reviewers": true} }' | arc call-conduit -- differential.revision.search | jq '.response.data[0].attachments.reviewers.reviewers[] | select(.status == "accepted").reviewerPHID') if [ -n "$userids" ]; then echo '{ "constraints": {"phids": ['$(echo -n $userids | tr '[:space:]' ',')']} }' | arc call-conduit -- user.search | jq -r '.response.data[].fields.username' fi } prompt() { local resp if [ "$ASSUME_YES" ]; then return 1 fi printf "\nDoes this look OK? [y/N] " read resp case $resp in [Yy]) return 0 ;; *) return 1 ;; esac } show_and_prompt() { local commit commit=$1 git show $commit prompt } save_head() { local orig if ! orig=$(git symbolic-ref --short -q HEAD); then orig=$(git show -s --pretty=%H HEAD) fi SAVED_HEAD=$orig } restore_head() { if [ -n "$SAVED_HEAD" ]; then git checkout -q $SAVED_HEAD SAVED_HEAD= fi } build_commit_list() { local chash _commits commits for chash in $@; do _commits=$(git rev-parse "${chash}") if ! git cat-file -e "${chash}"'^{commit}' >/dev/null 2>&1; then _commits=$(git rev-list $_commits | tail -r) fi [ -n "$_commits" ] || err "invalid commit ID ${chash}" commits="$commits $_commits" done echo $commits } gitarc::create() { local commit commits doprompt list o prev reviewers subscribers list= if eval $(git config --bool --default false --get arc.list); then list=1 fi doprompt=1 while getopts lr:s: o; do case "$o" in l) list=1 ;; r) reviewers="$OPTARG" ;; s) subscribers="$OPTARG" ;; *) err_usage ;; esac done shift $((OPTIND-1)) commits=$(build_commit_list $@) if [ "$list" ]; then for commit in ${commits}; do git --no-pager show --oneline --no-patch $commit done | git_pager if ! prompt; then return fi doprompt= fi save_head prev="" for commit in ${commits}; do if create_one_review "$commit" "$reviewers" "$subscribers" "$prev" \ "$doprompt"; then prev=$(commit2diff "$commit") else prev="" fi done restore_head } gitarc::list() { local chash commit commits diff title commits=$(build_commit_list $@) for commit in $commits; do chash=$(git show -s --format='%C(auto)%h' $commit) echo -n "${chash} " diff=$(log2diff $commit) if [ -n "$diff" ]; then diff2status $diff continue fi # This does not use commit2diff as it needs to handle errors # differently and keep the entire status. The extra 'cat' # after 'fgrep' avoids erroring due to -e. title=$(git show -s --format=%s $commit) diff=$(arc list | fgrep "$title" | cat) if [ -z "$diff" ]; then echo "No Review : $title" elif [ $(echo "$diff" | wc -l) -ne 1 ]; then echo -n "Ambiguous Reviews: " echo "$diff" | egrep -o 'D[1-9][0-9]*:' | tr -d ':' \ | paste -sd ',' - | sed 's/,/, /g' else echo "$diff" | sed -e 's/^[^ ]* *//' fi done } gitarc::patch() { local rev if [ $# -eq 0 ]; then err_usage fi for rev in $@; do arc patch --skip-dependencies --nocommit --nobranch --force $rev echo "Applying ${rev}..." [ $? -eq 0 ] || break done } gitarc::stage() { - local branch commit commits diff reviewers tmp + local author branch commit commits diff reviewers tmp branch=main while getopts b: o; do case "$o" in b) branch="$OPTARG" ;; *) err_usage ;; esac done shift $((OPTIND-1)) commits=$(build_commit_list $@) if [ "$branch" = "main" ]; then git checkout -q main else git checkout -q -b ${branch} main fi tmp=$(mktemp) for commit in $commits; do git show -s --format=%B $commit > $tmp diff=$(arc list | fgrep "$(git show -s --format=%s $commit)" | egrep -o 'D[1-9][0-9]*:' | tr -d ':') if [ -n "$diff" ]; then # XXX this leaves an extra newline in some cases. reviewers=$(diff2reviewers $diff | sed '/^$/d' | paste -sd ',' - | sed 's/,/, /g') if [ -n "$reviewers" ]; then printf "Reviewed by:\t${reviewers}\n" >> $tmp fi printf "Differential Revision:\thttps://reviews.freebsd.org/${diff}" >> $tmp fi + author=$(git show -s --format='%an <%ae>' ${commit}) if ! git cherry-pick --no-commit ${commit}; then warn "Failed to apply $(git rev-parse --short ${commit}). Are you staging patches in the wrong order?" git checkout -f break fi - git commit --edit --file $tmp + git commit --edit --file $tmp --author "${author}" done } gitarc::update() { local commit diff commit=$1 diff=$(commit2diff $commit) if ! show_and_prompt $commit; then return fi save_head git checkout -q $commit # The linter is stupid and applies patches to the working copy. # This would be tolerable if it didn't try to correct "misspelled" variable # names. arc diff --allow-untracked --never-apply-patches --update $diff HEAD~ restore_head } set -e ASSUME_YES= if eval $(git config --bool --default false --get arc.assume-yes); then ASSUME_YES=1 fi VERBOSE= while getopts vy o; do case "$o" in v) VERBOSE=1 ;; y) ASSUME_YES=1 ;; *) err_usage ;; esac done shift $((OPTIND-1)) [ $# -ge 1 ] || err_usage which arc >/dev/null 2>&1 || err "arc is required, install devel/arcanist" which jq >/dev/null 2>&1 || err "jq is required, install textproc/jq" if [ "$VERBOSE" ]; then exec 3>&1 else exec 3> /dev/null fi case "$1" in create|list|patch|stage|update) ;; *) err_usage ;; esac verb=$1 shift # All subcommands require at least one parameter. if [ $# -eq 0 ]; then err_usage fi # Pull in some git helper functions. git_sh_setup=$(git --exec-path)/git-sh-setup [ -f "$git_sh_setup" ] || err "cannot find git-sh-setup" SUBDIRECTORY_OK=y USAGE= . "$git_sh_setup" # Bail if the working tree is unclean, except for "list" and "patch" # operations. case $verb in list|patch) ;; *) require_clean_work_tree $verb ;; esac if eval $(git config --bool --default false --get arc.browse); then BROWSE=--browse fi trap restore_head EXIT INT gitarc::${verb} $@