tests: move code to run tests under bash into a helper library

The following patch will add tests for the bash prompt functions as a
new test script, which also has to be run under bash.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
tests: add tests for the bash prompt functions in the completion script

The tests cover the discovery of the '.git' directory in the
__gitdir() function in different scenarios, and the prompt itself,
i.e. branch name, detached heads, operations (rebase, merge,
cherry-pick, bisect), and status indicators (dirty, stash, untracked
files; but not the upstream status).

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
completion: use __gitdir() in _git_log()

The standard way to find out the path to the repository in the
completion script is the __gitdir() helper function, because that
handles the repository path given on the command line (i.e. git
--git-dir=/path/to/repo log --<TAB>).  However, there is one
exception: the completion function for 'git log' still uses 'git
rev-parse --git-dir' directly, and could offer (or not) the '--merge'
option erroneously when the repository is specified on the command
line.

Use __gitdir() there, too.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
completion: respect $GIT_DIR

The __gitdir() helper function finds out the path of the git
repository by running 'git rev-parse --git-dir'.  However, it has a
shortcut first to avoid the overhead of running a git command in a
subshell when the current directory is at the top of the work tree,
i.e. when it contains a '.git' subdirectory.

If the 'GIT_DIR' environment variable is set then it specifies the
path to the git repository, and the autodetection of the '.git'
directory is not necessary.  However, $GIT_DIR is only taken into
acocunt by 'git rev-parse --git-dir', and the check for the '.git'
subdirectory is performed first, so it wins over the path given in
$GIT_DIR.

There are several completion (helper) functions that depend on
__gitdir(), and when the above case triggers the completion script
will do weird things, like offering refs, aliases, or stashes from a
different repository, or displaying wrong or broken prompt, etc.

So check first whether $GIT_DIR is set, and only proceed with checking
the '.git' directory in the current directory if it isn't.  'git
rev-parse' would also check whether the path in $GIT_DIR is a proper
'.git' directory, i.e. 'HEAD', 'refs/', and 'objects/' are present and
accessible, but we don't have to be that thorough for the bash prompt.
And we've lived with an equally permissive check for '.git' in the
current working directory for years anyway.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: don't show the prompt when .git/HEAD is unreadable

__git_ps1() has a chain of commands to figure out what to display in
the prompt as current branch, i.e. the branch name, or a described
detached head, or the 7 hexdigits abbreviated object name, or
"(unknown)" when all of the above fails.

Now, when the 7 hexdigits case fails, then '/path/to/.git/HEAD' can't
be read.  This can happen when the file became unreadable after
__gitdir() found it: unlikely, but a parallel process can racily
delete it or change it's permissions in that short timeframe.
Alternatively, it is possible that either the subshell or the 'cut'
command fail.  Either way, when HEAD is not readable, then the path is
not considered to be a git repository, therefore the bash prompt
shouldn't be displayed at all.  And there is no point in continuing
the execution of __git_ps1(), because its subsequent git commands will
error out anyway.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: return early from __git_ps1() when not in a git repository

... to gain one level of indentation for the bulk of the function.

(The patch looks quite unreadable, you'd better check it with 'git
completion: make __gitdir() store repository path in $__git_dir

The __gitdir() helper function is responsible for finding out the path
to the repository, and it's invoked from several completion (helper)
functions and from __git_ps1().  The path of the repository is printed
to stdout, therefore it's invoked in a command substitution.  This has
the following drawbacks:

  1. The command substitution involves the forking of a subshell,
     which has a considerable overhead.

  2. There are a few cases, where __gitdir() is called more than once
     during a single completion, which means multiple command
     substitutions and eventual multiple 'git rev-parse --git-dir'
     executions.  For example, __gitdir() is invoked twice during the
     completion of options for 'gitk', 'git log', 'git rebase', but
     for certain aliases it might be invoked three times.

Now, _git(), the top-level git completion function declares the local
$__git_dir variable for storing the repository path given as parameter
to the '--git-dir=' option.  Since $__git_dir is declared at the
top-level, it is already available in all completion functions, but
it's only used in __gitdir() (if set then there's no need to
autodetect the path to the repository).  This means that we can use
$__git_dir to always store the path to the repository found by
__gitdir().

