From 77a02e68481024e10414595730c613450b7d38e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nelson=20Ben=C3=ADtez=20Le=C3=B3n?= Date: Sun, 8 Jun 2025 15:41:10 +0100 Subject: [PATCH] completion: new config var to use --sort in for-each-ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously when completing refs eg. "git checkout " all refs were alphabetically ordered, this was an implicit ordering and could not be changed. This commit adds a new config var to allow setting a custom ordering, the conf value will be used for the --sort= of for-each-ref. When a custom ordering is not set then alphabetical default is kept, but this time is explicit as we pass --sort='refname' This commit also adds '-o nosort' to 'complete' to disable its default alphabetical ordering so our custom ordering prevails. Signed-off-by: Nelson Benítez León --- contrib/completion/git-completion.bash | 56 +++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index e3d88b067..59964a805 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -77,18 +77,43 @@ # # GIT_COMPLETION_IGNORE_CASE # # When set, uses for-each-ref '--ignore-case' to find refs that match # case insensitively, even on systems with case sensitive file systems # (e.g., completing tag name "FOO" on "git checkout f"). +# +# GIT_COMPLETION_REFS_SORT_BY_FIELDNAME +# +# Fieldname string to use for --sort option of for-each-ref. If empty or +# not defined it defaults to "refname" which is the same default git uses +# when no --sort option is provided. Some example values: +# '-committerdate' to descending sort by committer date +# '-version:refname' to descending sort by refname interpreted as version +# More info and examples: https://git-scm.com/docs/git-for-each-ref#_field_names case "$COMP_WORDBREAKS" in *:*) : great ;; *) COMP_WORDBREAKS="$COMP_WORDBREAKS:" esac +# Reads and validates GIT_COMPLETION_REFS_SORT_BY_FIELDNAME configuration var, +# returning the content of it when it's valid, or if not valid or is empty or +# not defined, then it returns the documented default i.e. 'refname'. +__git_get_sort_by_fieldname () +{ + if [ -n "${GIT_COMPLETION_REFS_SORT_BY_FIELDNAME-}" ]; then + # Validate by using a regex pattern which only allows a set + # of characters that may appear in a --sort expression + if [[ "$GIT_COMPLETION_REFS_SORT_BY_FIELDNAME" =~ ^[a-zA-Z0-9%:=*(),_\ -]+$ ]]; then + echo "$GIT_COMPLETION_REFS_SORT_BY_FIELDNAME" + return + fi + fi + echo 'refname' +} + # Discovers the path to the git repository taking any '--git-dir=' and # '-C ' options into account and stores it in the $__git_repo_path # variable. __git_find_repo_path () { if [ -n "${__git_repo_path-}" ]; then @@ -748,13 +773,15 @@ __git_complete_index_file () # unset or empty). # 3: A suffix to be appended to each listed branch (optional). __git_heads () { local pfx="${1-}" cur_="${2-}" sfx="${3-}" - __git for-each-ref --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \ + local sortby=$(__git_get_sort_by_fieldname) + + __git for-each-ref --sort="$sortby" --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \ ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \ "refs/heads/$cur_*" "refs/heads/$cur_*/**" } # Lists branches from remote repositories. # 1: A prefix to be added to each listed branch (optional). @@ -762,24 +789,28 @@ __git_heads () # 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" \ + local sortby=$(__git_get_sort_by_fieldname) + + __git for-each-ref --sort="$sortby" --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \ ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \ "refs/remotes/$cur_*" "refs/remotes/$cur_*/**" } # Lists tags from the local repository. # Accepts the same positional parameters as __git_heads() above. __git_tags () { local pfx="${1-}" cur_="${2-}" sfx="${3-}" - __git for-each-ref --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \ + local sortby=$(__git_get_sort_by_fieldname) + + __git for-each-ref --sort="$sortby" --format="${pfx//\%/%%}%(refname:strip=2)$sfx" \ ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \ "refs/tags/$cur_*" "refs/tags/$cur_*/**" } # List unique branches from refs/remotes used for 'git checkout' and 'git # switch' tracking DWIMery. @@ -815,13 +846,15 @@ __git_dwim_remote_heads () print ENVIRON["PFX"] branch ENVIRON["SFX"] break } } } ' - __git for-each-ref --format='%(refname)' refs/remotes/ | + local sortby=$(__git_get_sort_by_fieldname) + + __git for-each-ref --sort="$sortby" --format='%(refname)' refs/remotes/ | PFX="$pfx" SFX="$sfx" CUR_="$cur_" \ IGNORE_CASE=${GIT_COMPLETION_IGNORE_CASE+1} \ REMOTES="$(__git_remotes | sort -r)" awk "$awk_script" | sort | uniq -u } @@ -844,12 +877,13 @@ __git_refs () local list_refs_from=path remote="${1-}" local format refs local pfx="${3-}" cur_="${4-$cur}" sfx="${5-}" local match="${4-}" local umatch="${4-}" local fer_pfx="${pfx//\%/%%}" # "escape" for-each-ref format specifiers + local sortby=$(__git_get_sort_by_fieldname) __git_find_repo_path dir="$__git_repo_path" if [ -z "$remote" ]; then if [ -z "$dir" ]; then @@ -902,13 +936,14 @@ __git_refs () format="refname:strip=2" refs=("refs/tags/$match*" "refs/tags/$match*/**" "refs/heads/$match*" "refs/heads/$match*/**" "refs/remotes/$match*" "refs/remotes/$match*/**") ;; esac - __git_dir="$dir" __git for-each-ref --format="$fer_pfx%($format)$sfx" \ + __git_dir="$dir" __git for-each-ref --sort="$sortby" \ + --format="$fer_pfx%($format)$sfx" \ ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \ "${refs[@]}" if [ -n "$track" ]; then __git_dwim_remote_heads "$pfx" "$match" "$sfx" fi return @@ -926,13 +961,14 @@ __git_refs () *) if [ "$list_refs_from" = remote ]; then case "HEAD" in $match*|$umatch*) echo "${pfx}HEAD$sfx" ;; esac local strip="$(__git_count_path_components "refs/remotes/$remote")" - __git for-each-ref --format="$fer_pfx%(refname:strip=$strip)$sfx" \ + __git for-each-ref --sort="$sortby" \ + --format="$fer_pfx%(refname:strip=$strip)$sfx" \ ${GIT_COMPLETION_IGNORE_CASE+--ignore-case} \ "refs/remotes/$remote/$match*" \ "refs/remotes/$remote/$match*/**" else local query_symref case "HEAD" in @@ -2858,13 +2894,14 @@ __git_complete_config_variable_value () __gitcomp_nl "$(__git_refs_remotes "$remote")" "" "$cur_" return ;; remote.*.push) local remote="${varname#remote.}" remote="${remote%.push}" - __gitcomp_nl "$(__git for-each-ref \ + local sortby=$(__git_get_sort_by_fieldname) + __gitcomp_nl "$(__git for-each-ref --sort="$sortby" \ --format='%(refname):%(refname)' refs/heads)" "" "$cur_" return ;; pull.twohead|pull.octopus) __git_compute_merge_strategies __gitcomp "$__git_merge_strategies" "" "$cur_" @@ -3980,14 +4017,15 @@ __git_func_wrap () } ___git_complete () { local wrapper="__git_wrap${2}" eval "$wrapper () { __git_func_wrap $2 ; }" - complete -o bashdefault -o default -o nospace -F $wrapper $1 2>/dev/null \ - || complete -o default -o nospace -F $wrapper $1 + complete -o bashdefault -o default -o nospace -o nosort \ + -F $wrapper $1 2>/dev/null \ + || complete -o default -o nospace -o nosort -F $wrapper $1 } # Setup the completion for git commands # 1: command or alias # 2: function to call (e.g. `git`, `gitk`, `git_fetch`) __git_complete () -- 2.49.0