Skip to content

Commit e896369

Browse files
szedergitster
authored andcommitted
completion: let 'for-each-ref' and 'ls-remote' filter matching refs
When completing refs, several __git_refs() code paths list all the refs from the refs/{heads,tags,remotes}/ hierarchy and then __gitcomp_nl() iterates over those refs in a shell loop to filter out refs not matching the current ref to be completed. This comes with a considerable performance penalty when a repository contains a lot of refs but the current ref can be uniquely completed or when only a handful of refs match the current ref. Reduce the number of iterations in __gitcomp_nl() from the number of refs to the number of matching refs by specifying appropriate globbing patterns to 'git for-each-ref' and 'git ls-remote' to list only those refs that match the current ref to be completed. However, do so only when the ref to match is explicitly given as parameter, because the current word on the command line might contain a prefix like '--option=' or 'branch..'. The __git_complete_refs() and __git_complete_fetch_refspecs() helpers introduced previously in this patch series already call __git_refs() specifying this current ref parameter, so all their callsites, i.e. all places in the completion script doing refs completion, can benefit from this optimization. Furthermore, list only those symbolic and pseudo refs that match the current ref to be completed. Though it doesn't matter at all in itself performance-wise, it will allow us further significant optimizations later in this series. This speeds up refs completion considerably when there are a lot of non-matching refs to be filtered out. Uniquely completing a branch in a repository with 100k local branches, all packed, best of five: On Linux, before: $ time __git_complete_refs --cur=maste real 0m0.831s user 0m0.808s sys 0m0.028s After: real 0m0.119s user 0m0.104s sys 0m0.008s On Windows, before: real 0m1.480s user 0m1.031s sys 0m0.060s After: real 0m0.377s user 0m0.015s sys 0m0.030s Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent b2b6811 commit e896369

File tree

2 files changed

+154
-11
lines changed

2 files changed

+154
-11
lines changed

contrib/completion/git-completion.bash

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,8 @@ __git_tags ()
355355
# 2: In addition to local refs, list unique branches from refs/remotes/ for
356356
# 'git checkout's tracking DWIMery (optional; ignored, if set but empty).
357357
# 3: Currently ignored.
358-
# 4: The current ref to be completed (optional).
358+
# 4: List only refs matching this word (optional; list all refs if unset or
359+
# empty).
359360
#
360361
# Use __git_complete_refs() instead.
361362
__git_refs ()
@@ -364,6 +365,7 @@ __git_refs ()
364365
local list_refs_from=path remote="${1-}"
365366
local format refs pfx
366367
local cur_="${4-$cur}"
368+
local match="${4-}"
367369

