Git bash-completion with filename support?

后端 未结 4 1290
独厮守ぢ
独厮守ぢ 2020-12-09 12:28

is there a bash-completion script that supports filename completion? I use mostly mercurial and there I can type:

hg diff test/test_
相关标签:
4条回答
  • 2020-12-09 12:40

    Since 2011, as the OP comments, Git supports full filename completion since ~1.8.2.

    But with Git 2.18 (Q2 2018), the shell completion (in contrib/) that gives list of paths have been optimized somewhat.

    See commit 78a2d21 (04 Apr 2018) by Clemens Buchacher (drizzd).
    (Merged by Junio C Hamano -- gitster -- in commit 3a940e9, 25 Apr 2018)

    completion: improve ls-files filter performance

    From the output of ls-files, we remove all but the leftmost path component and then we eliminate duplicates. We do this in a while loop, which is a performance bottleneck when the number of iterations is large (e.g. for 60000 files in linux.git).

    $ COMP_WORDS=(git status -- ar) COMP_CWORD=3; time _git
    
    real    0m11.876s
    user    0m4.685s
    sys     0m6.808s
    

    Replacing the loop with the cut command improves performance significantly:

    $ COMP_WORDS=(git status -- ar) COMP_CWORD=3; time _git
    
    real    0m1.372s
    user    0m0.263s
    sys     0m0.167s
    

    The measurements were done with Msys2 bash, which is used by Git for Windows.

    When filtering the ls-files output we take care not to touch absolute paths. This is redundant, because ls-files will never output absolute paths. Remove the unnecessary operations.

    The issue was originally reported Git for Windows issue 1533.


    The directory traversal code had redundant recursive calls which made its performance characteristics exponential with respect to the depth of the tree, which was corrected with Git 2.27 (Q2 2020).

    See commit c0af173, commit 95c11ec, commit 7f45ab2, commit 1684644, commit 8d92fb2, commit 2df179d, commit 0126d14, commit cd129ee, commit 446f46d, commit 7260c7b, commit ce5c61a (01 Apr 2020) by Elijah Newren (newren).
    See commit 0bbd0e8 (01 Apr 2020) by Derrick Stolee (derrickstolee).
    (Merged by Junio C Hamano -- gitster -- in commit 6eacc39, 29 Apr 2020)

    completion: fix 'git add' on paths under an untracked directory

    Signed-off-by: Elijah Newren

    As reported on the git mailing list, since git-2.25,

    git add untracked-dir/
    

    has been tab completing to

    git add untracked-dir/./
    

    The cause for this was that with commit b9670c1f5e ("dir: fix checks on common prefix directory", 2019-12-19, Git v2.25.0-rc0 -- merge),

    git ls-files -o --directory untracked-dir/
    

    (or the equivalent git -C untracked-dir ls-files -o --directory) began reporting

    untracked-dir/
    

    instead of listing paths underneath that directory.

    It may also be worth noting that the real command in question was

    git -C untracked-dir ls-files -o --directory '*'
    

    which is equivalent to:

    git ls-files -o --directory 'untracked-dir/*'
    

    which behaves the same for the purposes of this issue (the '*' can match the empty string), but becomes relevant for the proposed fix.

    At first, based on the report, I decided to try to view this as a regression and tried to find a way to recover the old behavior without breaking other stuff, or at least breaking as little as possible.
    However, in the end, I couldn't figure out a way to do it that wouldn't just cause lots more problems than it solved.

    The old behavior was a bug:

    • Although older git would avoid cleaning anything with git clean -f .git, it would wipe out everything under that directory with git clean -f .git/.
      Despite the difference in command used, this is relevant because the exact same change that fixed clean changed the behavior of ls-files.
    • Older git would report different results based solely on presence or absence of a trailing slash for $SUBDIR in the command git ls-files -o --directory $SUBDIR.
    • Older git violated the documented behavior of not recursing into directories that matched the pathspec when --directory was specified.
    • And, after all, commit b9670c1f5e (dir: fix checks on common prefix directory, 2019-12-19, Git v2.25.0-rc0) didn't overlook this issue; it explicitly stated that the behavior of the command was being changed to bring it inline with the docs.

    (Also, if it helps, despite that commit being merged during the 2.25 series, this bug was not reported during the 2.25 cycle, nor even during most of the 2.26 cycle -- it was reported a day before 2.26 was released.

    So the impact of the change is at least somewhat small.)

    Instead of relying on a bug of ls-files in reporting the wrong content, change the invocation of ls-files used by git-completion to make it grab paths one depth deeper.

    Do this by changing '$DIR/*' (match $DIR/ plus 0 or more characters) into '$DIR/?*' (match $DIR/ plus 1 or more characters).

    Note that the '?' character should not be added when trying to complete a filename (e.g. 'git ls-files -o --directory merge.c?*"' would not correctly return "[merge.c](https://github.com/git/git/blob/c0af173a136785b3cfad4bd414b2fb10a130760a/merge.c)" when such a file exists), so we have to make sure to add the '?`' character only in cases where the path specified so far is a directory.


    Warning: Git 2.29 (Q4 2020) fixes a regression introduced during 2.27 cycle.

    See commit cada730 (20 Jul 2020) by Martin Ågren (none).
    (Merged by Junio C Hamano -- gitster -- in commit 82fafc7, 30 Jul 2020)

    dir: check pathspecs before returning path_excluded

    Reported-by: Andreas Schwab
    Reviewed-by: Elijah Newren
    Signed-off-by: Martin Ågren

    In 95c11ecc73 ("Fix error-prone fill_directory() API; make it only return matches", 2020-04-01, Git v2.27.0-rc0 -- merge listed in batch #5), we taught fill_directory(), or more specifically treat_path(), to check against any pathspecs so that we could simplify the callers.

    But in doing so, we added a slightly-too-early return for the "excluded" case. We end up not checking the pathspecs, meaning we return path_excluded when maybe we should return path_none. As a result, git status --ignored -- pathspec(man) might show paths that don't actually match "pathspec".

    Move the "excluded" check down to after we've checked any pathspecs.

    0 讨论(0)
  • 2020-12-09 12:46

    Not really your desired answer but I wanted to let you know that soon fish (friendly interactive shell) will give you git filename completion support out of the box. It is currently in master with a 2.3.0 release coming soon.

    https://github.com/fish-shell/fish-shell/issues/901
    https://github.com/fish-shell/fish-shell/pull/2364
    https://github.com/fish-shell/fish-shell/commit/c5c59d4acb00674bc37198468b5978f69484c628

    If you have a status like this:

    $ git status
    modified: ../README.md
    $ git add <tab>
    :/README.md 
    

    You can also just type README and hit tab and it will insert it for you if it is the only match. Friggin nice!

    0 讨论(0)
  • 2020-12-09 12:55

    So, let's see how the Mercurial bash completion script does this.

    This is the important part:

    _hg_status()
    {
        local files="$(_hg_cmd status -n$1 .)"
        local IFS=$'\n'
        COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur"))
    }
    

    It gets called here:

    _hg_command_specific()
    {
        case "$cmd" in 
        [...]
        diff)
            _hg_status "mar"
        ;;
        [...]
        esac
        return 0
    }
    

    Thus, it is simply a call of hg status -nmar, and using the output as a list of files for completion.

    I think it would not be too hard to patch something similar into the git completion script - we would have to modify __git_diff here to not do a plain filename + branch completion, but calling git status instead.


    The commands

    git status --porcelain | grep '^.[^ ?]' | cut -b 4-
    

    (for git diff --cached) and

    git status --porcelain | grep '^[^ ?]' | cut -b 4-
    

    (for git diff) seem to output the right thing (if there are no renames).

    They both are not useful when diffing to anything other than HEAD, though.

    A more general way would be to use

    git diff --relative --name-only [--cached] [commit1] [commit2]]
    

    where commit1 and commit2 (and maybe --cached) come from the already given diff command line.


    I implemented the idea outlined above in bash, and patched into git-completion.bash. If you don't want to change your git-completion.bash, add these two functions to some bash file and source it after the original git-completion.bash. It should now work with commands like

    git diff -- <tab>
    git diff --cached -- <tab>
    git diff HEAD^^ -- <tab>
    git diff origin/master master -- <tab>
    

    I submitted this as a patch to the git mailing list, let's see what results from this. (I'll update this answer as I get feedback there.)

    # Completion for the file argument for git diff.
    # It completes only files actually changed. This might be useful
    # as completion for other commands as well.
    #
    # The idea comes from the bash completion for Mercurial (hg),
    # which does something similar (but more simple, only difference of
    # working directory to HEAD and/or index, if I understand right).
    # It (the idea) was brought to us by the question
    #      http://stackoverflow.com/q/6034472/600500
    #  from "olt".
    __git_complete_changed_files()
    {
      #
      # We use "git diff --name-only --relative" to generate the list,
      # but this needs the same --cached and <commit> arguments as the
      # command line being constructed.
      #
    
    
        # first grab arguments like --cached and any commit arguments.
    
        local -a args=()
        local finish=false
    
        for (( i=1 ; i < cword ; i++)) do
        local current_arg=${words[$i]}
        #  echo checking $current_arg >&2
           case $current_arg in
               --cached)
                   args+=( $current_arg )
                   ;;
               --)
                   # finish parsing arguments, the rest are file names
                   break
                   ;;
               -*)
                   # other options are ignored
                   ;;
               *)
                   if git cat-file -e $current_arg 2> /dev/null
                   then
                       case $( git cat-file -t $current_arg ) in
                           commit|tag)
                           # commits and tags are added to the command line.
                               args+=( $current_arg )
                               # echo adding $current_arg >&2
                               ;;
                           *)
                       esac
                   fi
                   ;;
           esac
        done
    
        # now we can call `git diff`
    
        COMPREPLY=( $( compgen \
            -W "$( git diff --name-only --relative "${args[@]}" -- )" -- $cur ) )
    }
    
    _git_diff ()
    {
        if __git_has_doubledash
        then
            # complete for the file part: only changed files
            __git_complete_changed_files
        else
        case "$cur" in
        --*)
            __gitcomp "--cached --staged --pickaxe-all --pickaxe-regex
                --base --ours --theirs --no-index
                $__git_diff_common_options
                "
            return
            ;;
        esac
        __git_complete_revlist_file
        fi
    }
    

    Update: Looks like this patch is not wanted in this form, as the current way to complete files is more useful for people which want to check whether there are changes in some subdirectory (e.g. completing when the diff output could be empty). It might be accepted if linked to some configuration variable (with the default being the current behavior). Also, the indenting should be adapted to the standard (see the answer from Junio C Hamano).

    I might take another go on it, but can't guarantee this for the near future. If someone else want to do, feel free to take my code, change it and submit it again.

    0 讨论(0)
  • 2020-12-09 12:59

    This solves the git diff <tab> problem for me, put the following in .bashrc:

    alias gid='git diff'
    __gdiff () {
        local cur prev opts
        COMPREPLY=()
        cur="${COMP_WORDS[COMP_CWORD]}"
        prev="${COMP_WORDS[COMP_CWORD-1]}"
        opts=$(git status --porcelain | grep '^.[^ ?]' | cut -b 4-)
    
        case "${prev}" in
            gid)
                COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
                ;;
        esac
    }
    complete -F __gdiff gid
    

    And then do gid <tab> instead of git diff <tab>. It may be simplified, but seems to work well as a quick fix.

    0 讨论(0)
提交回复
热议问题