From 735514635b49332efba080ed8e2c75b5e3c37a6e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 13 Aug 2026 12:12:32 -0700 Subject: [PATCH 1/3] completion: no-op refactoring of checkout completion The 'git checkout' completion function punts very early when it sees '--' on the command line, as it indicates that options or revisions can no longer appear. By returning early, it allows the default Bash action (which completes files in '$PWD') to kick in. In preparation for changing what happens in the next step when option or revision completion yields no matching candidates, or when '--' is present, reorganize the control flow to avoid this early return, and add explicit returns to the option completion branches. Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 82 +++++++++++++------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index d35b4f3024..38dec1cabe 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1735,49 +1735,51 @@ __git_checkout_default_dwim_mode () _git_checkout () { - __git_has_doubledash && return + if ! __git_has_doubledash; then + local dwim_opt="$(__git_checkout_default_dwim_mode)" - local dwim_opt="$(__git_checkout_default_dwim_mode)" + case "$prev" 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 - case "$prev" 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 + case "$cur" in + --conflict=*) + __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" + return + ;; + --*) + __gitcomp_builtin checkout + return + ;; + *) + # 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 + # - case "$cur" in - --conflict=*) - __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" - ;; - --*) - __gitcomp_builtin checkout - ;; - *) - # 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 "-t --track")" ]; then - __git_complete_refs --mode="remote-heads" - else - __git_complete_refs $dwim_opt --mode="refs" - fi - ;; - esac + if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then + __git_complete_refs --mode="refs" + elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then + __git_complete_refs --mode="remote-heads" + else + __git_complete_refs $dwim_opt --mode="refs" + fi + ;; + esac + fi } __git_sequencer_inprogress_options="--continue --quit --abort --skip" From 3fe92099860aabc8af341334562ff7cdb5e3a9a3 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 13 Aug 2026 12:12:33 -0700 Subject: [PATCH 2/3] completion: complete tracked paths for "git checkout" When completing arguments for "git checkout", _git_checkout() delegates to __git_complete_refs(), which only completes revision references. This is good, as mixing revisions and paths in a single list from which the user can choose is confusing. However, if no reference matches, or if "--" is given, _git_checkout() leaves COMPREPLY empty. Bash then falls back to the default filename completion in $PWD. This fails when "git -C " is used, as $PWD is not the target repository. Update _git_checkout() to use __git_complete_index_file() when "--" is present, or when revision reference completion yields no matching candidates, so that tracked paths are offered as candidates. Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 4 +++ t/t9902-completion.sh | 39 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 38dec1cabe..0eecfcbf8b 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1780,6 +1780,10 @@ _git_checkout () ;; esac fi + + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "" + fi } __git_sequencer_inprogress_options="--continue --quit --abort --skip" diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index b889ec8c77..13fa5c65c3 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2714,6 +2714,45 @@ test_expect_success 'git -C checkout uses the right repo' ' EOF ' +test_expect_success 'git checkout completes tracked paths when no refs match' ' + # file1 and file2 are tracked but file3 is not + # there is no ref that begins with f + test_completion "git checkout f" <<-\EOF && + file1 + file2 + EOF + test_completion "git checkout -- f" <<-\EOF + file1 + file2 + EOF +' + +test_expect_success 'git -C checkout completes paths in specified repo' ' + # otherfile is tracked, oops is not + # lostfile is tracked but lost + test_when_finished "rm -rf repo-for-checkout" && + git init repo-for-checkout && + echo content >repo-for-checkout/otherfile && + echo content >repo-for-checkout/lostfile && + git -C repo-for-checkout add otherfile && + git -C repo-for-checkout add lostfile && + git -C repo-for-checkout commit -m otherfile && + echo untracked >repo-for-checkout/oops && + rm -f repo-for-checkout/lostfile && + test_completion "git -C repo-for-checkout checkout o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-checkout checkout -- o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-checkout checkout l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-checkout checkout -- l" <<-\EOF + lostfile + EOF +' + test_expect_success 'git diff completes tracked paths when no refs match' ' # file1 and file2 are tracked but file3 is not # there is no ref that begins with f From 05e2ab1f31dd79ab6e17fc8f69a640ac8d0169d5 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 13 Aug 2026 12:12:34 -0700 Subject: [PATCH 3/3] completion: 'git checkout' completes untracked paths as a last resort We taught 'git checkout' to first try to complete revisions (unless '--' is present on the command line) and, failing that, to complete tracked paths. If this yields nothing, it lets the Bash default, which offers paths in $PWD, kick in. Teach it to complete untracked paths before giving up and letting the Bash default kick in. With this change, $ git -C another-directory checkout un finds the 'untracked' file in another-directory and offers it as a completion candidate. Note that this is of somewhat dubious value, as an untracked path by definition does not exist in the index, so checking it out from the index would not work well. Even when used to check out the path from a different branch, it is still of dubious value because it is unlikely that a path tracked in another branch is lying untracked in the working tree, as switching from a branch with the path to a branch without it will normally remove the file in the working tree. A better behavior probably is to detect the tree-ish argument on the command line and offer paths with the given prefix as candidates, but there is no __git_complete_from_tree() helper readily usable, so mark this as #leftoverbits to wait for another day. Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 4 ++++ t/t9902-completion.sh | 21 +++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 0eecfcbf8b..e6dce62d3c 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1784,6 +1784,10 @@ _git_checkout () if [ ${#COMPREPLY[@]} -eq 0 ]; then __git_complete_index_file "" fi + + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "--others --directory" + fi } __git_sequencer_inprogress_options="--continue --quit --abort --skip" diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 13fa5c65c3..e8418f069b 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2727,9 +2727,19 @@ test_expect_success 'git checkout completes tracked paths when no refs match' ' EOF ' +test_expect_success 'git checkout completes untracked paths, too' ' + # ufile is not tracked and there is no ref that begins with u + test_completion "git checkout u" <<-\EOF && + ufile + EOF + test_completion "git checkout -- u" <<-\EOF + ufile + EOF +' + test_expect_success 'git -C checkout completes paths in specified repo' ' # otherfile is tracked, oops is not - # lostfile is tracked but lost + # lostfile is tracked but lost, ufile is untracked. test_when_finished "rm -rf repo-for-checkout" && git init repo-for-checkout && echo content >repo-for-checkout/otherfile && @@ -2738,6 +2748,7 @@ test_expect_success 'git -C checkout completes paths in specified repo' ' git -C repo-for-checkout add lostfile && git -C repo-for-checkout commit -m otherfile && echo untracked >repo-for-checkout/oops && + echo untracked >repo-for-checkout/ufile && rm -f repo-for-checkout/lostfile && test_completion "git -C repo-for-checkout checkout o" <<-\EOF && otherfile @@ -2748,9 +2759,15 @@ test_expect_success 'git -C checkout completes paths in specified repo' ' test_completion "git -C repo-for-checkout checkout l" <<-\EOF && lostfile EOF - test_completion "git -C repo-for-checkout checkout -- l" <<-\EOF + test_completion "git -C repo-for-checkout checkout -- l" <<-\EOF && lostfile EOF + test_completion "git -C repo-for-checkout checkout u" <<-\EOF && + ufile + EOF + test_completion "git -C repo-for-checkout checkout -- u" <<-\EOF + ufile + EOF ' test_expect_success 'git diff completes tracked paths when no refs match' '