This way we can address both of the above drawbacks.  We won't need
the command substitution when invoking __gitdir(), because won't need
__gitdir()'s output anymore: the path to the repository will be
available in $__git_dir.  Furthermore, onlyt the first __gitdir()
invocation will perform the search for the repository, and all
subsequent calls will see the path already stored in $__git_dir by the
first call.

So change __gitdir() to store the path to the repository in
$__git_dir.  However, still print the found path, because there might
be user-supplied completion functions relying on it.

Also declare $__git_dir as local in __git_ps1() and _gitk() to prevent
the variable from leaking into the environment when they call
__gitdir() (that would break completion and bash prompt when the user
moves to a different git repository).

Finally, extend the __gitdir() tests to check the value of $__git_dir,
too.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
completion: use $__git_dir instead of $(__gitdir)

The previous commit changed __gitdir() to store the repository path in
the $__git_dir variable.  Now we change all call sites to just call
__gitdir() directly and then use $__git_dir instead of doing
'dir="$(__gitdir)"' command substitution, thereby sparing the overhead
of fork()ing a subshell.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
completion: platform-specific helper function to get physical path

Two upcoming optimizations will replace invocations of git commands in
command substitutions with bash builtins examining the path of the
current working directory.  Since git works with physical paths, we
can't use logical path provided in $PWD, but have to resort to the
'$(pwd -P)' command substitution to get the physical path of the
current directory.

However, on platforms not supporting symbolic links, such as MinGW,
the path in $PWD is bound to be the physical path.  So on those
platforms we could avoid the command substitution and use $PWD
directly.  Great for MinGW, because the overhead of forking a subshell
is relatively large there.

So add a platform-specific helper function to get the physical path of
the current directory: on MinGW it's defined such that it gets the
physical path from $PWD, while on other platforms from '$(pwd -P)'.
The path is stored in a variable whose name is passed as argument, so
no command substitution is needed when invoking this function.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
completion: use bash builtins to search for repository

When not at the top of the work tree or when no path is specified by
'git --git-dir=...' or $GIT_DIR, the __gitdir() helper function finds
out the physical path to the '.git' directory by running the '$(git
rev-parse --git-dir)' command substitution.  fork()ing a subshell and
fork()+exec()ing a git command take some time; during the same amount
of time we can stat a lot of files and directories using bash
builtins, likely finding the '.git' directory faster.

This patch extends __gitdir() to search for a '.git' directory using
only bash builtins.  Like the existing check for '.git' in the current
working directory or the previously added check for $GIT_DIR, this
search is not that thorough either, as it doesn't check whether the
found '.git' directory or the directory pointed to by a gitfile is a
valid '.git' repository.

Since git doesn't search for a .git directory beyond filesystem
boundaries and beyond paths specified in $GIT_CEILING_DIRECTORIES, the
search in __gitdir() should not do that either.  However, bash doesn't
provide builtins to check that two paths are on the same filesystem,
so we can't limit the search cheaply to one filesystem.  Therefore,
__gitdir() will only use bash builtins when neither of these limits
are active, i.e. $GIT_DISCOVERY_ACROSS_FILESYSTEM is set and
$GIT_CEILING_DIRECTORIES is empty, otherwise it will fall back on
executing 'git rev-parse'.  $GIT_CEILING_DIRECTORIES is empty by
default, but users have to set $GIT_DISCOVERY_ACROSS_FILESYSTEM
explicitly to enable this optimisation, when they find its
consequences acceptable.

Of course, stat()ing in C is much faster than in bash, so there is a
point when bash builtins will be slower than '$(git rev-parse
--git-dir)' despite all the fork()s+exec() overhead.  On MinGW this
overhead is considerable, and the builtins version is faster even at a
depth of 50 directories.  On Linux the builtins version is only faster
for paths less than 10 directories deeper than the toplevel; with my
usage patterns that covers 99.9% of the prompts displayed.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: use bash builtins to find out current branch

__git_ps1() runs the '$(git symbolic-ref HEAD)' command substitution
to find out whether we are on a branch and to find out the name of
that branch.  This imposes the overhead of fork()ing a subshell and
fork()+exec()ing a git process.

Since HEAD is a single-line file and the symbolic ref format is quite
simple to recognize and parse, read and parse it using only bash
builtins, thereby sparing all that fork()+exec() overhead.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: use bash builtins to check whether inside git dir

