Description
This is a copy-edited version of my advice to somebody who asked for it on Gitter (only to refuse to do any work).
There is a reason for bin\git.exe
and cmd\git.exe
to be identical: they are both copies of the same wrapper (or proxy, or primer). The idea here is to provide a consistent location for external tooling. However, they still need to spawn the real git.exe
.
Note that there are two "real" git.exe
: mingw64\bin\git.exe
and mingw64\libexec\git-core\git.exe
. Those two git.exe
files are hash-identical because the latter is a hard link to the former. There are many more hard links in mingw64\libexec\git-core
, and the reason is : these are hard-linked versions of git.exe
, provided for backwards-compatibility (for scripts that still call something like git-rev-list
despite over a decade of deprecation: they should call git rev-list
instead, i.e. with a space after git
instead of a dash).
Currently, the only supported way is to call cmd\git.exe
, not mingw64\bin\git.exe
directly. Calling cmd\git.exe
will be guaranteed to work for any future Git for Windows version.
Now, there is a really interesting question: why can't cmd\git.exe
be a hard-link to mingw64\bin\git.exe
in the first place?
Let's have a look at what cmd\git.exe
does actually: https://github.com/git-for-windows/MINGW-packages/blob/master/mingw-w64-git/git-wrapper.c. You will see that this wrapper is used for more things than just for cmd\git.exe
. It is used, for example, as cmd\gitk.exe
, too, and also for git-bash.exe
. The real interesting part for cmd\git.exe
(or for that matter, bin\git.exe
) is here: https://github.com/git-for-windows/MINGW-packages/blob/c29792d7d6419489d66e443559e1881737e9c55e/mingw-w64-git/git-wrapper.c#L695-L706:
We first detect the top-level directory (from the .exe
's location, as queried via GetModuleFileName()
). Then, we set exe
to <top-level>\mingw64\bin\git.exe
and test whether that file exists. If not, we set exe
to <top-level>\bin\git.exe
.
So now comes the real important part of the Git wrapper: https://github.com/git-for-windows/MINGW-packages/blob/c29792d7d6419489d66e443559e1881737e9c55e/mingw-w64-git/git-wrapper.c#L708-L714: In setup_environment()
, we set the environment variables MSYSTEM
, PLINK_PROTOCOL
, HOME
(unless it is set already), and we augment PATH
. After that, we merely replace the first component of the command-line by the path of the actual git.exe
, spawn that and wait until it is done.
Back to the interesting question why don't we just teach the real git.exe
do do those tricks and to replace cmd\git.exe
(and bin\git.exe
) by a hard-link to mingw64\bin\git.exe
?
This is not only an interesting question, but also a good idea, but we do have to teach git.exe
those tricks (i.e. setting the environment variables) before we can do that. We also have to be a bit careful about this, as we do not really want git.exe
to extend the PATH
over and over and over again, possibly running into the length limitation (PATH
can only contain 32k characters) for nested git invocations.
The good news is that HOME
is already taken care of: https://github.com/git-for-windows/git/blob/v2.22.0.windows.1/compat/mingw.c#L2950-L2972
(Why don't we remove this from the Git wrapper, then? Because it is also needed for git-bash.exe
.)
The PATH
extension is not there, but a similar thing is there: it adds mingw64\libexec\git-core
to the PATH
. To understand why, you have to learn a bit about Git's history. The original idea of Linus Torvalds was to ship Git as a set of Unix-y tools that do one thing, and one thing only, and as a set of Unix shell scripts that combine these functionalities. That seemed to be a really smart idea right up until we ran into portability, scale and performance issues, not to mention the difficulties with proper error handling. But by that time, we already had shipped several Git versions with e.g. git-rev-list
and git-log
in the bin
directory. Also, this cluttered the tab completion with low-level commands. So the low-level commands were moved into libexec/git-core
and the top-level git was kept in bin
. This required the PATH
to be extended to find the low-level commands, and subsequently a lot of them were turned into "built-ins", i.e. handled by the top-level git executable directly. However, for backwards-compatibility, we still needed the "dashed" forms (i.e. an executable called git-rev-list
that does the same as git
with the first argument rev-list
).
This PATH
extension is performed in setup_path()
.
But setup_path()
appends the libexec
path to PATH
, while we need to insert something in the beginning of it instead. So here is how I would go about it if I wanted to bring about this change:
- I would use the presence of the environment variable
MSYSTEM
as a tell-tale that all of this has been done already, i.e. guard at least thePATH
munging. - I would insert all the new code into
compat\mingw.c
, just afterHOME
has been handled.
The new code would look somewhat like this:
wchar_t buf[32768];
[...]
if (!GetEnvironmentVariableW(L"PLINK_PROTOCOL", buf, ARRAY_SIZE(buf)))
SetEnvironmentVariableW(L"PLINK_PROTOCOL", L"ssh");
if (!GetEnvironmentVariableW(L"MSYSTEM", buf, ARRAY_SIZE(buf))) {
int bitness = (int)(sizeof(void *) * 8);
#ifdef RUNTIME_PREFIX
wchar_t home[MAX_PATH], top_level[MAX_PATH], path[32768];
size_t off = 0;
if ([... get HOME variable ...])
off += swprintf(buf + off, ARRAY_SIZE(buf) - off - 1, L"%s\\bin;", home);
if ([... GetModuleFileNameW() ...]) {
/* strip trailing `git.exe`, then `cmd` or `mingw64\bin` or `mingw32\bin` or `bin` or `libexec\git-core` */
off += swprintf(buf + off, ARRAY_SIZE(buf) - off - 1, L"%s\\mingw%d\\bin;%s\\usr\\bin;",
top_level, bitness, top_level);
}
if ([... get PATH variable ...])
wcsncat(buf + off, ARRAY_SIZE(buf) - off, path);
else if (off > 0)
buf[off - 1] = L'\0'; /* strip trailing ';' */
SetEnvironmentVariableW(L"PATH", buf);
#endif
swprintf(buf, ARRAY_SIZE(buf), L"MINGW%d", bitness);
SetEnvironmentVariableW(L"MSYSTEM", buf);
}