368370
__git_find_repo_path
369371
dir="$__git_repo_path"
@@ -390,23 +392,32 @@ __git_refs ()
390392
if [[ "$cur_" == ^* ]]; then
391393
pfx="^"
392394
cur_=${cur_#^}
395+
match=${match#^}
393396
fi
394397
case "$cur_" in
395398
refs|refs/*)
396399
format="refname"
397-
refs="${cur_%/*}"
400+
refs=("$match*" "$match*/**")
398401
track=""
399402
;;
400403
*)
401404
for i in HEAD FETCH_HEAD ORIG_HEAD MERGE_HEAD; do
402-
if [ -e "$dir/$i" ]; then echo $pfx$i; fi
405+
case "$i" in
406+
$match*)
407+
if [ -e "$dir/$i" ]; then
408+
echo $pfx$i
409+
fi
410+
;;
411+
esac
403412
done
404413
format="refname:strip=2"
405-
refs="refs/tags refs/heads refs/remotes"
414+
refs=("refs/tags/$match*" "refs/tags/$match*/**"
415+
"refs/heads/$match*" "refs/heads/$match*/**"
416+
"refs/remotes/$match*" "refs/remotes/$match*/**")
406417
;;
407418
esac
408419
__git_dir="$dir" __git for-each-ref --format="$pfx%($format)" \
409-
$refs
420+
"${refs[@]}"
410421
if [ -n "$track" ]; then
411422
# employ the heuristic used by git checkout
412423
# Try to find a remote branch that matches the completion word
@@ -417,7 +428,7 @@ __git_refs ()
417428
while read -r entry; do
418429
eval "$entry"
419430
ref="${ref#*/}"
420-
if [[ "$ref" == "$cur_"* ]]; then
431+
if [[ "$ref" == "$match"* ]]; then
421432
echo "$ref"
422433
fi
423434
done | sort | uniq -u
@@ -426,7 +437,7 @@ __git_refs ()
426437
fi
427438
case "$cur_" in
428439
refs|refs/*)
429-
__git ls-remote "$remote" "$cur_*" | \
440+
__git ls-remote "$remote" "$match*" | \
430441
while read -r hash i; do
431442
case "$i" in
432443
*^{}) ;;
@@ -436,12 +447,20 @@ __git_refs ()
436447
;;
437448
*)
438449
if [ "$list_refs_from" = remote ]; then
439-
echo "HEAD"
450+
case "HEAD" in
451+
$match*) echo "HEAD" ;;
452+
esac
440453
__git for-each-ref --format="%(refname:strip=2)" \
441-
"refs/remotes/$remote/" | sed -e "s#^$remote/##"
454+
"refs/remotes/$remote/$match*" \
455+
"refs/remotes/$remote/$match*/**" | sed -e "s#^$remote/##"
442456
else
443-
__git ls-remote "$remote" HEAD \
444-
"refs/tags/*" "refs/heads/*" "refs/remotes/*" |
457+
local query_symref
458+
case "HEAD" in
459+
$match*) query_symref="HEAD" ;;
460+
esac
461+
__git ls-remote "$remote" $query_symref \
462+
"refs/tags/$match*" "refs/heads/$match*" \
463+
"refs/remotes/$match*" |
445464
while read -r hash i; do
446465
case "$i" in
447466
*^{}) ;;