__git_ps1() runs the '$(git rev-parse --is-inside-git-dir)' command
substitution to check whether we are inside a .git directory and the
bash prompt needs to be adjusted accordingly (i.e. display 'BARE!' or
'GIT_DIR!').  This imposes the overhead of fork()ing a subshell and
fork()+exec()ing a git process.

Perform this check by comparing the path to the repository and the
current directory using only bash builtins, thereby sparing all that
fork()+exec() overhead.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: check whether inside the worktree only when necessary

Besides the current branch name or detached head info __git_ps1() can
also display status indicators in the prompt for changes in the work
tree, stashes, untracked files, and divergence from upstream.  These
are only displayed when enabled by specific environment variables and
when we are in a work tree.  The latter condition is checked by the
'$(git rev-parse --is-inside-work-tree)' command substitution,
impossing the overhead of fork()ing a subshell and fork()+exec()ing a
git process.

However, the check for the work tree preceeds the check of the
environment variables enabling status indicators, so it's evaluated
even when all these indicators are disabled.

Check upfront whether any of these indicators are enabled, i.e. a
corresponding environment variable is set, to spare the unnecessary
fork()+exec() overhead when all of them are disabled.

(Ideally we could check whether we are in a work tree using only bash
builtins, like we did in the previous commit for .git directory, but
the path of the work tree can be specified by the 'core.worktree'
config variable, and running 'git config' to get its value would be
just as expensive.)

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: use bash builtins to find out current branch during rebase

During an ongoing interactive rebase __git_ps1() finds out the name of
the rebased branch by executing the '$(cat .git/rebase-merge/head-name)'
command substitution.  That is not quite the most efficient way to
read a single line single word file, because it imposes the overhead
of fork()ing a subshell and fork()+exec()ing 'cat'.

Use the 'read' bash builtin instead to avoid that overhead.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: use bash builtins to get detached HEAD abbrev. object name

When describing a detached HEAD according to the $GIT_PS1_DESCRIBE
environment variable fails, __git_ps1() runs the '$(cut -c1-7
.git/HEAD)' command substitution to put the 7 hexdigits abbreviated
commit object name in the prompt.  This imposes the overhead of
fork()ing a subshell and fork(+exec()ing 'cut'.

Thanks to an earlier commit in this series the contents of HEAD is
already read into a local variable, so we can get the 7 hexdigits
using only parameter expansions, sparing the fork()+exec() overhead.

Since zsh doesn't implement substring expansion we can't just use
${head:0:7}, hence the "remove everything except the first 7 chars"
parameter expansion combination.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: display stash and upstream state even inside the repository

Besides the current branch name or detached head info __git_ps1() can
also display some status indicators in the prompt.  The first such
indicator was for changes in the work tree in 738a94a9 (bash: offer to
show (un)staged changes, 2009-02-03), and was only checked/displayed
when inside the work tree.  Later other indicators were added in
2414b45c (Show presence of stashed changes in bash prompt.,
2009-06-02), 397f7c63 (Show the presence of untracked files in the
bash prompt., 2009-07-22), and 6d158cba (bash completion: Support
"divergence from upstream" messages in __git_ps1, 2010-06-17).  All of
these just followed suit and were checked only when inside the work
tree, i.e. after checking the results of the '$(git rev-parse
--is-inside-work-tree)' command substitution, imposing the overhead of
fork()ing a subshell and fork()+exec()ing a git process.

However, the presence of stashes and the divergence from upstream is
not a property of the work tree but a property of the repository, and
the implementation of their indicators doesn't actually require a work
tree.  Therefore, we can display these two indicators even inside the
repository.  Not that it's very useful to see the stash status while
poking around deep inside the .git directory, but this way users
enabling only the stash indicator won't pay the additional performance
penalty of the check for the work tree.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: use bash builtins to check stash state

When the environment variable $GIT_PS1_SHOWSTASHSTATE is set
__git_ps1() checks the presence of stashes by running 'git rev-parse
--verify refs/stash'.  This command not only checks that the
'refs/stash' ref exists but also, well, verifies that it's a valid
ref.

However, we don't need to be that thorough for the bash prompt.  We
can omit that verification and only check whether 'refs/stash' exists
or not.  Since 'git pack-refs' never packs 'refs/stash', it's a matter
of checking the existence of a ref file.  Perform this check using
only bash builtins to spare the overhead of fork()+exec()ing a git
process.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: avoid command substitution when checking for untracked files

