为什么 TortoiseGit Git 命令进度对话框在 post-checkout 挂钩启动记事本后挂起?

Why does TortoiseGit Git Command Progress dialog hang after post-checkout hook starts Notepad?

在我的日常工作中,我使用 TortoiseGit 并且我正在尝试编写一个 post-checkout hook。我更喜欢在 Windows-environment 中工作,所以钩子文件唯一做的就是调用标准 windows .bat-file:

#!/bin/sh
echo "The Git post-checkout Linux Shell-file has now started to execute"
cmd.exe "/c post-checkout.bat"
echo "The Git post-checkout Linux Shell-file has now finished executing"

在我的标准 Windows.bat 文件中,我执行以下操作:

@echo off
echo --------------------------------------------------------------------------
echo post-checkout.bat in repository root has now started to execute

START /b  notepad.exe

echo post-checkout.bat in repository root has now finished executing
echo --------------------------------------------------------------------------
cmd /c exit 0

当我在 TortoiseGit 中选择 Switch/Checkout 时,我的钩子文件被成功执行并且记事本启动。然而,奇怪的是 TortoiseGit Git 命令进度对话框一直挂起,直到我关闭记事本。请注意,我可以在 TortoiseGit Git 中看到“The Git post-checkout Linux Shell-file has now finished execution”关闭记事本之前的命令进度对话框。如果我使用 C:\Program Files\Git\bin\bash.exe 命令 window 结帐,则不会出现挂起问题。有人知道如何解决这个问题吗?

编辑:将以下内容直接放入 Linux 挂钩文件(即完全忘记 Window bat 文件)产生完全相同的结果,TortoiseGit Git 命令进度对话框挂起,直到我关闭记事本:

#!/bin/sh
echo "The Git post-checkout Linux Shell-file has now started to execute"
notepad &
echo "The Git post-checkout Linux Shell-file has now finished executing"

在源代码中https://gitlab.com/tortoisegit/tortoisegit/-/blob/master/src/Utils/Hooks.cpp it can be seen that a hook-file is executed in a new process that is created using CreateProcess and then it waits for the process and all its children to finish by calling WaitForSingleObject (see e.g. this explanation How to start a process and make it 'independent'). So the behavior is very much expected. One solution can be found here which refers to this webpage https://svn.haxx.se/users/archive-2008-11/0301.shtml。我稍微重写了一下:

#include "stdafx.h"
#include <windows.h>

int getFirstIndexOfChar(WCHAR* stringToInvestigate, int startIndex, WCHAR charToLookFor);
int getLastIndexOfChar(WCHAR* stringToInvestigate, int startIndex, WCHAR charToLookFor);

