git: f86c3e6de479 - main - git-arc: Handle -h on subcommands and align usage with the man page

From: Devin Teske <dteske_at_FreeBSD.org>
Date: Tue, 25 Aug 2026 20:48:48 UTC
The branch main has been updated by dteske:

URL: https://cgit.FreeBSD.org/src/commit/?id=f86c3e6de4795cb8b44d104635eb39e1f63209b4

commit f86c3e6de4795cb8b44d104635eb39e1f63209b4
Author:     Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-08-25 20:46:32 +0000
Commit:     Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-08-25 20:46:32 +0000

    git-arc: Handle -h on subcommands and align usage with the man page
    
    git-sh-setup treats -h as help against an empty USAGE, so
    "git arc create -h" prints "usage: git arc". Handle -h before
    sourcing it so every subcommand prints the real synopsis.
    
    The create, stage, and update synopses showed optional commit-refs
    while git-arc(1) and the code require them. Advertise -p parent on
    create; the option was already implemented and documented.
    
    Sort create sub-command option-arguments alphabetically in three
    places: (1) synopsis from tool, (2) man-page synopsis, and (3)
    man-page description.
    
    Check for jq(1) / arc after checking for usage so -h always works.
    
    While here, fix missing "local o" in gitarc__stage().
    
    Reviewed by:    markj
    Differential Revision:  https://reviews.freebsd.org/D59129
---
 tools/tools/git/git-arc.1  |  27 +++---
 tools/tools/git/git-arc.sh | 209 ++++++++++++++++++++++++++++++++-------------
 2 files changed, 165 insertions(+), 71 deletions(-)

diff --git a/tools/tools/git/git-arc.1 b/tools/tools/git/git-arc.1
index 218d852db984..d6b9ed686aaa 100644
--- a/tools/tools/git/git-arc.1
+++ b/tools/tools/git/git-arc.1
@@ -24,7 +24,7 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd August 20, 2026
+.Dd August 23, 2026
 .Dt GIT-ARC 1
 .Os
 .Sh NAME
@@ -32,28 +32,31 @@
 .Nd a wrapper to improve integration between git and arcanist
 .Sh SYNOPSIS
 .Nm
+.Fl h
+.Nm
 .Cm create
-.Op Fl dl
+.Op Fl dhl
+.Op Fl p Ar parent
 .Op Fl r Ar reviewer1 Ns Op Cm \&, Ns Ar reviewer2 ...
 .Op Fl s Ar subscriber1 Ns Op Cm \&, Ns Ar subscriber2 ...
 .Op Fl t Ar tag1 Ns Op Cm \&, Ns Ar tag2 ...
-.Op Fl p Ar parent
 .Ar commit-ref Op Ar commit-ref ...
 .Nm
-.Cm diff Ar commit-ref Op Ar commit-ref ...
+.Cm diff Oo Fl h Oc Ar commit-ref Op Ar commit-ref ...
 .Nm
-.Cm list Ar commit-ref Op Ar commit-ref ...
+.Cm list Oo Fl h Oc Ar commit-ref Op Ar commit-ref ...
 .Nm
 .Cm patch
-.Op Fl bcrs
+.Op Fl bchrs
 .Ar diff1 Op Cm Ar diff2 Op Ar ...
 .Nm
 .Cm stage
+.Op Fl h
 .Op Fl b Ar branch
 .Ar commit-ref Op Ar commit-ref ...
 .Nm
 .Cm update
-.Op Fl l
+.Op Fl hl
 .Op Fl m Ar message
 .Ar commit-ref Op Ar commit-ref ...
 .Sh DESCRIPTION
@@ -104,9 +107,15 @@ In this mode, notifications are not sent to reviewers and subscribers
 until the review is published via the web UI.
 The draft is still visible to anyone with the URL (or able to guess it),
 but the review's visibility settings can be modified before publishing.
+.It Fl h
+Print usage statement and exit.
 .It Fl l
 Before processing commit(s) display list of commits to be processed
 and wait for confirmation.
