Hi, I'm attaching a patch for the Bash completion script, to be able to change the default implicit alphabetical ordering used when returning refs e.g. when doing "git checkout <TAB>" I wanted the completed refs to be ordered descending by committer date i.e. --sort="-committerdate" because that shows on top the branches I've been recently working on. The completion script didn't allow to set a custom ordering from the default alphabetical one, so I'm sending a patch which adds a new config var where the user can set their desired custom ordering. I've not added tests because I'm not familiar with the test machinery, hopefully this is still useful. Regards, PD. I send from Gmail web interface because git send-email for Gmail requires 2-factor authentication and I chose not to enable it.
From 77a02e68481024e10414595730c613450b7d38e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nelson=20Ben=C3=ADtez=20Le=C3=B3n?= <nbenitezl@xxxxxxxxx> 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 <TAB>" 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=<val> 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 <nbenitezl@xxxxxxxxx> --- 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<TAB>"). +# +# 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=<path>' and # '-C <path>' 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