1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-05-09 09:06:57 +02:00

Merge branch 'jk/complete-git-switch'

The command line completion (in contrib/) learned to complete
options that the "git switch" command takes.

* jk/complete-git-switch:
  completion: improve handling of --orphan option of switch/checkout
  completion: improve handling of -c/-C and -b/-B in switch/checkout
  completion: improve handling of --track in switch/checkout
  completion: improve handling of --detach in checkout
  completion: improve completion for git switch with no options
  completion: improve handling of DWIM mode for switch/checkout
  completion: perform DWIM logic directly in __git_complete_refs
  completion: extract function __git_dwim_remote_heads
  completion: replace overloaded track term for __git_complete_refs
  completion: add tests showing subpar switch/checkout --orphan logic
  completion: add tests showing subpar -c/C argument completion
  completion: add tests showing subpar -c/-C startpoint completion
  completion: add tests showing subpar switch/checkout --track logic
  completion: add tests showing subar checkout --detach logic
  completion: add tests showing subpar DWIM logic for switch/checkout
  completion: add test showing subpar git switch completion
This commit is contained in:
Junio C Hamano 2020-06-25 12:27:45 -07:00
commit 320421840e
2 changed files with 668 additions and 39 deletions

View File

@ -301,6 +301,19 @@ __gitcomp_direct ()
COMPREPLY=($1)
}
# Similar to __gitcomp_direct, but appends to COMPREPLY instead.
# Callers must take care of providing only words that match the current word
# to be completed and adding any prefix and/or suffix (trailing space!), if
# necessary.
# 1: List of newline-separated matching completion words, complete with
# prefix and suffix.
__gitcomp_direct_append ()
{
local IFS=$'\n'
COMPREPLY+=($1)
}
__gitcompappend ()
{
local x i=${#COMPREPLY[@]}
@ -611,6 +624,19 @@ __git_heads ()
"refs/heads/$cur_*" "refs/heads/$cur_*/**"
}
# Lists branches from remote repositories.
# 1: A prefix to be added to each listed branch (optional).
# 2: List only branches matching this word (optional; list all branches if
# unset or empty).
# 3: A suffix to be appended to each listed branch (optional).
__git_remote_heads ()
{
local pfx="${1-}" cur_="${2-}" sfx="${3-}"
__git for-each-ref --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \
"refs/remotes/$cur_*" "refs/remotes/$cur_*/**"
}
# Lists tags from the local repository.
# Accepts the same positional parameters as __git_heads() above.
__git_tags ()
@ -621,6 +647,26 @@ __git_tags ()
"refs/tags/$cur_*" "refs/tags/$cur_*/**"
}
# List unique branches from refs/remotes used for 'git checkout' and 'git
# switch' tracking DWIMery.
# 1: A prefix to be added to each listed branch (optional)
# 2: List only branches matching this word (optional; list all branches if
# unset or empty).
# 3: A suffix to be appended to each listed branch (optional).
__git_dwim_remote_heads ()
{
local pfx="${1-}" cur_="${2-}" sfx="${3-}"
local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers
# employ the heuristic used by git checkout and git switch
# Try to find a remote branch that cur_es the completion word
# but only output if the branch name is unique
__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
--sort="refname:strip=3" \
"refs/remotes/*/$cur_*" "refs/remotes/*/$cur_*/**" | \
uniq -u
}
# Lists refs from the local (by default) or from a remote repository.
# It accepts 0, 1 or 2 arguments:
# 1: The remote to list refs from (optional; ignored, if set but empty).
@ -696,13 +742,7 @@ __git_refs ()
__git_dir="$dir" __git for-each-ref --format="$fer_pfx%($format)$sfx" \
"${refs[@]}"
if [ -n "$track" ]; then
# employ the heuristic used by git checkout
# Try to find a remote branch that matches the completion word
# but only output if the branch name is unique
__git for-each-ref --format="$fer_pfx%(refname:strip=3)$sfx" \
--sort="refname:strip=3" \
"refs/remotes/*/$match*" "refs/remotes/*/$match*/**" | \
uniq -u
__git_dwim_remote_heads "$pfx" "$match" "$sfx"
fi
return
fi
@ -749,29 +789,51 @@ __git_refs ()
# Usage: __git_complete_refs [<option>]...
# --remote=<remote>: The remote to list refs from, can be the name of a
# configured remote, a path, or a URL.
# --track: List unique remote branches for 'git checkout's tracking DWIMery.
# --dwim: List unique remote branches for 'git switch's tracking DWIMery.
# --pfx=<prefix>: A prefix to be added to each ref.
# --cur=<word>: The current ref to be completed. Defaults to the current
# word to be completed.
# --sfx=<suffix>: A suffix to be appended to each ref instead of the default
# space.
# --mode=<mode>: What set of refs to complete, one of 'refs' (the default) to
# complete all refs, 'heads' to complete only branches, or
# 'remote-heads' to complete only remote branches. Note that
# --remote is only compatible with --mode=refs.
__git_complete_refs ()
{
local remote track pfx cur_="$cur" sfx=" "
local remote dwim pfx cur_="$cur" sfx=" " mode="refs"
while test $# != 0; do
case "$1" in
--remote=*) remote="${1##--remote=}" ;;
--track) track="yes" ;;
--dwim) dwim="yes" ;;
# --track is an old spelling of --dwim
--track) dwim="yes" ;;
--pfx=*) pfx="${1##--pfx=}" ;;
--cur=*) cur_="${1##--cur=}" ;;
--sfx=*) sfx="${1##--sfx=}" ;;
--mode=*) mode="${1##--mode=}" ;;
*) return 1 ;;
esac
shift
done
__gitcomp_direct "$(__git_refs "$remote" "$track" "$pfx" "$cur_" "$sfx")"
# complete references based on the specified mode
case "$mode" in
refs)
__gitcomp_direct "$(__git_refs "$remote" "" "$pfx" "$cur_" "$sfx")" ;;
heads)
__gitcomp_direct "$(__git_heads "$pfx" "$cur_" "$sfx")" ;;
remote-heads)
__gitcomp_direct "$(__git_remote_heads "$pfx" "$cur_" "$sfx")" ;;
*)
return 1 ;;
esac
# Append DWIM remote branch names if requested
if [ "$dwim" = "yes" ]; then
__gitcomp_direct_append "$(__git_dwim_remote_heads "$pfx" "$cur_" "$sfx")"
fi
}
# __git_refs2 requires 1 argument (to pass to __git_refs)
@ -1102,6 +1164,40 @@ __git_find_on_cmdline ()
done
}
# Similar to __git_find_on_cmdline, except that it loops backwards and thus
# prints the *last* word found. Useful for finding which of two options that
# supersede each other came last, such as "--guess" and "--no-guess".
#
# Usage: __git_find_last_on_cmdline [<option>]... "<wordlist>"
# --show-idx: Optionally show the index of the found word in the $words array.
__git_find_last_on_cmdline ()
{
local word c=$cword show_idx
while test $# -gt 1; do
case "$1" in
--show-idx) show_idx=y ;;
*) return 1 ;;
esac
shift
done
local wordlist="$1"
while [ $c -gt 1 ]; do
((c--))
for word in $wordlist; do
if [ "$word" = "${words[c]}" ]; then
if [ -n "$show_idx" ]; then
echo "$c $word"
else
echo "$word"
fi
return
fi
done
done
}
# Echo the value of an option set on the command line or config
#
# $1: short option name
@ -1356,6 +1452,46 @@ _git_bundle ()
esac
}
# Helper function to decide whether or not we should enable DWIM logic for
# git-switch and git-checkout.
#
# To decide between the following rules in priority order
# 1) the last provided of "--guess" or "--no-guess" explicitly enable or
# disable completion of DWIM logic respectively.
# 2) If the --no-track option is provided, take this as a hint to disable the
# DWIM completion logic
# 3) If GIT_COMPLETION_CHECKOUT_NO_GUESS is set, disable the DWIM completion
# logic, as requested by the user.
# 4) Enable DWIM logic otherwise.
#
__git_checkout_default_dwim_mode ()
{
local last_option dwim_opt="--dwim"
if [ "$GIT_COMPLETION_CHECKOUT_NO_GUESS" = "1" ]; then
dwim_opt=""
fi
# --no-track disables DWIM, but with lower priority than
# --guess/--no-guess
if [ -n "$(__git_find_on_cmdline "--no-track")" ]; then
dwim_opt=""
fi
# Find the last provided --guess or --no-guess
last_option="$(__git_find_last_on_cmdline "--guess --no-guess")"
case "$last_option" in
--guess)
dwim_opt="--dwim"
;;
--no-guess)
dwim_opt=""
;;
esac
echo "$dwim_opt"
}
_git_checkout ()
{
__git_has_doubledash && return
@ -1368,14 +1504,38 @@ _git_checkout ()
__gitcomp_builtin checkout
;;
*)
# check if --track, --no-track, or --no-guess was specified
# if so, disable DWIM mode
local flags="--track --no-track --no-guess" track_opt="--track"
if [ "$GIT_COMPLETION_CHECKOUT_NO_GUESS" = "1" ] ||
[ -n "$(__git_find_on_cmdline "$flags")" ]; then
track_opt=''
local dwim_opt="$(__git_checkout_default_dwim_mode)"
local prevword prevword="${words[cword-1]}"
case "$prevword" in
-b|-B|--orphan)
# Complete local branches (and DWIM branch
# remote branch names) for an option argument
# specifying a new branch name. This is for
# convenience, assuming new branches are
# possibly based on pre-existing branch names.
__git_complete_refs $dwim_opt --mode="heads"
return
;;
*)
;;
esac
# At this point, we've already handled special completion for
# the arguments to -b/-B, and --orphan. There are 3 main
# things left we can possibly complete:
# 1) a start-point for -b/-B, -d/--detach, or --orphan
# 2) a remote head, for --track
# 3) an arbitrary reference, possibly including DWIM names
#
if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then
__git_complete_refs --mode="refs"
elif [ -n "$(__git_find_on_cmdline "--track")" ]; then
__git_complete_refs --mode="remote-heads"
else
__git_complete_refs $dwim_opt --mode="refs"
fi
__git_complete_refs $track_opt
;;
esac
}
@ -2224,29 +2384,43 @@ _git_switch ()
__gitcomp_builtin switch
;;
*)
# check if --track, --no-track, or --no-guess was specified
# if so, disable DWIM mode
local track_opt="--track" only_local_ref=n
if [ "$GIT_COMPLETION_CHECKOUT_NO_GUESS" = "1" ] ||
[ -n "$(__git_find_on_cmdline "--track --no-track --no-guess")" ]; then
track_opt=''
local dwim_opt="$(__git_checkout_default_dwim_mode)"
local prevword prevword="${words[cword-1]}"
case "$prevword" in
-c|-C|--orphan)
# Complete local branches (and DWIM branch
# remote branch names) for an option argument
# specifying a new branch name. This is for
# convenience, assuming new branches are
# possibly based on pre-existing branch names.
__git_complete_refs $dwim_opt --mode="heads"
return
;;
*)
;;
esac
# Unlike in git checkout, git switch --orphan does not take
# a start point. Thus we really have nothing to complete after
# the branch name.
if [ -n "$(__git_find_on_cmdline "--orphan")" ]; then
return
fi
# explicit --guess enables DWIM mode regardless of
# $GIT_COMPLETION_CHECKOUT_NO_GUESS
if [ -n "$(__git_find_on_cmdline "--guess")" ]; then
track_opt='--track'
fi
if [ -z "$(__git_find_on_cmdline "-d --detach")" ]; then
only_local_ref=y
# At this point, we've already handled special completion for
# -c/-C, and --orphan. There are 3 main things left to
# complete:
# 1) a start-point for -c/-C or -d/--detach
# 2) a remote head, for --track
# 3) a branch name, possibly including DWIM remote branches
if [ -n "$(__git_find_on_cmdline "-c -C -d --detach")" ]; then
__git_complete_refs --mode="refs"
elif [ -n "$(__git_find_on_cmdline "--track")" ]; then
__git_complete_refs --mode="remote-heads"
else
# --guess --detach is invalid combination, no
# dwim will be done when --detach is specified
track_opt=
fi
if [ $only_local_ref = y -a -z "$track_opt" ]; then
__gitcomp_direct "$(__git_heads "" "$cur" " ")"
else
__git_complete_refs $track_opt
__git_complete_refs $dwim_opt --mode="heads"
fi
;;
esac

