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>

SZEDER Gábor committed Mar 23, 2017 at 16:29 UTC e896369beb6c9cc9dc1a37d77cb9751be13ce959
2 files changed +154 -11
contrib/completion/git-completion.bash
+30 -11
@@ -355,7 +355,8 @@ __git_tags ()
355 # 2: In addition to local refs, list unique branches from refs/remotes/ for
356 # 'git checkout's tracking DWIMery (optional; ignored, if set but empty).
357 # 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).
360 #
361 # Use __git_complete_refs() instead.
362 __git_refs ()
@@ -364,6 +365,7 @@ __git_refs ()
365 local list_refs_from=path remote="${1-}"
366 local format refs pfx
367 local cur_="${4-$cur}"
368 + local match="${4-}"
369
370 __git_find_repo_path
371 dir="$__git_repo_path"
@@ -390,23 +392,32 @@ __git_refs ()
392 if [[ "$cur_" == ^* ]]; then
393 pfx="^"
394 cur_=${cur_#^}
395 + match=${match#^}
396 fi
397 case "$cur_" in
398 refs|refs/*)
399 format="refname"
397 - refs="${cur_%/*}"
400 + refs=("$match*" "$match*/**")
401 track=""
402 ;;
403 *)
404 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
412 done
413 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*/**")
417 ;;
418 esac
419 __git_dir="$dir" __git for-each-ref --format="$pfx%($format)" \
409 - $refs
420 + "${refs[@]}"
421 if [ -n "$track" ]; then
422 # employ the heuristic used by git checkout
423 # Try to find a remote branch that matches the completion word
@@ -417,7 +428,7 @@ __git_refs ()
428 while read -r entry; do
429 eval "$entry"
430 ref="${ref#*/}"
420 - if [[ "$ref" == "$cur_"* ]]; then
431 + if [[ "$ref" == "$match"* ]]; then
432 echo "$ref"
433 fi
434 done | sort | uniq -u
@@ -426,7 +437,7 @@ __git_refs ()
437 fi
438 case "$cur_" in
439 refs|refs/*)
429 - __git ls-remote "$remote" "$cur_*" | \
440 + __git ls-remote "$remote" "$match*" | \
441 while read -r hash i; do
442 case "$i" in
443 *^{}) ;;
@@ -436,12 +447,20 @@ __git_refs ()
447 ;;
448 *)
449 if [ "$list_refs_from" = remote ]; then
439 - echo "HEAD"
450 + case "HEAD" in
451 + $match*) echo "HEAD" ;;
452 + esac
453 __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/##"
456 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*" |
464 while read -r hash i; do
465 case "$i" in
466 *^{}) ;;
t/t9902-completion.sh
+124
@@ -555,6 +555,9 @@ test_expect_success '__git_refs - full refs' '
555 cat >expected <<-EOF &&
556 refs/heads/master
557 refs/heads/matching-branch
558 + refs/remotes/other/branch-in-other
559 + refs/remotes/other/master-in-other
560 + refs/tags/matching-tag
561 EOF
562 (
563 cur=refs/heads/ &&
@@ -620,6 +623,7 @@ test_expect_success '__git_refs - configured remote' '
623
624 test_expect_success '__git_refs - configured remote - full refs' '
625 cat >expected <<-EOF &&
626 + HEAD
627 refs/heads/branch-in-other
628 refs/heads/master-in-other
629 refs/tags/tag-in-other
@@ -648,6 +652,7 @@ test_expect_success '__git_refs - configured remote - repo given on the command
652
653 test_expect_success '__git_refs - configured remote - full refs - repo given on the command line' '
654 cat >expected <<-EOF &&
655 + HEAD
656 refs/heads/branch-in-other
657 refs/heads/master-in-other
658 refs/tags/tag-in-other
@@ -692,6 +697,7 @@ test_expect_success '__git_refs - URL remote' '
697
698 test_expect_success '__git_refs - URL remote - full refs' '
699 cat >expected <<-EOF &&
700 + HEAD
701 refs/heads/branch-in-other
702 refs/heads/master-in-other
703 refs/tags/tag-in-other
@@ -837,6 +843,124 @@ test_expect_success '__git refs - exluding full refs' '
843 test_cmp expected "$actual"
844 '
845
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 +
964 test_expect_success '__git_complete_refs - simple' '
965 sed -e "s/Z$//" >expected <<-EOF &&
966 HEAD Z