When enabled, the bash prompt can indicate the presence of untracked
files with a '%' sign.  __git_ps1() checks for untracked files by running the
'$(git ls-files --others --exclude-standard)' command substitution,
and displays the indicator when there is no output.

Avoid this command substitution by additionally passing
'--error-unmatch *', and checking the command's return value.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>
bash prompt: alternative git prompt without command substitution

__git_ps1() prints the branch name, status indicators, etc. to stdout,
therefore it has to be included in $PS1 through a command substitution
to display that information in the prompt.  The configuration is
straightforward, but it imposes the overhead of fork()ing a subshell
for the command substitution.

However, bash has the $PROMPT_COMMAND shell variable, which "if set,
the value is executed as a command prior to issuing each primary
prompt" (quoted from bash man page).  Its value isn't executed in a
subshell but in the context of the "main" shell, hence (non-local)
variables set in invoked shell functions are available when expanding
$PS1.  We can use this facility to avoid that command substitution for
__git_ps1().

So split out the meat of __git_ps1() into the new
__git_prompt_command() function, which stores the branch name & co.
in the $__git_ps1_string variable.  This function, as its name
suggests, should be included in $PROMPT_COMMAND, and $__git_ps1_string
should in turn be included in $PS1 with a bit of a twist to put the
parentheses around it:

   PROMPT_COMMAND=__git_prompt_command
   PS1='[\u@\h \W${__git_ps1_string:+ ($__git_ps1_string)}]\$ '

Turn __git_ps1() into a wrapper around __git_prompt_command() such
that it's functionality remains unaltered, so already configured
prompts won't break.

The whole series speeds up the bash prompt on Windows/MinGW
immensely, in many cases brings it down to around 10ms on my
machine while in powersave mode.  Here are some timing results in
three common scenarios (repeated 10 times, because the after cases
were too fast to measure a single execution accurately with 'time'):

In my home directory, i.e. not in a git repository, before:

    /c/Users/szeder
    $ time for i in {0..9} ; do prompt=$(__git_ps1) ; done

    real    0m0.952s
    user    0m0.214s
    sys     0m0.444s

  After:

    /c/Users/szeder
    $ time for i in {0..9} ; do __git_prompt_command ;
           prompt=${__git_ps1_string:+ ($__git_ps1_string)} ; done

    real    0m0.718s
    user    0m0.136s
    sys     0m0.354s

  After, with discovery across filesystems enabled:

    /c/Users/szeder
    $ time for i in {0..9} ; do __git_prompt_command ;
           prompt=${__git_ps1_string:+ ($__git_ps1_string)} ; done

    real    0m0.078s
    user    0m0.016s
    sys     0m0.062s

At the top of a work tree, before:

    /c/Users/szeder/repo (master)
    $ time for i in {0..9} ; do prompt=$(__git_ps1) ; done

    real    0m2.901s
    user    0m0.391s
    sys     0m1.468s

  After:

    /c/Users/szeder/repo (master)
    $ time for i in {0..9} ; do __git_prompt_command ;
           prompt=${__git_ps1_string:+ ($__git_ps1_string)} ; done

    real    0m0.094s
    user    0m0.047s
    sys     0m0.047s

In a subdirectory, stash indicator enabled, before:

    /c/Users/szeder/repo/subdir (master $)
    $ time for i in {0..9} ; do prompt=$(__git_ps1) ; done

    real    0m4.118s
    user    0m0.468s
    sys     0m2.056s

  After:

    /c/Users/szeder/repo/subdir (master $)
    $ time for i in {0..9} ; do __git_prompt_command ;
           prompt=${__git_ps1_string:+ ($__git_ps1_string)} ; done

    real    0m0.858s
    user    0m0.152s
    sys     0m0.322s

  After, discovery across filesystems enabled:

    /c/Users/szeder/repo/subdir (master $)
    $ time for i in {0..9} ; do __git_prompt_command ;
           prompt=${__git_ps1_string:+ ($__git_ps1_string)} ; done

    real    0m0.109s
    user    0m0.047s
    sys     0m0.063s

Well, that's about 97% improvement.

The performance gain on Linux is smaller, the latter case goes down
from 0.264s to 0.047, but since it was fast enough to begin with I
won't lengthen this commit message with further timing results on
Linux.

Signed-off-by: SZEDER Gábor <szeder@ira.uka.de>