View File

@ -1240,6 +1240,461 @@ test_expect_success '__git_complete_fetch_refspecs - fully qualified & prefix' '
test_cmp expected out
'
test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
test_completion "git switch " <<-\EOF
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
test_completion "git checkout " <<-\EOF
HEAD Z
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with --no-guess, complete only local branches' '
test_completion "git switch --no-guess " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete only local branches' '
GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
EOF
'
test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
test_completion "git switch --no-guess --guess " <<-\EOF
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
EOF
'
test_expect_success 'git switch - a later --no-guess overrides previous --guess, complete only local branches' '
test_completion "git switch --guess --no-guess " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only completes refs' '
GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
HEAD Z
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with --no-guess, only completes refs' '
test_completion "git checkout --no-guess " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
test_completion "git checkout --no-guess --guess " <<-\EOF
HEAD Z
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - a later --no-guess overrides previous --guess, complete only refs' '
test_completion "git checkout --guess --no-guess " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with --detach, complete all references' '
test_completion "git switch --detach " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with --detach, complete only references' '
test_completion "git checkout --detach " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with -d, complete all references' '
test_completion "git switch -d " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with -d, complete only references' '
test_completion "git checkout -d " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with --track, complete only remote branches' '
test_completion "git switch --track " <<-\EOF
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with --track, complete only remote branches' '
test_completion "git checkout --track " <<-\EOF
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with --no-track, complete only local branch names' '
test_completion "git switch --no-track " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - with --no-track, complete only local references' '
test_completion "git checkout --no-track " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with -c, complete all references' '
test_completion "git switch -c new-branch " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with -C, complete all references' '
test_completion "git switch -C new-branch " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with -c and --track, complete all references' '
test_completion "git switch -c new-branch --track " <<-EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with -C and --track, complete all references' '
test_completion "git switch -C new-branch --track " <<-EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with -c and --no-track, complete all references' '
test_completion "git switch -c new-branch --no-track " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - with -C and --no-track, complete all references' '
test_completion "git switch -C new-branch --no-track " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with -b, complete all references' '
test_completion "git checkout -b new-branch " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with -B, complete all references' '
test_completion "git checkout -B new-branch " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with -b and --track, complete all references' '
test_completion "git checkout -b new-branch --track " <<-EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with -B and --track, complete all references' '
test_completion "git checkout -B new-branch --track " <<-EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with -b and --no-track, complete all references' '
test_completion "git checkout -b new-branch --no-track " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git checkout - with -B and --no-track, complete all references' '
test_completion "git checkout -B new-branch --no-track " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
test_completion "git switch -c " <<-\EOF
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
EOF
'
test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
test_completion "git switch -C " <<-\EOF
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
EOF
'
test_expect_success 'git switch - for -c with --no-guess, complete local branches only' '
test_completion "git switch --no-guess -c " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git switch - for -C with --no-guess, complete local branches only' '
test_completion "git switch --no-guess -C " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git switch - for -c with --no-track, complete local branches only' '
test_completion "git switch --no-track -c " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git switch - for -C with --no-track, complete local branches only' '
test_completion "git switch --no-track -C " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
test_completion "git checkout -b " <<-\EOF
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
test_completion "git checkout -B " <<-\EOF
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - for -b with --no-guess, complete local branches only' '
test_completion "git checkout --no-guess -b " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - for -B with --no-guess, complete local branches only' '
test_completion "git checkout --no-guess -B " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - for -b with --no-track, complete local branches only' '
test_completion "git checkout --no-track -b " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - for -B with --no-track, complete local branches only' '
test_completion "git checkout --no-track -B " <<-\EOF
master Z
matching-branch Z
EOF
'
test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
test_completion "git switch --orphan " <<-\EOF
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
EOF
'
test_expect_success 'git switch - --orphan with branch already provided completes nothing else' '
test_completion "git switch --orphan master " <<-\EOF
EOF
'
test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
test_completion "git checkout --orphan " <<-\EOF
branch-in-other Z
master Z
master-in-other Z
matching-branch Z
EOF
'
test_expect_success 'git checkout - --orphan with branch already provided completes local refs for a start-point' '
test_completion "git checkout --orphan master " <<-\EOF
HEAD Z
master Z
matching-branch Z
matching-tag Z
other/branch-in-other Z
other/master-in-other Z
EOF
'
test_expect_success 'teardown after ref completion' '
git branch -d matching-branch &&
git tag -d matching-tag &&