+.It Fl p Ar parent
+Specify the parent of the first commit in the list.
+This is useful when adding more commits on top of an already existing
+stack in Phabricator.
 .It Fl r Ar reviewer
 Add one or more reviewers, separated by commas, to revision(s) being created.
 Each argument must be an existing Phabricator user or group.
@@ -123,10 +132,6 @@ Unlike group reviewers, a leading
 is optional and is added if omitted.
 Spaces in tag names should be replaced with underscores
 .Pq e.g., Src_committers for the Do Src Committers Dc tag .
-.It Fl p Ar parent
-Specify the parent of the first commit in the list.
-This is useful when adding more commits on top of an already existing
-stack in Phabricator.
 .El
 .It Cm diff
 Show the differences between the specified commits and their associated
diff --git a/tools/tools/git/git-arc.sh b/tools/tools/git/git-arc.sh
index edaa7a62d846..07ce6539634a 100755
--- a/tools/tools/git/git-arc.sh
+++ b/tools/tools/git/git-arc.sh
@@ -55,15 +55,16 @@ cleanup()
 err_usage()
 {
     cat >&2 <<__EOF__
-Usage: git arc [-vy] <command> <arguments>
+Usage: git arc [-hvy] <command> <arguments>
 
 Commands:
-  create [-dl] [-r <reviewer1>[,<reviewer2>...]] [-s subscriber[,...]] [<commit>|<commit range>]
-  diff <commit>|<commit range>
-  list <commit>|<commit range>
-  patch [-bcrs] <diff1> [<diff2> ...]
-  stage [-b branch] [<commit>|<commit range>]
-  update [-l] [-m message] [<commit>|<commit range>]
+  create [-dhl] [-p parent] [-r <reviewer1>[,<reviewer2>...]] \\
+                [-s subscriber[,...]] [-t tag[,...]] <commit>|<commit range>
+  diff [-h] <commit>|<commit range>
+  list [-h] <commit>|<commit range>
+  patch [-bchrs] <diff1> [<diff2> ...]
+  stage [-h] [-b branch] <commit>|<commit range>
+  update [-hl] [-m message] <commit>|<commit range>
 
 See git-arc(1) for details.
 __EOF__
@@ -86,6 +87,65 @@ get_bool_config()
     test "$(git config --bool --get $1 2>/dev/null || echo $2)" != "false"
 }
 
+#
+# Per-subcommand initialization, invoked only after that sub-command's getopts
+# loop and arity check have succeeded. git-sh-setup treats leading -h as help
+# against empty USAGE, so must not be sourced until getopts has distinguished
+# a usage request from an option argument (for example, "create -t -h"). Same
+# delay skips jq(1) / arc checks when user only asked for usage.
+#
+# Invoke with no arguments because git-sh-setup inspects $1 (only for `-h')
+# and sourcing inherits this function's args (empty $1 keeps its help check
+# inert). Helpers it defines (require_clean_work_tree, git_pager) available
+# only after this returns.
+#
+setup()
+{
+    [ -x "${ARC_CMD}" ] || err "arc is required, install devel/arcanist-lib"
+    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
+
+    # 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=
+    # shellcheck disable=SC1090
+    . "$git_sh_setup"
+
+    # git commands use GIT_EDITOR instead of EDITOR, so try to provide consistent
+    # behaviour.  Ditto for PAGER.  This makes git-arc play nicer with editor
+    # plugins like vim-fugitive.
+    if [ -n "$GIT_EDITOR" ]; then
+        EDITOR=$GIT_EDITOR
+    fi
+    if [ -n "$GIT_PAGER" ]; then
+        PAGER=$GIT_PAGER
+    fi
+
+    # Bail if the working tree is unclean, except for "diff", "list" and
+    # "patch" operations.
+    case $verb in
+    diff|list|patch)
+        ;;
+    *)
+        require_clean_work_tree "$verb"
+        ;;
+    esac
+
+    if get_bool_config arc.browse false; then
+        BROWSE=--browse
+    fi
+
+    GITARC_TMPDIR=$(mktemp -d) || exit 1
+    trap cleanup EXIT HUP INT QUIT TRAP USR1 TERM
+}
+
 #
 # Invoke the actual arc command.  This allows us to only rely on the
 # devel/arcanist-lib port, which installs the actual script, rather than