int _tmain(int argc, _TCHAR* argv[])
{
    WCHAR* pOriginalCmd = ::GetCommandLine();
    // Test code (modify paths to RunDetached.exe and MyFile.txt appropriately)
//    pOriginalCmd = _T("\"D:\My Visual Studio Projects\RunDetached\debug\RunDetached.exe\" \"C:\Windows\System32\notepad.exe\" \"D:\1.txt\"");

    int CmdLen = (int)wcslen(pOriginalCmd);

    // Determine where certain characters are located (excl means the particular index is not included, e.g. 
    // if indexExcl is 5 then index 4 is the last included index).
    int beginningOf1stArg   = getFirstIndexOfChar(pOriginalCmd, 0,                     L'\"');
    int endOf1stArgExcl     = getFirstIndexOfChar(pOriginalCmd, beginningOf1stArg + 1, L'\"') + 1;
    int beginningOf2ndArg   = getFirstIndexOfChar(pOriginalCmd, endOf1stArgExcl   + 1, L'\"');
    int endOf2ndArgExcl     = getFirstIndexOfChar(pOriginalCmd, beginningOf2ndArg + 1, L'\"') + 1;
    int beginningOf3rdArg   = getFirstIndexOfChar(pOriginalCmd, endOf2ndArgExcl   + 1, L'\"');
    int endOfLastArgExcl    = getLastIndexOfChar (pOriginalCmd, CmdLen            - 1, L'\"') + 1;
    int beginningOfFileName = getLastIndexOfChar (pOriginalCmd, endOf2ndArgExcl   - 2, L'\') + 1;
    int endOfFileNameExcl   = endOf2ndArgExcl - 1;
    if ((beginningOf1stArg < 0) || (endOf1stArgExcl     < 0) || (beginningOf2ndArg < 0) || (endOf2ndArgExcl < 0) ||
        (endOfLastArgExcl  < 0) || (beginningOfFileName < 0) || (endOfFileNameExcl < 0))
    {
        return -1;
    }

    // Determine the application to execute including full path. E.g. for notepad this should be:
    // C:\Windows\System32\notepad.exe (without any double-quotes)
    int lengthOfApplicationNameAndPathInChars = (endOf2ndArgExcl -1) - (beginningOf2ndArg + 1);  // Skip double-quotes
    WCHAR* lpApplicationNameAndPath = (WCHAR*)malloc(sizeof(WCHAR) * (lengthOfApplicationNameAndPathInChars + 1));
    memcpy(lpApplicationNameAndPath, &pOriginalCmd[beginningOf2ndArg + 1], sizeof(WCHAR) * (lengthOfApplicationNameAndPathInChars));
    lpApplicationNameAndPath[lengthOfApplicationNameAndPathInChars] = (WCHAR)0;  // Null terminate

    // Determine the command argument. Must be in modifyable memory and should start with the
    // application name without the path. E.g. for notepad with command argument D:\MyFile.txt:
    // "notepad.exe" "D:\MyFile.txt" (with the double-quotes).
    WCHAR* modifiedCmd = NULL;
    if (0 < beginningOf3rdArg)
    {
        int lengthOfApplicationNameInChars = endOfFileNameExcl - beginningOfFileName;  // Application name without path
        int lengthOfRestOfCmdInChars = CmdLen - beginningOf3rdArg;
        int neededCmdLengthInChars = 1 + lengthOfApplicationNameInChars + 2 + lengthOfRestOfCmdInChars; // Two double-quotes and one space extra

        modifiedCmd = (WCHAR*)malloc(sizeof(WCHAR) * (neededCmdLengthInChars + 1));  // Extra char is null-terminator
        modifiedCmd[0] = L'\"';                                                             // Start with double-quoute
        memcpy(&modifiedCmd[1], &pOriginalCmd[beginningOfFileName], sizeof(WCHAR) * (lengthOfApplicationNameInChars));
        modifiedCmd[1 + (lengthOfApplicationNameInChars)] = L'\"';
        modifiedCmd[1 + (lengthOfApplicationNameInChars) + 1] = L' ';
        memcpy(&modifiedCmd[1 + (lengthOfApplicationNameInChars) + 2], &pOriginalCmd[beginningOf3rdArg], sizeof(WCHAR) * lengthOfRestOfCmdInChars);
        modifiedCmd[neededCmdLengthInChars] = (WCHAR)0;
    }

    STARTUPINFO si;
    ZeroMemory( &si, sizeof(si) );
    si.cb = sizeof(si);
    PROCESS_INFORMATION pi;
    ZeroMemory( &pi, sizeof(pi) );

    BOOL result = CreateProcess    // Start the detached process.
    (
        lpApplicationNameAndPath, // Module name and full path
        modifiedCmd,              // Command line
        NULL,                     // Process handle not inheritable
        NULL,                     // Thread handle not inheritable
        FALSE,                    // Set bInheritHandles to FALSE
        DETACHED_PROCESS,         // Detach process
        NULL,                     // Use parent's environment block
        NULL,                     // Use parent's starting directory
        &si,                      // Pointer to STARTUPINFO structure
        &pi                       // Pointer to PROCESS_INFORMATION structure (returned)
    );
    free(lpApplicationNameAndPath);
    if (modifiedCmd != NULL)
    {
        free(modifiedCmd);
    }
    if (result) return 0;
    wchar_t msg[2048];
    FormatMessage
    (
        FORMAT_MESSAGE_FROM_SYSTEM,
        NULL,
        ::GetLastError(),
        MAKELANGID(LANG_NEUTRAL, SUBLANG_SYS_DEFAULT),
        msg, sizeof(msg),
        NULL
    );
    fputws(msg, stderr);
    _flushall();
    return -1;
}

int getFirstIndexOfChar(WCHAR* stringToInvestigate, int startIndex, WCHAR charToLookFor)
{
    int stringLen = (int)wcslen(stringToInvestigate);
    if (5000 < stringLen)   // Sanity check
    {
        return -1;
    }
    for (int i = startIndex; i < stringLen; i++)
    {
        if (stringToInvestigate[i] == charToLookFor)
        {
            return i;
        }
    }
    return -1;
}

int getLastIndexOfChar(WCHAR* stringToInvestigate, int startIndex, WCHAR charToLookFor)
{
    int stringLen = (int)wcslen(stringToInvestigate);
    if (5000 < stringLen)   // Sanity check
    {
        return -1;
    }
    for (int i = min(stringLen - 1, startIndex); 0 <= i; i--)
    {
        if (stringToInvestigate[i] == charToLookFor)
        {
            return i;
        }
    }
    return -1;
}

编译完以上内容后,我修改了 .bat 文件,现在它运行良好:

@echo off
echo --------------------------------------------------------------------------
echo post-checkout.bat in repository root has now started to execute

RunDetached  notepad

echo post-checkout.bat in repository root has now finished executing
echo --------------------------------------------------------------------------
exit 0