1
// TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2011-2016, 2018 - 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
))
309 error
.LoadString(IDS_ERR_HOOKNOTAPPROVED
);
312 CString sCmd
= it
->second
.commandline
;
313 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
314 AddPathParam(sCmd
, pathList
);
315 CTGitPath temppath
= AddMessageFileParam(sCmd
, message
);
316 AddCWDParam(sCmd
, workingTree
);
317 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
318 if (!exitcode
&& !temppath
.IsEmpty())
320 CStringUtils::ReadStringFromTextFile(temppath
.GetWinPathString(), message
);
325 bool CHooks::PreCommit(HWND hWnd
, const CString
& workingTree
, const CTGitPathList
& pathList
, CString
& message
, DWORD
& exitcode
, CString
& error
)
327 auto it
= FindItem(pre_commit_hook
, workingTree
);
330 if (!ApproveHook(hWnd
, it
))
333 error
.LoadString(IDS_ERR_HOOKNOTAPPROVED
);
336 CString sCmd
= it
->second
.commandline
;
337 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
338 AddPathParam(sCmd
, pathList
);
339 CTGitPath temppath
= AddMessageFileParam(sCmd
, message
);
340 AddCWDParam(sCmd
, workingTree
);
341 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
342 if (!exitcode
&& !temppath
.IsEmpty())
343 CStringUtils::ReadStringFromTextFile(temppath
.GetWinPathString(), message
);
347 bool CHooks::PostCommit(HWND hWnd
, const CString
& workingTree
, bool amend
, DWORD
& exitcode
, CString
& error
)
349 auto it
= FindItem(post_commit_hook
, workingTree
);
352 if (!ApproveHook(hWnd
, it
))
355 error
.LoadString(IDS_ERR_HOOKNOTAPPROVED
);
358 CString sCmd
= it
->second
.commandline
;
359 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
360 AddCWDParam(sCmd
, workingTree
);
362 AddParam(sCmd
, L
"true");
364 AddParam(sCmd
, L
"false");
365 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
369 bool CHooks::PrePush(HWND hWnd
, const CString
& workingTree
, DWORD
& exitcode
, CString
& error
)
371 auto it
= FindItem(pre_push_hook
, workingTree
);
374 if (!ApproveHook(hWnd
, it
))
377 error
.LoadString(IDS_ERR_HOOKNOTAPPROVED
);
380 CString sCmd
= it
->second
.commandline
;
381 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
382 AddErrorParam(sCmd
, error
);
383 AddCWDParam(sCmd
, workingTree
);
384 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
388 bool CHooks::PostPush(HWND hWnd
, const CString
& workingTree
, DWORD
& exitcode
, CString
& error
)
390 auto it
= FindItem(post_push_hook
, workingTree
);
393 if (!ApproveHook(hWnd
, it
))
396 error
.LoadString(IDS_ERR_HOOKNOTAPPROVED
);
399 CString sCmd
= it
->second
.commandline
;
400 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
401 AddErrorParam(sCmd
, error
);
402 AddCWDParam(sCmd
, workingTree
);
403 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
407 bool CHooks::PreRebase(HWND hWnd
, const CString
& workingTree
, const CString
& upstream
, const CString
& rebasedBranch
, DWORD
& exitcode
, CString
& error
)
409 auto it
= FindItem(pre_rebase_hook
, workingTree
);
412 if (!ApproveHook(hWnd
, it
))
415 error
.LoadString(IDS_ERR_HOOKNOTAPPROVED
);
418 CString sCmd
= it
->second
.commandline
;
419 sCmd
.Replace(L
"%root%", m_RootPath
.GetWinPathString());
420 AddParam(sCmd
, upstream
);
421 AddParam(sCmd
, rebasedBranch
);
422 AddErrorParam(sCmd
, error
);
423 AddCWDParam(sCmd
, workingTree
);
424 exitcode
= RunScript(sCmd
, workingTree
, error
, it
->second
.bWait
, it
->second
.bShow
);
428 bool CHooks::IsHookPresent(hooktype t
, const CString
& workingTree
)
430 auto it
= FindItem(t
, workingTree
);
434 hookiterator
CHooks::FindItem(hooktype t
, const CString
& workingTree
)
437 CTGitPath path
= workingTree
;
443 if (it
!= end() && it
->second
.bEnabled
)
445 path
= path
.GetContainingDirectory();
446 } while(!path
.IsEmpty());
447 // look for a script with a path as '*'
449 key
.path
= CTGitPath(L
"*");
451 if (it
!= end() && it
->second
.bEnabled
)
458 key
.path
= m_RootPath
;
463 // look for a script with a path as '*'
465 key
.path
= CTGitPath(L
"*");
473 void CHooks::ParseHookString(CString strhooks
, bool bLocal
)
475 // now fill the map with all the hooks defined in the string
476 // the string consists of multiple lines, where one hook script is defined
478 // line 1: the hook type
479 // line 2: path to working copy where to apply the hook script,
480 // if it starts with "!" this hook is disabled (this should provide backward and forward compatibility)
481 // if it starts with "?" this hook is for the current project folder
482 // line 3: command line to execute
483 // line 4: 'true' or 'false' for waiting for the script to finish
484 // line 5: 'show' or 'hide' on how to start the hook script
489 cmd
.bApproved
= !bLocal
; // user configured scripts are pre-approved
491 while ((pos
= strhooks
.Find(L
'\n')) >= 0)
494 key
.htype
= GetHookType(strhooks
.Left(pos
));
495 if (pos
+ 1 < strhooks
.GetLength())
496 strhooks
= strhooks
.Mid(pos
+ 1);
499 bool bComplete
= false;
500 if ((pos
= strhooks
.Find(L
'\n')) >= 0)
504 if (strhooks
[0] == L
'!' && pos
> 1)
506 cmd
.bEnabled
= false;
507 strhooks
= strhooks
.Mid((int)wcslen(L
"!"));
510 if (strhooks
[0] == L
'?' && pos
> 0)
511 key
.path
= CTGitPath(m_RootPath
);
513 key
.path
= CTGitPath(strhooks
.Left(pos
));
514 if (pos
+ 1 < strhooks
.GetLength())
515 strhooks
= strhooks
.Mid(pos
+ 1);
518 if ((pos
= strhooks
.Find(L
'\n')) >= 0)
521 cmd
.commandline
= strhooks
.Left(pos
);
522 if (pos
+ 1 < strhooks
.GetLength())
523 strhooks
= strhooks
.Mid(pos
+ 1);
526 if ((pos
= strhooks
.Find(L
'\n')) >= 0)
529 cmd
.bWait
= (strhooks
.Left(pos
).CompareNoCase(L
"true") == 0);
530 if (pos
+ 1 < strhooks
.GetLength())
531 strhooks
= strhooks
.Mid(pos
+ 1);
534 if ((pos
= strhooks
.Find(L
'\n')) >= 0)
537 cmd
.bShow
= (strhooks
.Left(pos
).CompareNoCase(L
"show") == 0);
538 if (pos
+ 1 < strhooks
.GetLength())
539 strhooks
= strhooks
.Mid(pos
+ 1);
545 temp
.Format(L
"%s|%d%s", m_RootPath
.GetWinPath(), (int)key
.htype
, (LPCTSTR
)cmd
.commandline
);
547 cmd
.sRegKey
= L
"Software\\TortoiseGit\\approvedhooks\\" + CalcSHA256(temp
);
548 CRegDWORD
reg(cmd
.sRegKey
, 0);
549 cmd
.bApproved
= (DWORD(reg
) != 0);
550 cmd
.bStored
= reg
.exists();
559 m_pInstance
->insert(std::pair
<hookkey
, hookcmd
>(key
, cmd
));
563 DWORD
CHooks::RunScript(CString cmd
, LPCTSTR currentDir
, CString
& error
, bool bWait
, bool bShow
)
566 SECURITY_ATTRIBUTES sa
= { 0 };
567 sa
.nLength
= sizeof(sa
);
568 sa
.bInheritHandle
= TRUE
;
574 // clear the error string
577 // Create Temp File for redirection
578 TCHAR szTempPath
[MAX_PATH
] = {0};
579 TCHAR szOutput
[MAX_PATH
] = {0};
580 TCHAR szErr
[MAX_PATH
] = {0};
581 GetTortoiseGitTempPath(_countof(szTempPath
), szTempPath
);
582 GetTempFileName(szTempPath
, L
"git", 0, szErr
);
584 // setup redirection handles
585 // output handle must be WRITE mode, share READ
586 // redirect handle must be READ mode, share WRITE
587 hErr
= CreateFile(szErr
, GENERIC_WRITE
, FILE_SHARE_READ
, &sa
, CREATE_ALWAYS
, FILE_ATTRIBUTE_TEMPORARY
, nullptr);
591 error
= CFormatMessageWrapper();
595 hRedir
= CreateFile(szErr
, GENERIC_READ
, FILE_SHARE_WRITE
, nullptr, OPEN_EXISTING
, 0, nullptr);
599 error
= CFormatMessageWrapper();
603 GetTempFileName(szTempPath
, L
"git", 0, szOutput
);
604 hOut
= CreateFile(szOutput
, GENERIC_WRITE
, FILE_SHARE_READ
, &sa
, CREATE_ALWAYS
, FILE_ATTRIBUTE_TEMPORARY
, nullptr);
608 error
= CFormatMessageWrapper();
612 // setup startup info, set std out/err handles
614 STARTUPINFO si
= { 0 };
616 si
.dwFlags
= STARTF_USESTDHANDLES
| STARTF_USESHOWWINDOW
;
617 si
.hStdOutput
= hOut
;
619 si
.wShowWindow
= bShow
? SW_SHOW
: SW_HIDE
;
621 PROCESS_INFORMATION pi
= { 0 };
622 if (!CreateProcess(nullptr, cmd
.GetBuffer(), nullptr, nullptr, TRUE
, CREATE_UNICODE_ENVIRONMENT
, nullptr, currentDir
, &si
, &pi
))
624 const DWORD err
= GetLastError(); // preserve the CreateProcess error
625 error
= CFormatMessageWrapper(err
);
632 CloseHandle(pi
.hThread
);
634 // wait for process to finish, capture redirection and
635 // send it to the parent window/console
639 char buf
[256] = { 0 };
642 while (ReadFile(hRedir
, &buf
, sizeof(buf
) - 1, &dw
, nullptr))
646 error
+= CString(CStringA(buf
,dw
));
649 } while (WaitForSingleObject(pi
.hProcess
, 0) != WAIT_OBJECT_0
);
651 // perform any final flushing
652 while (ReadFile(hRedir
, &buf
, sizeof(buf
) - 1, &dw
, nullptr))
657 error
+= CString(CStringA(buf
, dw
));
659 WaitForSingleObject(pi
.hProcess
, INFINITE
);
660 GetExitCodeProcess(pi
.hProcess
, &exitcode
);
662 CloseHandle(pi
.hProcess
);
663 DeleteFile(szOutput
);
669 bool CHooks::ApproveHook(HWND hWnd
, hookiterator it
)
671 if (it
->second
.bApproved
|| it
->second
.bStored
)
672 return it
->second
.bApproved
;
675 sQuestion
.Format(IDS_HOOKS_APPROVE_TASK1
, (LPCWSTR
)it
->second
.commandline
);
676 bool bApproved
= false;
677 bool bDoNotAskAgain
= false;
678 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
);
679 taskdlg
.AddCommandControl(1, CString(MAKEINTRESOURCE(IDS_HOOKS_APPROVE_TASK3
)));
680 taskdlg
.AddCommandControl(2, CString(MAKEINTRESOURCE(IDS_HOOKS_APPROVE_TASK4
)));
681 taskdlg
.SetCommonButtons(TDCBF_CANCEL_BUTTON
);
682 taskdlg
.SetVerificationCheckboxText(CString(MAKEINTRESOURCE(IDS_HOOKS_APPROVE_TASK5
)));
683 taskdlg
.SetVerificationCheckbox(false);
684 taskdlg
.SetDefaultCommandControl(2);
685 taskdlg
.SetMainIcon(TD_WARNING_ICON
);
686 taskdlg
.SetFooterText(CString(MAKEINTRESOURCE(IDS_HOOKS_APPROVE_SECURITYHINT
)));
687 bApproved
= taskdlg
.DoModal(hWnd
) == 1;
688 bDoNotAskAgain
= !!taskdlg
.GetVerificationCheckboxState();
692 CRegDWORD
reg(it
->second
.sRegKey
, 0);
693 reg
= bApproved
? 1 : 0;
694 it
->second
.bStored
= true;
696 it
->second
.bApproved
= bApproved
;