@@ -411,6 +471,8 @@ build_commit_list()
 
 gitarc__create()
 {
+    local OPTIND=1 OPTARG
+    # NB: reviewers / subscribers / tags not initialized; inheritance allowed.
     local commit commits doprompt draft list o prev reviewers subscribers
     local tags
 
@@ -419,9 +481,8 @@ gitarc__create()
     if get_bool_config arc.list false; then
         list=1
     fi
-    doprompt=1
     draft=0
-    while getopts dlp:r:s:t: o; do
+    while getopts dhlp:r:s:t: o; do
         case "$o" in
         d)
             draft=1
@@ -448,8 +509,17 @@ gitarc__create()
     done
     shift $((OPTIND-1))
 
+    # NB: Earlier check of $# in main may have been duped by option-flags.
+    if [ $# -eq 0 ]; then
+        warn "Too few arguments"
+        err_usage
+    fi
+
+    setup
+
     commits=$(build_commit_list "$@")
 
+    doprompt=1
     if [ "$list" ]; then
         for commit in ${commits}; do
             git --no-pager show --oneline --no-patch "$commit"
@@ -479,8 +549,23 @@ gitarc__create()
 #
 gitarc__diff()
 {
+    local OPTIND=1 OPTARG o
     local commit commits diff rawdiff rtree
 
+    while getopts h o; do
+        case "$o" in
+        *)
+            err_usage
+            ;;
+        esac
+    done
+    shift $((OPTIND-1))
+
+    # NB: Earlier check of $# in main enough to ensure sufficient args.
+    # NB: If any option-flags besides -h are added, add re-check of $#.
+
+    setup
+
     commits=$(build_commit_list "$@")
 
     for commit in $commits; do
@@ -507,8 +592,23 @@ gitarc__diff()
 
 gitarc__list()
 {
+    local OPTIND=1 OPTARG o
     local chash commit commits diff openrevs title
 
+    while getopts h o; do
+        case "$o" in
+        *)
+            err_usage
+            ;;
+        esac
+    done
+    shift $((OPTIND-1))
+
+    # NB: Earlier check of $# in main enough to ensure sufficient args.
+    # NB: If any option-flags besides -h are added, add re-check of $#.
+
+    setup
+
     commits=$(build_commit_list "$@")
     openrevs=$(arc_list --ansi)
 
@@ -731,20 +831,19 @@ apply_rev()
 
 gitarc__patch()
 {
+    local OPTIND=1 OPTARG
     local branch commit o raw rev stack
 
     branch=false
     commit=false
     raw=false
     stack=false
-    while getopts bcrs o; do
+    while getopts bchrs o; do
         case "$o" in
         b)
-            require_clean_work_tree "patch -b"
             branch=true
             ;;
         c)
-            require_clean_work_tree "patch -c"
             commit=true
             ;;
         r)
@@ -760,13 +859,22 @@ gitarc__patch()
     done
     shift $((OPTIND-1))
 
