mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-26 12:53:13 +00:00
git completions: Parse git status --porcelain (#4673)
* git completions: Parse git status --porcelain This is much faster on large repositories, as it allows us to do a lot more with a single git call. It also makes it easy to add descriptions to distinguish modified files from untracked ones. TBD is if all commands now have the right kinds of files. [ci skip]
This commit is contained in:
parent
12c249abbe
commit
ea897fcc0f
1 changed files with 115 additions and 30 deletions
|
@ -66,17 +66,108 @@ function __fish_git_remotes
|
||||||
command git remote ^/dev/null
|
command git remote ^/dev/null
|
||||||
end
|
end
|
||||||
|
|
||||||
function __fish_git_modified_files
|
function __fish_git_files
|
||||||
# git diff --name-only hands us filenames relative to the git toplevel
|
# A function to show various kinds of files git knows about,
|
||||||
set -l root (command git rev-parse --show-toplevel ^/dev/null)
|
# by parsing `git status --porcelain`.
|
||||||
# Print files from the current $PWD as-is, prepend all others with ":/" (relative to toplevel in git-speak)
|
#
|
||||||
# This is a bit simplistic but finding the lowest common directory and then replacing everything else in $PWD with ".." is a bit annoying
|
# This accepts arguments to denote the kind of files:
|
||||||
string replace -- "$PWD/" "" "$root/"(command git diff --name-only $argv ^/dev/null) | string replace "$root/" ":/"
|
# - added: Staged added files (unstaged adds are untracked)
|
||||||
end
|
# - copied
|
||||||
|
# - deleted
|
||||||
|
# - deleted-staged
|
||||||
|
# - ignored
|
||||||
|
# - modified: Files that have been modified (but aren't staged)
|
||||||
|
# - modified-staged: Staged modified files
|
||||||
|
# - renamed
|
||||||
|
# - untracked
|
||||||
|
# and as a convenience "all-staged"
|
||||||
|
# to get _all_ kinds of staged files.
|
||||||
|
|
||||||
function __fish_git_add_files
|
# git status --porcelain gives us all the info we need, in a format we don't.
|
||||||
set -l root (command git rev-parse --show-toplevel ^/dev/null)
|
# The v2 format thankfully doesn't use " M" to denote a changed unstaged file
|
||||||
string replace -- "$PWD/" "" "$root/"(command git -C $root ls-files -mo --exclude-standard ^/dev/null) | string replace "$root/" ":/"
|
# and "M " to denote a changed staged file, opting for a "." in place of the space.
|
||||||
|
# So the v1 format would require something other than this `while read` loop.
|
||||||
|
#
|
||||||
|
# Unfortunately, the v2 format is really 4 different subformats
|
||||||
|
# - see the explanation inline. (Or on https://git-scm.com/docs/git-status)
|
||||||
|
#
|
||||||
|
# Also, we ignore submodules because they aren't useful as arguments (generally),
|
||||||
|
# and they slow things down quite significantly.
|
||||||
|
# E.g. `git reset $submodule` won't do anything (not even print an error).
|
||||||
|
set -l use_next
|
||||||
|
command git status --porcelain=2 -z --ignore-submodules=all \
|
||||||
|
| while read -laz -d ' ' line
|
||||||
|
# The entire line is the "from" from a rename.
|
||||||
|
if set -q use_next[1]
|
||||||
|
contains -- $use_next $argv
|
||||||
|
and echo "$line"
|
||||||
|
set -e use_next[1]
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
# The basic status format is "XY", where X is "our" state (meaning the staging area),
|
||||||
|
# and "Y" is "their" state.
|
||||||
|
# A "." means it's unmodified.
|
||||||
|
switch "$line[1..2]"
|
||||||
|
case 'u *'
|
||||||
|
# Unmerged
|
||||||
|
# "Unmerged entries have the following format; the first character is a "u" to distinguish from ordinary changed entries."
|
||||||
|
# "u <xy> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>"
|
||||||
|
# This is first to distinguish it from normal modifications et al.
|
||||||
|
contains -- unmerged $argv
|
||||||
|
and printf '%s\t%s\n' "$line[11..-1]" (_ "Unmerged file")
|
||||||
|
case '? .R*' '? R.*'
|
||||||
|
# Renamed/Copied
|
||||||
|
# From the docs: "Renamed or copied entries have the following format:"
|
||||||
|
# "2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path><sep><origPath>"
|
||||||
|
# Since <sep> is NUL, the <origPath> (meaning the old name) is in the next batch.
|
||||||
|
# TODO: Do we care about the new one?
|
||||||
|
set use_next renamed
|
||||||
|
continue
|
||||||
|
case '? .C*' '? C.*'
|
||||||
|
set use_next copied
|
||||||
|
continue
|
||||||
|
case '? A.*'
|
||||||
|
# Additions are only shown here if they are staged.
|
||||||
|
# Otherwise it's an untracked file.
|
||||||
|
contains -- added $argv; or contains -- all-staged $argv
|
||||||
|
and printf '%s\t%s\n' "$line[9..-1]" (_ "Added file")
|
||||||
|
case '? .M*'
|
||||||
|
# Modified
|
||||||
|
# From the docs: "Ordinary changed entries have the following format:"
|
||||||
|
# "1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>"
|
||||||
|
# Since <path> can contain spaces, print from element 9 onwards
|
||||||
|
contains -- modified $argv
|
||||||
|
and printf '%s\t%s\n' "$line[9..-1]" (_ "Modified file")
|
||||||
|
case '? M.*'
|
||||||
|
# If the character is first ("M."), then that means it's "our" change,
|
||||||
|
# which means it is staged.
|
||||||
|
# This is useless for many commands - e.g. `checkout` won't do anything with this.
|
||||||
|
# So it needs to be requested explicitly.
|
||||||
|
contains -- modified-staged $argv; or contains -- all-staged $argv
|
||||||
|
and printf '%s\t%s\n' "$line[9..-1]" (_ "Staged modified file")
|
||||||
|
case '? .D*'
|
||||||
|
contains -- deleted $argv
|
||||||
|
and printf '%s\t%s\n' "$line[9..-1]" (_ "Deleted file")
|
||||||
|
case '? D.*'
|
||||||
|
# TODO: The docs are unclear on this.
|
||||||
|
# There is both X unmodified and Y either M or D ("not updated")
|
||||||
|
# and Y is D and X is unmodified or [MARC] ("deleted in work tree").
|
||||||
|
# For our purposes, we assume this is a staged deletion.
|
||||||
|
contains -- deleted-staged $argv; or contains -- all-staged $argv
|
||||||
|
and printf '%s\t%s\n' "$line[9..-1]" (_ "Staged deleted file")
|
||||||
|
case '\? *'
|
||||||
|
# Untracked
|
||||||
|
# "? <path>" - print from element 2 on.
|
||||||
|
contains -- untracked $argv
|
||||||
|
and printf '%s\t%s\n' "$line[2..-1]" (_ "Untracked file")
|
||||||
|
case '! *'
|
||||||
|
# Ignored
|
||||||
|
# "! <path>" - print from element 2 on.
|
||||||
|
contains -- ignored $argv
|
||||||
|
and printf '%s\t%s\n' "$line[2..-1]" (_ "Ignored file")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function __fish_git_ranges
|
function __fish_git_ranges
|
||||||
|
@ -365,8 +456,9 @@ complete -c git -n '__fish_git_using_command add' -s N -l intent-to-add -d 'Reco
|
||||||
complete -c git -n '__fish_git_using_command add' -l refresh -d "Don't add the file(s), but only refresh their stat"
|
complete -c git -n '__fish_git_using_command add' -l refresh -d "Don't add the file(s), but only refresh their stat"
|
||||||
complete -c git -n '__fish_git_using_command add' -l ignore-errors -d 'Ignore errors'
|
complete -c git -n '__fish_git_using_command add' -l ignore-errors -d 'Ignore errors'
|
||||||
complete -c git -n '__fish_git_using_command add' -l ignore-missing -d 'Check if any of the given files would be ignored'
|
complete -c git -n '__fish_git_using_command add' -l ignore-missing -d 'Check if any of the given files would be ignored'
|
||||||
complete -f -c git -n '__fish_git_using_command add; and __fish_contains_opt -s p patch' -a '(__fish_git_modified_files)'
|
# Renames also show up as untracked + deleted, and to get git to show it as a rename _both_ need to be added.
|
||||||
complete -f -c git -n '__fish_git_using_command add' -a '(__fish_git_add_files)'
|
# However, we can't do that as it is two tokens, so we don't need renamed here.
|
||||||
|
complete -f -c git -n '__fish_git_using_command add' -a '(__fish_git_files modified untracked deleted)'
|
||||||
# TODO options
|
# TODO options
|
||||||
|
|
||||||
### checkout
|
### checkout
|
||||||
|
@ -376,7 +468,7 @@ complete -k -f -c git -n '__fish_git_using_command checkout' -a '(__fish_git_rem
|
||||||
complete -k -f -c git -n '__fish_git_using_command checkout' -a '(__fish_git_heads)' -d 'Head'
|
complete -k -f -c git -n '__fish_git_using_command checkout' -a '(__fish_git_heads)' -d 'Head'
|
||||||
complete -k -f -c git -n '__fish_git_using_command checkout' -a '(__fish_git_unique_remote_branches)' -d 'Remote branch'
|
complete -k -f -c git -n '__fish_git_using_command checkout' -a '(__fish_git_unique_remote_branches)' -d 'Remote branch'
|
||||||
complete -k -f -c git -n '__fish_git_using_command checkout' -a '(__fish_git_tags)' -d 'Tag'
|
complete -k -f -c git -n '__fish_git_using_command checkout' -a '(__fish_git_tags)' -d 'Tag'
|
||||||
complete -k -f -c git -n '__fish_git_using_command checkout' -a '(__fish_git_modified_files)' -d 'File'
|
complete -k -f -c git -n '__fish_git_using_command checkout' -a '(__fish_git_files modified deleted)'
|
||||||
complete -f -c git -n '__fish_git_using_command checkout' -s b -d 'Create a new branch'
|
complete -f -c git -n '__fish_git_using_command checkout' -s b -d 'Create a new branch'
|
||||||
complete -f -c git -n '__fish_git_using_command checkout' -s t -l track -d 'Track a new branch'
|
complete -f -c git -n '__fish_git_using_command checkout' -s t -l track -d 'Track a new branch'
|
||||||
complete -f -c git -n '__fish_git_using_command checkout' -l theirs -d 'Keep staged changes'
|
complete -f -c git -n '__fish_git_using_command checkout' -l theirs -d 'Keep staged changes'
|
||||||
|
@ -437,7 +529,7 @@ complete -f -c git -n '__fish_git_using_command clone' -l recursive -d 'Initiali
|
||||||
### commit
|
### commit
|
||||||
complete -c git -n '__fish_git_needs_command' -a commit -d 'Record changes to the repository'
|
complete -c git -n '__fish_git_needs_command' -a commit -d 'Record changes to the repository'
|
||||||
complete -c git -n '__fish_git_using_command commit' -l amend -d 'Amend the log message of the last commit'
|
complete -c git -n '__fish_git_using_command commit' -l amend -d 'Amend the log message of the last commit'
|
||||||
complete -f -c git -n '__fish_git_using_command commit' -a '(__fish_git_modified_files)'
|
complete -f -c git -n '__fish_git_using_command commit' -a '(__fish_git_files modified)'
|
||||||
complete -f -c git -n '__fish_git_using_command commit' -l fixup -d 'Fixup commit to be used with rebase --autosquash'
|
complete -f -c git -n '__fish_git_using_command commit' -l fixup -d 'Fixup commit to be used with rebase --autosquash'
|
||||||
complete -f -c git -n '__fish_git_using_command commit; and __fish_contains_opt fixup' -k -a '(__fish_git_recent_commits)'
|
complete -f -c git -n '__fish_git_using_command commit; and __fish_contains_opt fixup' -k -a '(__fish_git_recent_commits)'
|
||||||
# TODO options
|
# TODO options
|
||||||
|
@ -447,14 +539,14 @@ complete -c git -n '__fish_git_needs_command' -a diff -d 'Show changes between c
|
||||||
complete -c git -n '__fish_git_using_command diff' -a '(__fish_git_ranges)' -d 'Branch'
|
complete -c git -n '__fish_git_using_command diff' -a '(__fish_git_ranges)' -d 'Branch'
|
||||||
complete -c git -n '__fish_git_using_command diff' -l cached -d 'Show diff of changes in the index'
|
complete -c git -n '__fish_git_using_command diff' -l cached -d 'Show diff of changes in the index'
|
||||||
complete -c git -n '__fish_git_using_command diff' -l no-index -d 'Compare two paths on the filesystem'
|
complete -c git -n '__fish_git_using_command diff' -l no-index -d 'Compare two paths on the filesystem'
|
||||||
complete -f -c git -n '__fish_git_using_command diff' -a '(__fish_git_modified_files)' -d 'File'
|
complete -f -c git -n '__fish_git_using_command diff' -a '(__fish_git_files modified deleted)'
|
||||||
# TODO options
|
# TODO options
|
||||||
|
|
||||||
### difftool
|
### difftool
|
||||||
complete -c git -n '__fish_git_needs_command' -a difftool -d 'Open diffs in a visual tool'
|
complete -c git -n '__fish_git_needs_command' -a difftool -d 'Open diffs in a visual tool'
|
||||||
complete -c git -n '__fish_git_using_command difftool' -a '(__fish_git_ranges)' -d 'Branch'
|
complete -c git -n '__fish_git_using_command difftool' -a '(__fish_git_ranges)' -d 'Branch'
|
||||||
complete -c git -n '__fish_git_using_command difftool' -l cached -d 'Visually show diff of changes in the index'
|
complete -c git -n '__fish_git_using_command difftool' -l cached -d 'Visually show diff of changes in the index'
|
||||||
complete -f -c git -n '__fish_git_using_command difftool' -a '(__fish_git_modified_files)' -d 'File'
|
complete -f -c git -n '__fish_git_using_command difftool' -a '(__fish_git_files modified deleted)'
|
||||||
# TODO options
|
# TODO options
|
||||||
|
|
||||||
|
|
||||||
|
@ -699,24 +791,15 @@ complete -f -c git -n '__fish_git_using_command merge' -l abort -d 'Abort the cu
|
||||||
function __fish_git_mergetools
|
function __fish_git_mergetools
|
||||||
set -l tools diffuse diffmerge ecmerge emerge kdiff3 meld opendiff tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc codecompare
|
set -l tools diffuse diffmerge ecmerge emerge kdiff3 meld opendiff tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc codecompare
|
||||||
for tool in $tools
|
for tool in $tools
|
||||||
if command --search $tool >/dev/null
|
if command -sq $tool
|
||||||
echo "$tool"
|
echo "$tool"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns list of files with status:
|
|
||||||
# "UU"=unmerged "\?\?"=untracked "M "=staged " M"=changed, not staged "MM"=staged and changed locally
|
|
||||||
function __fish_git_status --argument-names "statusmarker"
|
|
||||||
for line in (git status -s)
|
|
||||||
set -l filename (string replace -r "^$statusmarker\s+" "" $line)
|
|
||||||
and echo $filename
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
complete -f -c git -n '__fish_git_needs_command' -a mergetool -d 'Run merge conflict resolution tools to resolve merge conflicts'
|
complete -f -c git -n '__fish_git_needs_command' -a mergetool -d 'Run merge conflict resolution tools to resolve merge conflicts'
|
||||||
complete -f -c git -n '__fish_git_using_command mergetool' -s t -l tool -d "Use specific merge resolution program" -a "(__fish_git_mergetools)"
|
complete -f -c git -n '__fish_git_using_command mergetool' -s t -l tool -d "Use specific merge resolution program" -a "(__fish_git_mergetools)"
|
||||||
complete -f -c git -n '__fish_git_using_command mergetool' -a "(__fish_git_status 'UU')" -d "File"
|
complete -f -c git -n '__fish_git_using_command mergetool' -a "(__fish_git_files unmerged)"
|
||||||
|
|
||||||
|
|
||||||
### mv
|
### mv
|
||||||
|
@ -799,8 +882,10 @@ complete -f -c git -n '__fish_git_using_command rebase' -l no-ff -d 'No fast-for
|
||||||
complete -c git -n '__fish_git_needs_command' -a reset -d 'Reset current HEAD to the specified state'
|
complete -c git -n '__fish_git_needs_command' -a reset -d 'Reset current HEAD to the specified state'
|
||||||
complete -f -c git -n '__fish_git_using_command reset' -l hard -d 'Reset files in working directory'
|
complete -f -c git -n '__fish_git_using_command reset' -l hard -d 'Reset files in working directory'
|
||||||
complete -c git -n '__fish_git_using_command reset' -a '(__fish_git_branches)' -d 'Branch'
|
complete -c git -n '__fish_git_using_command reset' -a '(__fish_git_branches)' -d 'Branch'
|
||||||
# reset changes the index, so we need to compare that to the commit.
|
# reset can either undo changes to versioned modified files,
|
||||||
complete -f -c git -n '__fish_git_using_command reset' -a '(__fish_git_modified_files --staged)' -d 'File'
|
# or remove files from the staging area.
|
||||||
|
# TODO: Deleted files seem to need a "--" separator.
|
||||||
|
complete -f -c git -n '__fish_git_using_command reset' -a '(__fish_git_files all-staged modified)'
|
||||||
complete -f -c git -n '__fish_git_using_command reset' -a '(__fish_git_reflog)' -d 'Reflog'
|
complete -f -c git -n '__fish_git_using_command reset' -a '(__fish_git_reflog)' -d 'Reflog'
|
||||||
# TODO options
|
# TODO options
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue