[completions/git] Allow relative paths for files

This is _ugly_ because it is implemented for both status --porcelain
formats, but it is both fast and accurate.
This commit is contained in:
Fabian Homborg 2018-06-15 14:05:15 +02:00
parent f25a1d720a
commit 7b86062720

View file

@ -89,74 +89,194 @@ function __fish_git_files
# Save the repo root to remove it from the path later. # Save the repo root to remove it from the path later.
set -l root (command git rev-parse --show-toplevel 2>/dev/null) set -l root (command git rev-parse --show-toplevel 2>/dev/null)
# Do not continue if not inside a Git repository
or return or return
# Cache the translated descriptions so we don't have to get it # Cache the translated descriptions so we don't have to get it
# once per file. # once per file.
# This is slightly slower for < 8 files, but that is fast enough anyway. contains -- unmerged $argv; and set -l unmerged
set -l unmerged_desc (_ "Unmerged File") and set -l unmerged_desc (_ "Unmerged File")
set -l added_desc (_ "Added file") contains -- added $argv; or contains -- all-staged $argv; and set -l added
set -l modified_desc (_ "Modified file") and set -l added_desc (_ "Added file")
set -l staged_modified_desc (_ "Staged modified file") contains -- modified $argv; and set -l modified
set -l deleted_desc (_ "Deleted file") and set -l modified_desc (_ "Modified file")
set -l staged_deleted_desc (_ "Staged deleted file") contains -- untracked $argv; and set -l untracked
set -l untracked_desc (_ "Untracked file") and set -l untracked_desc (_ "Untracked file")
set -l ignored_desc (_ "Ignored file") contains -- modified-staged $argv; or contains -- all-staged $argv; and set -l modified_staged
and set -l staged_modified_desc (_ "Staged modified file")
contains -- deleted $argv; and set -l deleted
and set -l deleted_desc (_ "Deleted file")
contains -- deleted-staged $argv; or contains -- all-staged $argv; and set -l deleted_staged
and set -l staged_deleted_desc (_ "Staged deleted file")
contains -- ignored $argv; and set -l ignored
and set -l ignored_desc (_ "Ignored file")
contains -- renamed $argv; and set -l renamed
and set -l renamed_desc (_ "Renamed file")
contains -- copied $argv; and set -l copied
and set -l copied_desc (_ "Copied file")
# A literal "?" for use in `case`.
set -l q '\\?'
if status test-feature qmark-noglob
set q '?'
end
set -l use_next
# git status --porcelain gives us all the info we need, in a format we don't. # git status --porcelain gives us all the info we need, in a format we don't.
# The v2 format has better documentation and doesn't use " " to denote anything, # The v2 format has better documentation and doesn't use " " to denote anything,
# but it's only been added in git 2.11.0, which was released November 2016. # but it's only been added in git 2.11.0, which was released November 2016.
# Instead, we use the v1 format, without explicitly specifying it (since that errors out as well).
#
# Also, we ignore submodules because they aren't useful as arguments (generally), # Also, we ignore submodules because they aren't useful as arguments (generally),
# and they slow things down quite significantly. # and they slow things down quite significantly.
# E.g. `git reset $submodule` won't do anything (not even print an error). # E.g. `git reset $submodule` won't do anything (not even print an error).
# --ignore-submodules=all was added in git 1.7.2, released July 2010. # --ignore-submodules=all was added in git 1.7.2, released July 2010.
set -l use_next #
command git status --porcelain -z --ignore-submodules=all \ set -l status_opt --ignore-submodules=all
| while read -lz -d '' line
# If we aren't looking for untracked/ignored files, let git status skip them.
set -q untracked; and set -a status_opt -unormal
or set -a status_opt -uno
set -q ignored; and set -a status_opt --ignored=matching
or set -a status_opt --ignored=no
# We pick the v2 format if we can, because it shows relative filenames (if used without "-z").
# We fall back on the v1 format by reading git's _version_, because trying v2 first is too slow.
set -l ver (command git --version | string replace -rf 'git version (\d+)\.(\d+)\.?.*' '$1\n$2')
# Version >= 2.11.* has the v2 format.
if test "$ver[1]" -gt 2 2>/dev/null; or test "$ver[1]" -eq 2 -a "$ver[2]" -ge 11 2>/dev/null
# We need to set status.relativePaths to true because we want relative paths,
# and core.quotePath to false so characters > 0x80 (i.e. non-ASCII) aren't escaped.
command git -c status.relativePaths -c core.quotePath= status --porcelain=2 $status_opt \
| while read -la -d ' ' line
set -l file
set -l desc
# 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.
set -ql unmerged
and set file "$line[11..-1]"
and set desc $unmerged_desc
case '2 .R*' '2 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 \t, we can't really parse it unambiguously.
# The "-z" format would be great here!
set -ql renamed
and set file (string replace -r '\t[^\t]*' '' -- "$line[10..-1]")
and set desc $renamed_desc
case '2 .C*' '2 C.*'
set -ql copied
and set file (string replace -r '\t[^\t].*' '' -- "$line[10..-1]")
and set desc $copied_desc
case '1 A.*'
# Additions are only shown here if they are staged.
# Otherwise it's an untracked file.
set -ql added
and set file "$line[9..-1]"
and set desc $added_desc
case '1 .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
set -ql modified
and set file "$line[9..-1]"
and set desc $modified_desc
case '1 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.
set -ql modified_staged
and set file "$line[9..-1]"
and set desc $staged_modified_desc
case '1 .D*'
set -ql deleted
and set file "$line[9..-1]"
and set desc $deleted_desc
case '1 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.
set -ql deleted-staged; or set -ql all-staged
and set file "$line[9..-1]"
and set desc $staged_deleted_desc
case "$q"' *'
# Untracked
# "? <path>" - print from element 2 on.
set -ql untracked
and set file "$line[2..-1]"
and set desc $untracked_desc
case '! *'
# Ignored
# "! <path>" - print from element 2 on.
set -ql ignored
and set file "$line[2..-1]"
and set desc $ignored_desc
end
# Only try printing if the file was selected.
if set -q file[1]
# Without "-z", git sometimes _quotes_ filenames.
# It adds quotes around it _and_ escapes the character.
# e.g. `"a\\b"`.
# We just remove the quotes and hope it works out.
# If this contains newlines or tabs,
# there is nothing we can do, but that's a general issue with scripted completions.
set file (string trim -c \" -- $file)
# First the relative filename.
printf '%s\t%s\n' "$file" $desc
# Now from repo root.
set -l fromroot (builtin realpath -- $file 2>/dev/null)
and set fromroot (string replace -- "$root/" ":/" "$fromroot")
and printf '%s\t%s\n' "$fromroot" $desc
end
end
else
# v1 format logic
# We need to compute relative paths on our own, which is slow.
# Pre-remove the root at least, so we have fewer components to deal with.
set -l _pwd_list (string replace "$root/" "" -- $PWD | string split /)
# Cache the previous relative path because these are sorted, so we can reuse it
# often for files in the same directory.
set -l previous
set -l previousfile
# Note that we can't use space as a delimiter between status and filename, because
# the status can contain spaces - " M" is different from "M ".
command git -c core.quotePath= status --porcelain -z $status_opt \
| while read -lz line
set -l desc
# The entire line is the "from" from a rename. # The entire line is the "from" from a rename.
if set -q use_next[1] if set -q use_next[1]
if contains -- $use_next $argv if contains -- $use_next $argv
string replace -f -- "$PWD/" "" "$root/$line" set -l var "$use_next"_desc
or string replace -- "$root/" ":/" "$root/$line" set desc $$var
end set -e use_next[1]
else
set -e use_next[1] set -e use_next[1]
continue continue
end end
end
# The format is two characters for status, then a space and then # The format is two characters for status, then a space and then
# up to a NUL for the filename. # up to a NUL for the filename.
# #
# Use IFS to handle newlines in filenames.
set -l IFS
set -l stat (string sub -l 2 -- $line) set -l stat (string sub -l 2 -- $line)
set -l file (string sub -s 4 -- $line)
# 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
set file (string replace -f -- "$PWD/" "" "$root/$file"; or string replace -- "$root/" ":/" "$root/$file")
set -e IFS
# The basic status format is "XY", where X is "our" state (meaning the staging area), # The basic status format is "XY", where X is "our" state (meaning the staging area),
# and "Y" is "their" state (meaning the work tree). # and "Y" is "their" state (meaning the work tree).
# A " " means it's unmodified. # A " " means it's unmodified.
# #
# Be careful about the ordering here! # Be careful about the ordering here!
#
# HACK: To allow this to work both with and without '?' globs
set -l dq '\\?\\?'
if status test-feature qmark-noglob
# ? is not a glob
set dq '??'
end
switch "$stat" switch "$stat"
case DD AU UD UA DU AA UU case DD AU UD UA DU AA UU
# Unmerged # Unmerged
# TODO: It might be useful to split this up. set -ql unmerged
contains -- unmerged $argv and set desc $unmerged_desc
and printf '%s\t%s\n' "$file" $unmerged_desc
case 'R ' RM RD case 'R ' RM RD
# Renamed/Copied # Renamed/Copied
# These have the "from" name as the next batch. # These have the "from" name as the next batch.
@ -169,40 +289,64 @@ function __fish_git_files
case 'A ' AM AD case 'A ' AM AD
# Additions are only shown here if they are staged. # Additions are only shown here if they are staged.
# Otherwise it's an untracked file. # Otherwise it's an untracked file.
contains -- added $argv set -ql added
or contains -- all-staged $argv and set desc $added_desc
and printf '%s\t%s\n' "$file" $added_desc
case '*M' case '*M'
# Modified # Modified
contains -- modified $argv set -ql modified
and printf '%s\t%s\n' "$file" $modified_desc and set desc $modified_desc
case 'M*' case 'M*'
# If the character is first ("M "), then that means it's "our" change, # If the character is first ("M "), then that means it's "our" change,
# which means it is staged. # which means it is staged.
# This is useless for many commands - e.g. `checkout` won't do anything with this. # This is useless for many commands - e.g. `checkout` won't do anything with this.
# So it needs to be requested explicitly. # So it needs to be requested explicitly.
contains -- modified-staged $argv set -ql modified_staged
or contains -- all-staged $argv and set desc $staged_modified_desc
and printf '%s\t%s\n' "$file" $staged_modified_desc
case '*D' case '*D'
contains -- deleted $argv set -ql deleted
and printf '%s\t%s\n' "$file" $deleted_desc and set desc $deleted_desc
case 'D*' case 'D*'
# TODO: The docs are unclear on this. # TODO: The docs are unclear on this.
# There is both X unmodified and Y either M or D ("not updated") # 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"). # and Y is D and X is unmodified or [MARC] ("deleted in work tree").
# For our purposes, we assume this is a staged deletion. # For our purposes, we assume this is a staged deletion.
contains -- deleted-staged $argv set -ql deleted_staged
or contains -- all-staged $argv and set desc $staged_deleted_desc
and printf '%s\t%s\n' "$file" $staged_deleted_desc case "$q$q"
case "$dq" # a literal '??'
# Untracked # Untracked
contains -- untracked $argv set -ql untracked
and printf '%s\t%s\n' "$file" $untracked_desc and set desc $untracked_desc
case '!!' case '!!'
# Ignored # Ignored
contains -- ignored $argv set -ql ignored
and printf '%s\t%s\n' "$file" $ignored_desc and set desc $ignored_desc
end
if set -q desc[1]
# Again: "XY filename", so the filename starts on character 4.
set -l relfile (string sub -s 4 -- $line)
# The filename with ":/" prepended.
set -l file (string replace -- "$root/" ":/" "$root/$relfile")
# Computing relative path by hand.
set -l abs (string split / -- $relfile)
# If it's in the same directory, we just need to change the filename.
if test "$abs[1..-2]" = "$previousfile[1..-2]"
set previous[-1] $abs[-1]
else
set -l pwd_list $_pwd_list
set previousfile $abs
# Remove common prefix
while test "$pwd_list[1]" = "$abs[1]"
set -e pwd_list[1]
set -e abs[1]
end
# Go a dir up for every entry left in pwd_list, then into $abs
set previous (string replace -r '.*' '..' -- $pwd_list) $abs
end
set -a file (string join / -- $previous)
printf '%s\n' $file\t$desc
end
end end
end end
end end