+    # NB: Earlier check of $# in main may have been duped by option-flags.
     if [ $# -eq 0 ]; then
+        warn "Too few arguments"
         err_usage
     fi
 
+    setup
+
     if ${branch}; then
+        require_clean_work_tree "patch -b"
         patch_branch "$@"
     fi
+    if ${commit}; then
+        require_clean_work_tree "patch -c"
+    fi
+
     for rev in "$@"; do
         echo "Applying ${rev}..."
         apply_rev $rev $commit $raw $stack
@@ -775,10 +883,11 @@ gitarc__patch()
 
 gitarc__stage()
 {
+    local OPTIND=1 OPTARG o
     local author branch commit commits diff reviewers title tmp
 
     branch=main
-    while getopts b: o; do
+    while getopts b:h o; do
         case "$o" in
         b)
             branch="$OPTARG"
@@ -790,6 +899,14 @@ gitarc__stage()
     done
     shift $((OPTIND-1))
 
+    # NB: Earlier check of $# in main may have been duped by option-flags.
+    if [ $# -eq 0 ]; then
+        warn "Too few arguments"
+        err_usage
+    fi
+
+    setup
+
     commits=$(build_commit_list "$@")
 
     if [ "$branch" = "main" ]; then
@@ -823,14 +940,15 @@ gitarc__stage()
 
 gitarc__update()
 {
+    local OPTIND=1 OPTARG
+    # NB: msg / have_msg not initialized; inheritance allowed.
     local commit commits diff doprompt have_msg list o msg
 
     list=
     if get_bool_config arc.list false; then
         list=1
     fi
-    doprompt=1
-    while getopts lm: o; do
+    while getopts hlm: o; do
         case "$o" in
         l)
             list=1
@@ -846,8 +964,17 @@ gitarc__update()
     done
     shift $((OPTIND-1))
 
+    # NB: Earlier check of $# in main may have been duped by option-flags.
+    if [ $# -eq 0 ]; then
+        warn "Too few arguments"
+        err_usage
+    fi
+
+    setup
+
     commits=$(build_commit_list "$@")
 
+    doprompt=1
     if [ "$list" ]; then
         for commit in ${commits}; do
             git --no-pager show --oneline --no-patch "$commit"
@@ -886,7 +1013,7 @@ if get_bool_config arc.assume-yes false; then
 fi
 
 VERBOSE=
-while getopts vy o; do
+while getopts hvy o; do
     case "$o" in
     v)
         VERBOSE=1
@@ -901,21 +1028,17 @@ while getopts vy o; do
 done
 shift $((OPTIND-1))
 
-[ $# -ge 1 ] || err_usage
-
-[ -x "${ARC_CMD}" ] || err "arc is required, install devel/arcanist-lib"
-which jq >/dev/null 2>&1 || err "jq is required, install textproc/jq"
-
-if [ "$VERBOSE" ]; then
-    exec 3>&1
-else
-    exec 3> /dev/null
+# NB: Only catches if neither option-flags nor sub-command.
+if [ $# -eq 0 ]; then
+    warn "Too few arguments"
+    err_usage
 fi
 
 case "$1" in
 create|diff|list|patch|stage|update)
     ;;
 *)
+    warn "Unrecognized sub-command: $1"
     err_usage
     ;;
 esac
@@ -924,42 +1047,8 @@ shift
 
 # All subcommands require at least one parameter.
 if [ $# -eq 0 ]; then
+    warn "Too few arguments"
     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=
-# shellcheck disable=SC1090
-. "$git_sh_setup"
-
-# git commands use GIT_EDITOR instead of EDITOR, so try to provide consistent
-# behaviour.  Ditto for PAGER.  This makes git-arc play nicer with editor
-# plugins like vim-fugitive.
-if [ -n "$GIT_EDITOR" ]; then
-    EDITOR=$GIT_EDITOR
-fi
-if [ -n "$GIT_PAGER" ]; then
-    PAGER=$GIT_PAGER
-fi
-
-# Bail if the working tree is unclean, except for "diff", "list" and
-# "patch" operations.
-case $verb in
-diff|list|patch)
-    ;;
-*)
-    require_clean_work_tree "$verb"
-    ;;
-esac
-
-if get_bool_config arc.browse false; then
-    BROWSE=--browse
-fi
-
-GITARC_TMPDIR=$(mktemp -d) || exit 1
-trap cleanup EXIT HUP INT QUIT TRAP USR1 TERM
-
 gitarc__"${verb}" "$@"