1
// TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2011-2016, 2018-2019 - TortoiseGit
4 // Copyright (C) 2007-2008 - TortoiseSVN
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 #include "StringUtils.h"
25 #include "SmartHandle.h"
28 #include <afxtaskdialog.h>
31 CHooks
* CHooks::m_pInstance
;
32 CTGitPath
CHooks::m_RootPath
;
34 static CString
CalcSHA256(const CString
& text
)
37 if (!CryptAcquireContext(&hProv
, nullptr, nullptr, PROV_RSA_AES
, CRYPT_VERIFYCONTEXT
))
39 SCOPE_EXIT
{ CryptReleaseContext(hProv
, 0); };
42 if (!CryptCreateHash(hProv
, CALG_SHA_256
, 0, 0, &hHash
))
44 SCOPE_EXIT
{ CryptDestroyHash(hHash
); };
46 CStringA textA
= CUnicodeUtils::GetUTF8(text
);
47 if (!CryptHashData(hHash
, (LPBYTE
)(LPCSTR
)textA
, textA
.GetLength(), 0))
52 DWORD cbHash
= _countof(rgbHash
);
53 if (!CryptGetHashParam(hHash
, HP_HASHVAL
, rgbHash
, &cbHash
, 0))
56 for (DWORD i
= 0; i
< cbHash
; i
++)
58 BYTE hi
= rgbHash
[i
] >> 4;
59 BYTE lo
= rgbHash
[i
] & 0xf;
60 hash
.AppendChar(hi
+ (hi
> 9 ? 87 : 48));
61 hash
.AppendChar(lo
+ (lo
> 9 ? 87 : 48));
78 m_pInstance
= new CHooks();
79 CRegString
reghooks(L
"Software\\TortoiseGit\\hooks");
80 CString strhooks
= reghooks
;
81 ParseHookString(strhooks
, false);
85 CHooks
& CHooks::Instance()
90 void CHooks::Destroy()
98 for (hookiterator it
= begin(); it
!= end(); ++it
)
100 if (it
->second
.bLocal
)
102 strhooks
+= GetHookTypeString(it
->first
.htype
);
104 if (!it
->second
.bEnabled
)
106 strhooks
+= it
->first
.path
.GetWinPathString();
108 strhooks
+= it
->second
.commandline
;
110 strhooks
+= (it
->second
.bWait
? L
"true" : L
"false");
112 strhooks
+= (it
->second
.bShow
? L
"show" : L
"hide");
115 CRegString
reghooks(L
"Software\\TortoiseGit\\hooks");
117 if (reghooks
.GetLastError())
120 if (g_Git
.m_CurrentDir
.IsEmpty() || GitAdminDir::IsBareRepo(g_Git
.m_CurrentDir
))
123 // load the .tgitconfig file
124 CAutoConfig
gitconfig(true);
125 git_config_add_file_ondisk(gitconfig
, CGit::GetGitPathStringA(g_Git
.CombinePath(L
".tgitconfig")), GIT_CONFIG_LEVEL_LOCAL
, nullptr, FALSE
); // this needs to have the second highest priority
127 // delete all existing local hooks
128 for (const auto& hook
: { PROJECTPROPNAME_STARTCOMMITHOOK
, PROJECTPROPNAME_PRECOMMITHOOK
, PROJECTPROPNAME_POSTCOMMITHOOK
, PROJECTPROPNAME_PREPUSHHOOK
, PROJECTPROPNAME_POSTPUSHHOOK
, PROJECTPROPNAME_PREREBASEHOOK
})
130 CStringA hookA
= CUnicodeUtils::GetUTF8(hook
);
131 for (const auto& val
: { "cmdline", "wait", "show" })
133 git_config_delete_entry(gitconfig
, hookA
+ val
);
137 // now save the local hooks to .tgitconfig
138 for (const auto& hook
: CHooks::Instance())
140 if (!hook
.second
.bLocal
)
143 CStringA sHookPropName
;
144 switch (hook
.first
.htype
)
146 case start_commit_hook
:
147 sHookPropName
= CUnicodeUtils::GetUTF8(PROJECTPROPNAME_STARTCOMMITHOOK
);
149 case pre_commit_hook
:
150 sHookPropName
= CUnicodeUtils::GetUTF8(PROJECTPROPNAME_PRECOMMITHOOK
);
152 case post_commit_hook
:
153 sHookPropName
= CUnicodeUtils::GetUTF8(PROJECTPROPNAME_POSTCOMMITHOOK
);
156 sHookPropName
= CUnicodeUtils::GetUTF8(PROJECTPROPNAME_PREPUSHHOOK
);
159 sHookPropName
= CUnicodeUtils::GetUTF8(PROJECTPROPNAME_POSTPUSHHOOK
);
161 case pre_rebase_hook
:
162 sHookPropName
= CUnicodeUtils::GetUTF8(PROJECTPROPNAME_PREREBASEHOOK
);
165 git_config_set_string(gitconfig
, sHookPropName
+ "cmdline", CUnicodeUtils::GetUTF8(hook
.second
.commandline
));
166 git_config_set_bool(gitconfig
, sHookPropName
+ "wait", hook
.second
.bWait
);
167 git_config_set_bool(gitconfig
, sHookPropName
+ "show", hook
.second
.bShow
);
173 bool CHooks::Remove(const hookkey
&key
)
175 return (erase(key
) > 0);
178 void CHooks::Add(hooktype ht
, const CTGitPath
& Path
, LPCTSTR szCmd
, bool bWait
, bool bShow
, bool bEnabled
, bool bLocal
)
183 hookiterator it
= find(key
);
188 cmd
.commandline
= szCmd
;
191 cmd
.bEnabled
= bEnabled
;
193 insert(std::pair
<hookkey
, hookcmd
>(key
, cmd
));
196 bool CHooks::SetEnabled(const hookkey
& k
, bool bEnabled
)
201 if (it
->second
.bEnabled
== bEnabled
)
203 it
->second
.bEnabled
= bEnabled
;
207 CString
CHooks::GetHookTypeString(hooktype t
)
211 case start_commit_hook
:
212 return L
"start_commit_hook";
213 case pre_commit_hook
:
214 return L
"pre_commit_hook";
215 case post_commit_hook
:
216 return L
"post_commit_hook";
218 return L
"pre_push_hook";
220 return L
"post_push_hook";
221 case pre_rebase_hook
:
222 return L
"pre_rebase_hook";
227 hooktype
CHooks::GetHookType(const CString
& s
)
229 if (s
.Compare(L
"start_commit_hook") == 0)
230 return start_commit_hook
;
231 if (s
.Compare(L
"pre_commit_hook") == 0)
232 return pre_commit_hook
;
233 if (s
.Compare(L
"post_commit_hook") == 0)
234 return post_commit_hook
;
235 if (s
.Compare(L
"pre_push_hook") == 0)
236 return pre_push_hook
;
237 if (s
.Compare(L
"post_push_hook") == 0)
238 return post_push_hook
;
239 if (s
.Compare(L
"pre_rebase_hook") == 0)
240 return pre_rebase_hook
;
245 void CHooks::SetProjectProperties(const CTGitPath
& Path
, const ProjectProperties
& pp
)
250 if (!pp
.sPreCommitHook
.IsEmpty())
251 sHookString
+= GetHookTypeString(pre_commit_hook
) + L
"\n?\n" + pp
.sPreCommitHook
+ L
"\n";
252 if (!pp
.sStartCommitHook
.IsEmpty())
253 sHookString
+= GetHookTypeString(start_commit_hook
) + L
"\n?\n" + pp
.sStartCommitHook
+ L
"\n";
254 if (!pp
.sPostCommitHook
.IsEmpty())
255 sHookString
+= GetHookTypeString(post_commit_hook
) + L
"\n?\n" + pp
.sPostCommitHook
+ L
"\n";
256 if (!pp
.sPrePushHook
.IsEmpty())
257 sHookString
+= GetHookTypeString(pre_push_hook
) + L
"\n?\n" + pp
.sPrePushHook
+ L
"\n";
258 if (!pp
.sPostPushHook
.IsEmpty())
259 sHookString
+= GetHookTypeString(post_push_hook
) + L
"\n?\n" + pp
.sPostPushHook
+ L
"\n";
260 if (!pp
.sPreRebaseHook
.IsEmpty())
261 sHookString
+= GetHookTypeString(pre_rebase_hook
) + L
"\n?\n" + pp
.sPreRebaseHook
+ L
"\n";
262 ParseHookString(sHookString
, true);
265 void CHooks::AddParam(CString
& sCmd
, const CString
& param
)
272 void CHooks::AddPathParam(CString
& sCmd
, const CTGitPathList
& pathList
)
274 CTGitPath temppath
= CTempFiles::Instance().GetTempFilePath(true);
275 pathList
.WriteToFile(temppath
.GetWinPathString(), true);
276 AddParam(sCmd
, temppath
.GetWinPathString());
279 void CHooks::AddCWDParam(CString
& sCmd
, const CString
& workingTree
)
281 AddParam(sCmd
, workingTree
);
284 void CHooks::AddErrorParam(CString
& sCmd
, const CString
& error
)
287 tempPath
= CTempFiles::Instance().GetTempFilePath(true);
288 CStringUtils::WriteStringToTextFile(tempPath
.GetWinPath(), (LPCTSTR
)error
);
289 AddParam(sCmd
, tempPath
.GetWinPathString());
292 CTGitPath
CHooks::AddMessageFileParam(CString
& sCmd
, const CString
& message
)
295 tempPath
= CTempFiles::Instance().GetTempFilePath(true);
296 CStringUtils::WriteStringToTextFile(tempPath
.GetWinPath(), (LPCTSTR
)message
);
297 AddParam(sCmd
, tempPath
.GetWinPathString());
301 bool CHooks::StartCommit(HWND hWnd
, const CString
& workingTree
, const CTGitPathList
& pathList
, CString
& message
, DWORD
& exitcode
, CString
& error
)
303 auto it
= FindItem(start_commit_hook
, workingTree
);
306 if (!ApproveHook(hWnd
, it
, exitcode
))
308 error
.LoadString(IDS_USERCANCELLED
);
311 CString sCmd
= it
->second
.commandline
;
312 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
313 AddPathParam(sCmd
, pathList
);
314 CTGitPath temppath
= AddMessageFileParam(sCmd
, message
);
315 AddCWDParam(sCmd
, workingTree
);
316 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
317 if (!exitcode
&& !temppath
.IsEmpty())
319 CStringUtils::ReadStringFromTextFile(temppath
.GetWinPathString(), message
);
324 bool CHooks::PreCommit(HWND hWnd
, const CString
& workingTree
, const CTGitPathList
& pathList
, CString
& message
, DWORD
& exitcode
, CString
& error
)
326 auto it
= FindItem(pre_commit_hook
, workingTree
);
329 if (!ApproveHook(hWnd
, it
, exitcode
))
331 error
.LoadString(IDS_USERCANCELLED
);
334 CString sCmd
= it
->second
.commandline
;
335 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
336 AddPathParam(sCmd
, pathList
);
337 CTGitPath temppath
= AddMessageFileParam(sCmd
, message
);
338 AddCWDParam(sCmd
, workingTree
);
339 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
340 if (!exitcode
&& !temppath
.IsEmpty())
341 CStringUtils::ReadStringFromTextFile(temppath
.GetWinPathString(), message
);
345 bool CHooks::PostCommit(HWND hWnd
, const CString
& workingTree
, bool amend
, DWORD
& exitcode
, CString
& error
)
347 auto it
= FindItem(post_commit_hook
, workingTree
);
350 if (!ApproveHook(hWnd
, it
, exitcode
))
352 error
.LoadString(IDS_USERCANCELLED
);
355 CString sCmd
= it
->second
.commandline
;
356 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
357 AddCWDParam(sCmd
, workingTree
);
359 AddParam(sCmd
, L
"true");
361 AddParam(sCmd
, L
"false");
362 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
366 bool CHooks::PrePush(HWND hWnd
, const CString
& workingTree
, DWORD
& exitcode
, CString
& error
)
368 auto it
= FindItem(pre_push_hook
, workingTree
);
371 if (!ApproveHook(hWnd
, it
, exitcode
))
373 error
.LoadString(IDS_USERCANCELLED
);
376 CString sCmd
= it
->second
.commandline
;
377 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
378 AddErrorParam(sCmd
, error
);
379 AddCWDParam(sCmd
, workingTree
);
380 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
384 bool CHooks::PostPush(HWND hWnd
, const CString
& workingTree
, DWORD
& exitcode
, CString
& error
)
386 auto it
= FindItem(post_push_hook
, workingTree
);
389 if (!ApproveHook(hWnd
, it
, exitcode
))
391 error
.LoadString(IDS_USERCANCELLED
);
394 CString sCmd
= it
->second
.commandline
;
395 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
396 AddErrorParam(sCmd
, error
);
397 AddCWDParam(sCmd
, workingTree
);
398 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
402 bool CHooks::PreRebase(HWND hWnd
, const CString
& workingTree
, const CString
& upstream
, const CString
& rebasedBranch
, DWORD
& exitcode
, CString
& error
)
404 auto it
= FindItem(pre_rebase_hook
, workingTree
);
407 if (!ApproveHook(hWnd
, it
, exitcode
))
409 error
.LoadString(IDS_USERCANCELLED
);
412 CString sCmd
= it
->second
.commandline
;
413 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
414 AddParam(sCmd
, upstream
);
415 AddParam(sCmd
, rebasedBranch
);
416 AddErrorParam(sCmd
, error
);
417 AddCWDParam(sCmd
, workingTree
);
418 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
422 bool CHooks::IsHookPresent(hooktype t
, const CString
& workingTree
)
424 auto it
= FindItem(t
, workingTree
);
428 hookiterator
CHooks::FindItem(hooktype t
, const CString
& workingTree
)
431 CTGitPath path
= workingTree
;
437 if (it
!= end() && it
->second
.bEnabled
)
439 path
= path
.GetContainingDirectory();
440 } while(!path
.IsEmpty());
442 /* if this ever gets called with something different than the workingTree root,
443 * recheck whether it is necessary to add a check for "key.path = m_RootPath"
444 * (e.g., for sparse checkouts or other hook types).
446 ATLASSERT(CTGitPath(workingTree
).IsWCRoot());
448 // look for a script with a path as '*'
450 key
.path
= CTGitPath(L
"*");
452 if (it
!= end() && it
->second
.bEnabled
)
460 void CHooks::ParseHookString(CString strhooks
, bool bLocal
)
462 // now fill the map with all the hooks defined in the string
463 // the string consists of multiple lines, where one hook script is defined
465 // line 1: the hook type
466 // line 2: path to working copy where to apply the hook script,
467 // if it starts with "!" this hook is disabled (this should provide backward and forward compatibility)
468 // if it starts with "?" this hook is for the current project folder
469 // line 3: command line to execute
470 // line 4: 'true' or 'false' for waiting for the script to finish
471 // line 5: 'show' or 'hide' on how to start the hook script
476 cmd
.bApproved
= !bLocal
; // user configured scripts are pre-approved
478 while ((pos
= strhooks
.Find(L
'\n')) >= 0)
481 key
.htype
= GetHookType(strhooks
.Left(pos
));
482 if (pos
+ 1 < strhooks
.GetLength())
483 strhooks
= strhooks
.Mid(pos
+ 1);
486 bool bComplete
= false;
487 if ((pos
= strhooks
.Find(L
'\n')) >= 0)
491 if (strhooks
[0] == L
'!' && pos
> 1)
493 cmd
.bEnabled
= false;
494 strhooks
= strhooks
.Mid((int)wcslen(L
"!"));
497 if (strhooks
[0] == L
'?' && pos
> 0)
498 key
.path
= CTGitPath(m_RootPath
);
500 key
.path
= CTGitPath(strhooks
.Left(pos
));
501 if (pos
+ 1 < strhooks
.GetLength())
502 strhooks
= strhooks
.Mid(pos
+ 1);
505 if ((pos
= strhooks
.Find(L
'\n')) >= 0)
508 cmd
.commandline
= strhooks
.Left(pos
);
509 if (pos
+ 1 < strhooks
.GetLength())
510 strhooks
= strhooks
.Mid(pos
+ 1);
513 if ((pos
= strhooks
.Find(L
'\n')) >= 0)
516 cmd
.bWait
= (strhooks
.Left(pos
).CompareNoCase(L
"true") == 0);
517 if (pos
+ 1 < strhooks
.GetLength())
518 strhooks
= strhooks
.Mid(pos
+ 1);
521 if ((pos
= strhooks
.Find(L
'\n')) >= 0)
524 cmd
.bShow
= (strhooks
.Left(pos
).CompareNoCase(L
"show") == 0);
525 if (pos
+ 1 < strhooks
.GetLength())
526 strhooks
= strhooks
.Mid(pos
+ 1);
532 temp
.Format(L
"%s|%d%s", m_RootPath
.GetWinPath(), (int)key
.htype
, (LPCTSTR
)cmd
.commandline
);
534 cmd
.sRegKey
= L
"Software\\TortoiseGit\\approvedhooks\\" + CalcSHA256(temp
);
535 CRegDWORD
reg(cmd
.sRegKey
, 0);
536 cmd
.bApproved
= (DWORD(reg
) != 0);
537 cmd
.bStored
= reg
.exists();
546 m_pInstance
->insert(std::pair
<hookkey
, hookcmd
>(key
, cmd
));
550 DWORD
CHooks::RunScript(CString cmd
, LPCTSTR currentDir
, CString
& error
, bool bWait
, bool bShow
)
553 SECURITY_ATTRIBUTES sa
= { 0 };
554 sa
.nLength
= sizeof(sa
);
555 sa
.bInheritHandle
= TRUE
;
561 // clear the error string
564 // Create Temp File for redirection
565 TCHAR szTempPath
[MAX_PATH
] = {0};
566 TCHAR szOutput
[MAX_PATH
] = {0};
567 TCHAR szErr
[MAX_PATH
] = {0};
568 GetTortoiseGitTempPath(_countof(szTempPath
), szTempPath
);
569 GetTempFileName(szTempPath
, L
"git", 0, szErr
);
571 // setup redirection handles
572 // output handle must be WRITE mode, share READ
573 // redirect handle must be READ mode, share WRITE
574 hErr
= CreateFile(szErr
, GENERIC_WRITE
, FILE_SHARE_READ
, &sa
, CREATE_ALWAYS
, FILE_ATTRIBUTE_TEMPORARY
, nullptr);
578 error
= CFormatMessageWrapper();
582 hRedir
= CreateFile(szErr
, GENERIC_READ
, FILE_SHARE_WRITE
, nullptr, OPEN_EXISTING
, 0, nullptr);
586 error
= CFormatMessageWrapper();
590 GetTempFileName(szTempPath
, L
"git", 0, szOutput
);
591 hOut
= CreateFile(szOutput
, GENERIC_WRITE
, FILE_SHARE_READ
, &sa
, CREATE_ALWAYS
, FILE_ATTRIBUTE_TEMPORARY
, nullptr);
595 error
= CFormatMessageWrapper();
599 // setup startup info, set std out/err handles
601 STARTUPINFO si
= { 0 };
603 si
.dwFlags
= STARTF_USESTDHANDLES
| STARTF_USESHOWWINDOW
;
604 si
.hStdOutput
= hOut
;
606 si
.wShowWindow
= bShow
? SW_SHOW
: SW_HIDE
;
608 PROCESS_INFORMATION pi
= { 0 };
609 if (!CreateProcess(nullptr, cmd
.GetBuffer(), nullptr, nullptr, TRUE
, CREATE_UNICODE_ENVIRONMENT
, nullptr, currentDir
, &si
, &pi
))
611 const DWORD err
= GetLastError(); // preserve the CreateProcess error
612 error
= CFormatMessageWrapper(err
);
619 CloseHandle(pi
.hThread
);
621 // wait for process to finish, capture redirection and
622 // send it to the parent window/console
626 char buf
[256] = { 0 };
629 while (ReadFile(hRedir
, &buf
, sizeof(buf
) - 1, &dw
, nullptr))
633 error
+= CString(CStringA(buf
,dw
));
636 } while (WaitForSingleObject(pi
.hProcess
, 0) != WAIT_OBJECT_0
);
638 // perform any final flushing
639 while (ReadFile(hRedir
, &buf
, sizeof(buf
) - 1, &dw
, nullptr))
644 error
+= CString(CStringA(buf
, dw
));
646 WaitForSingleObject(pi
.hProcess
, INFINITE
);
647 GetExitCodeProcess(pi
.hProcess
, &exitcode
);
649 CloseHandle(pi
.hProcess
);
650 DeleteFile(szOutput
);
656 bool CHooks::ApproveHook(HWND hWnd
, hookiterator it
, DWORD
& exitcode
)
658 if (it
->second
.bApproved
|| it
->second
.bStored
)
661 return it
->second
.bApproved
;
665 sQuestion
.Format(IDS_HOOKS_APPROVE_TASK1
, (LPCWSTR
)it
->second
.commandline
);
666 CTaskDialog
taskdlg(sQuestion
, CString(MAKEINTRESOURCE(IDS_HOOKS_APPROVE_TASK2
)), L
"TortoiseGit", 0, TDF_USE_COMMAND_LINKS
| TDF_ALLOW_DIALOG_CANCELLATION
| TDF_POSITION_RELATIVE_TO_WINDOW
| TDF_SIZE_TO_CONTENT
);
667 taskdlg
.AddCommandControl(101, CString(MAKEINTRESOURCE(IDS_HOOKS_APPROVE_TASK3
)));
668 taskdlg
.AddCommandControl(102, CString(MAKEINTRESOURCE(IDS_HOOKS_APPROVE_TASK4
)));
669 taskdlg
.SetCommonButtons(TDCBF_CANCEL_BUTTON
);
670 taskdlg
.SetVerificationCheckboxText(CString(MAKEINTRESOURCE(IDS_HOOKS_APPROVE_TASK5
)));
671 taskdlg
.SetVerificationCheckbox(false);
672 taskdlg
.SetDefaultCommandControl(102);
673 taskdlg
.SetMainIcon(TD_WARNING_ICON
);
674 taskdlg
.SetFooterText(CString(MAKEINTRESOURCE(IDS_HOOKS_APPROVE_SECURITYHINT
)));
675 auto ret
= taskdlg
.DoModal(hWnd
);
681 bool bApproved
= (ret
== 101);
682 bool bDoNotAskAgain
= !!taskdlg
.GetVerificationCheckboxState();
686 CRegDWORD
reg(it
->second
.sRegKey
, 0);
687 reg
= bApproved
? 1 : 0;
688 it
->second
.bStored
= true;
689 it
->second
.bApproved
= bApproved
;