t/t9902-completion.sh

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,9 @@ test_expect_success '__git_refs - full refs' '
555555
cat >expected <<-EOF &&
556556
refs/heads/master
557557
refs/heads/matching-branch
558+
refs/remotes/other/branch-in-other
559+
refs/remotes/other/master-in-other
560+
refs/tags/matching-tag
558561
EOF
559562
(
560563
cur=refs/heads/ &&
@@ -620,6 +623,7 @@ test_expect_success '__git_refs - configured remote' '
620623

621624
test_expect_success '__git_refs - configured remote - full refs' '
622625
cat >expected <<-EOF &&
626+
HEAD
623627
refs/heads/branch-in-other
624628
refs/heads/master-in-other
625629
refs/tags/tag-in-other
@@ -648,6 +652,7 @@ test_expect_success '__git_refs - configured remote - repo given on the command
648652

649653
test_expect_success '__git_refs - configured remote - full refs - repo given on the command line' '
650654
cat >expected <<-EOF &&
655+
HEAD
651656
refs/heads/branch-in-other
652657
refs/heads/master-in-other
653658
refs/tags/tag-in-other
@@ -692,6 +697,7 @@ test_expect_success '__git_refs - URL remote' '
692697

693698
test_expect_success '__git_refs - URL remote - full refs' '
694699
cat >expected <<-EOF &&
700+
HEAD
695701
refs/heads/branch-in-other
696702
refs/heads/master-in-other
697703
refs/tags/tag-in-other
@@ -837,6 +843,124 @@ test_expect_success '__git refs - exluding full refs' '
837843
test_cmp expected "$actual"
838844
'
839845

846+
test_expect_success 'setup for filtering matching refs' '
847+
git branch matching/branch &&
848+
git tag matching/tag &&
849+
git -C otherrepo branch matching/branch-in-other &&
850+
git fetch --no-tags other &&
851+
rm -f .git/FETCH_HEAD
852+
'
853+
854+
test_expect_success '__git_refs - dont filter refs unless told so' '
855+
cat >expected <<-EOF &&
856+
HEAD
857+
master
858+
matching-branch
859+
matching/branch
860+
other/branch-in-other
861+
other/master-in-other
862+
other/matching/branch-in-other
863+
matching-tag
864+
matching/tag
865+
EOF
866+
(
867+
cur=master &&
868+
__git_refs >"$actual"
869+
) &&
870+
test_cmp expected "$actual"
871+
'
872+
873+
test_expect_success '__git_refs - only matching refs' '
874+
cat >expected <<-EOF &&
875+
matching-branch
876+
matching/branch
877+
matching-tag
878+
matching/tag
879+
EOF
880+
(
881+
cur=mat &&
882+
__git_refs "" "" "" "$cur" >"$actual"
883+
) &&
884+
test_cmp expected "$actual"
885+
'
886+
887+
test_expect_success '__git_refs - only matching refs - full refs' '
888+
cat >expected <<-EOF &&
889+
refs/heads/matching-branch
890+
refs/heads/matching/branch
891+
EOF
892+
(
893+
cur=refs/heads/mat &&
894+
__git_refs "" "" "" "$cur" >"$actual"
895+
) &&
896+
test_cmp expected "$actual"
897+
'
898+
899+
test_expect_success '__git_refs - only matching refs - remote on local file system' '
900+
cat >expected <<-EOF &&
901+
master-in-other
902+
matching/branch-in-other
903+
EOF
904+
(
905+
cur=ma &&
906+
__git_refs otherrepo "" "" "$cur" >"$actual"
907+
) &&
908+
test_cmp expected "$actual"
909+
'
910+
911+
test_expect_success '__git_refs - only matching refs - configured remote' '
912+
cat >expected <<-EOF &&
913+
master-in-other
914+
matching/branch-in-other
915+
EOF
916+
(
917+
cur=ma &&
918+
__git_refs other "" "" "$cur" >"$actual"
919+
) &&
920+
test_cmp expected "$actual"
921+
'
922+
923+
test_expect_success '__git_refs - only matching refs - remote - full refs' '
924+
cat >expected <<-EOF &&
925+
refs/heads/master-in-other
926+
refs/heads/matching/branch-in-other
927+
EOF
928+
(
929+
cur=refs/heads/ma &&
930+
__git_refs other "" "" "$cur" >"$actual"
931+
) &&
932+
test_cmp expected "$actual"
933+
'
934+
935+
test_expect_success '__git_refs - only matching refs - checkout DWIMery' '
936+
cat >expected <<-EOF &&
937+
matching-branch
938+
matching/branch
939+
matching-tag
940+
matching/tag
941+
matching/branch-in-other
942+
EOF
943+
for remote_ref in refs/remotes/other/ambiguous \
944+
refs/remotes/remote/ambiguous \
945+
refs/remotes/remote/branch-in-remote
946+
do
947+
git update-ref $remote_ref master &&
948+
test_when_finished "git update-ref -d $remote_ref"
949+
done &&
950+
(
951+
cur=mat &&
952+
__git_refs "" 1 "" "$cur" >"$actual"
953+
) &&
954+
test_cmp expected "$actual"
955+
'
956+
957+
test_expect_success 'teardown after filtering matching refs' '
958+
git branch -d matching/branch &&
959+
git tag -d matching/tag &&
960+
git update-ref -d refs/remotes/other/matching/branch-in-other &&
961+
git -C otherrepo branch -D matching/branch-in-other
962+
'
963+
840964
test_expect_success '__git_complete_refs - simple' '
841965
sed -e "s/Z$//" >expected <<-EOF &&
842966
HEAD Z

0 commit comments

Comments
 (0)