Drop support for Git < 2.24 (released in November 2019)
[TortoiseGit.git] / src / TortoiseProc / ProjectProperties.cpp
blob4d45fc6e6d42ec528e7e9eceef339287b42cda30
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2021 - TortoiseGit
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software Foundation,
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 #include "stdafx.h"
20 #include "ProjectProperties.h"
21 #include "CommonAppUtils.h"
22 #include "Git.h"
23 #include "UnicodeUtils.h"
24 #include "TempFile.h"
25 #include <WinInet.h>
26 #include "SysInfo.h"
28 struct num_compare
30 bool operator() (const CString& lhs, const CString& rhs) const
32 return StrCmpLogicalW(lhs, rhs) < 0;
36 ProjectProperties::ProjectProperties()
37 : regExNeedUpdate (true)
38 , nBugIdPos(-1)
39 , bWarnNoSignedOffBy(FALSE)
40 , bNumber(TRUE)
41 , bWarnIfNoIssue(FALSE)
42 , nLogWidthMarker(0)
43 , nMinLogSize(0)
44 , bFileListInEnglish(TRUE)
45 , bAppend(TRUE)
46 , lProjectLanguage(0)
50 int ProjectProperties::ReadProps()
52 CAutoConfig gitconfig(true);
53 CAutoRepository repo(g_Git.GetGitRepository());
54 CString adminDirPath;
55 if (GitAdminDir::GetAdminDirPath(g_Git.m_CurrentDir, adminDirPath))
56 git_config_add_file_ondisk(gitconfig, CGit::GetGitPathStringA(adminDirPath + L"config"), GIT_CONFIG_LEVEL_APP, repo, FALSE); // this needs to have the highest priority in order to override .tgitconfig settings
58 if (!GitAdminDir::IsBareRepo(g_Git.m_CurrentDir))
59 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
60 else
62 CString tmpFile = CTempFiles::Instance().GetTempFilePath(true).GetWinPathString();
63 CTGitPath path(L".tgitconfig");
64 if (g_Git.GetOneFile(L"HEAD", path, tmpFile) == 0)
65 git_config_add_file_ondisk(gitconfig, CGit::GetGitPathStringA(tmpFile), GIT_CONFIG_LEVEL_LOCAL, nullptr, FALSE); // this needs to have the second highest priority
68 git_config_add_file_ondisk(gitconfig, CGit::GetGitPathStringA(g_Git.GetGitGlobalConfig()), GIT_CONFIG_LEVEL_GLOBAL, repo, FALSE);
69 git_config_add_file_ondisk(gitconfig,CGit::GetGitPathStringA(g_Git.GetGitGlobalXDGConfig()), GIT_CONFIG_LEVEL_XDG, repo, FALSE);
70 git_config_add_file_ondisk(gitconfig, CGit::GetGitPathStringA(g_Git.GetGitSystemConfig()), GIT_CONFIG_LEVEL_SYSTEM, repo, FALSE);
71 git_error_clear();
73 CString sPropVal;
75 gitconfig.GetString(BUGTRAQPROPNAME_LABEL, sLabel);
76 gitconfig.GetString(BUGTRAQPROPNAME_MESSAGE, sMessage);
77 nBugIdPos = sMessage.Find(L"%BUGID%");
78 gitconfig.GetString(BUGTRAQPROPNAME_URL, sUrl);
80 gitconfig.GetBOOL(BUGTRAQPROPNAME_WARNIFNOISSUE, bWarnIfNoIssue);
81 gitconfig.GetBOOL(BUGTRAQPROPNAME_NUMBER, bNumber);
82 gitconfig.GetBOOL(BUGTRAQPROPNAME_APPEND, bAppend);
84 gitconfig.GetString(BUGTRAQPROPNAME_PROVIDERUUID, sProviderUuid);
85 gitconfig.GetString(BUGTRAQPROPNAME_PROVIDERUUID64, sProviderUuid64);
86 gitconfig.GetString(BUGTRAQPROPNAME_PROVIDERPARAMS, sProviderParams);
88 gitconfig.GetBOOL(PROJECTPROPNAME_WARNNOSIGNEDOFFBY, bWarnNoSignedOffBy);
89 gitconfig.GetString(PROJECTPROPNAME_ICON, sIcon);
91 gitconfig.GetString(BUGTRAQPROPNAME_LOGREGEX, sPropVal);
93 sCheckRe = sPropVal;
94 if (sCheckRe.Find('\n')>=0)
96 sBugIDRe = sCheckRe.Mid(sCheckRe.Find('\n')).Trim();
97 sCheckRe = sCheckRe.Left(sCheckRe.Find('\n')).Trim();
99 if (!sCheckRe.IsEmpty())
100 sCheckRe = sCheckRe.Trim();
102 if (gitconfig.GetString(PROJECTPROPNAME_LOGWIDTHLINE, sPropVal) == 0)
104 CString val;
105 val = sPropVal;
106 if (!val.IsEmpty())
107 nLogWidthMarker = _wtoi(val);
110 if (gitconfig.GetString(PROJECTPROPNAME_PROJECTLANGUAGE, sPropVal) == 0)
112 CString val;
113 val = sPropVal;
114 if (val == L"-1")
115 lProjectLanguage = -1;
116 if (!val.IsEmpty())
118 LPWSTR strEnd;
119 lProjectLanguage = wcstol(val, &strEnd, 0);
123 if (gitconfig.GetString(PROJECTPROPNAME_LOGMINSIZE, sPropVal) == 0)
125 CString val;
126 val = sPropVal;
127 if (!val.IsEmpty())
128 nMinLogSize = _wtoi(val);
131 FetchHookString(gitconfig, PROJECTPROPNAME_STARTCOMMITHOOK, sStartCommitHook);
132 FetchHookString(gitconfig, PROJECTPROPNAME_PRECOMMITHOOK, sPreCommitHook);
133 FetchHookString(gitconfig, PROJECTPROPNAME_POSTCOMMITHOOK, sPostCommitHook);
134 FetchHookString(gitconfig, PROJECTPROPNAME_PREPUSHHOOK, sPrePushHook);
135 FetchHookString(gitconfig, PROJECTPROPNAME_POSTPUSHHOOK, sPostPushHook);
136 FetchHookString(gitconfig, PROJECTPROPNAME_PREREBASEHOOK, sPreRebaseHook);
138 return 0;
141 CString ProjectProperties::GetBugIDFromLog(CString& msg)
143 CString sBugID;
145 if (!sMessage.IsEmpty())
147 CString sBugLine;
148 CString sFirstPart;
149 CString sLastPart;
150 BOOL bTop = FALSE;
151 if (nBugIdPos < 0)
152 return sBugID;
153 sFirstPart = sMessage.Left(nBugIdPos);
154 sLastPart = sMessage.Mid(nBugIdPos + 7);
155 msg.TrimRight('\n');
156 if (msg.ReverseFind('\n')>=0)
158 if (bAppend)
159 sBugLine = msg.Mid(msg.ReverseFind('\n')+1);
160 else
162 sBugLine = msg.Left(msg.Find('\n'));
163 bTop = TRUE;
166 else
168 if (bNumber)
170 // find out if the message consists only of numbers
171 bool bOnlyNumbers = true;
172 for (int i=0; i<msg.GetLength(); ++i)
174 if (!_istdigit(msg[i]))
176 bOnlyNumbers = false;
177 break;
180 if (bOnlyNumbers)
181 sBugLine = msg;
183 else
184 sBugLine = msg;
186 if (sBugLine.IsEmpty() && (msg.ReverseFind('\n') < 0))
187 sBugLine = msg.Mid(msg.ReverseFind('\n')+1);
188 if (sBugLine.Left(sFirstPart.GetLength()).Compare(sFirstPart)!=0)
189 sBugLine.Empty();
190 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
191 sBugLine.Empty();
192 if (sBugLine.IsEmpty())
194 if (msg.Find('\n')>=0)
195 sBugLine = msg.Left(msg.Find('\n'));
196 if (sBugLine.Left(sFirstPart.GetLength()).Compare(sFirstPart)!=0)
197 sBugLine.Empty();
198 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
199 sBugLine.Empty();
200 bTop = TRUE;
202 if (sBugLine.IsEmpty())
203 return sBugID;
204 sBugID = sBugLine.Mid(sFirstPart.GetLength(), sBugLine.GetLength() - sFirstPart.GetLength() - sLastPart.GetLength());
205 if (bTop)
207 msg = msg.Mid(sBugLine.GetLength());
208 msg.TrimLeft('\n');
210 else
212 msg = msg.Left(msg.GetLength()-sBugLine.GetLength());
213 msg.TrimRight('\n');
216 return sBugID;
219 void ProjectProperties::AutoUpdateRegex()
221 if (regExNeedUpdate)
225 regCheck = std::wregex(sCheckRe);
226 regBugID = std::wregex(sBugIDRe);
228 catch (std::exception&)
232 regExNeedUpdate = false;
236 void ProjectProperties::FetchHookString(CAutoConfig& gitconfig, const CString& sBase, CString& sHook)
238 sHook.Empty();
239 CString sVal;
240 gitconfig.GetString(sBase + L"cmdline", sVal);
241 if (sVal.IsEmpty())
242 return;
243 sHook += sVal + L'\n';
244 bool boolval = false;
245 gitconfig.GetBool(sBase + L"wait", boolval);
246 if (boolval)
247 sHook += L"true\n";
248 else
249 sHook += L"false\n";
250 boolval = false;
251 gitconfig.GetBool(sBase + L"show", boolval);
252 if (boolval)
253 sHook += L"show";
254 else
255 sHook += L"hide";
258 std::vector<CHARRANGE> ProjectProperties::FindBugIDPositions(const CString& msg)
260 size_t offset1 = 0;
261 size_t offset2 = 0;
262 std::vector<CHARRANGE> result;
264 // first use the checkre string to find bug ID's in the message
265 if (!sCheckRe.IsEmpty())
267 if (!sBugIDRe.IsEmpty())
269 // match with two regex strings (without grouping!)
272 AutoUpdateRegex();
273 const std::wsregex_iterator end;
274 std::wstring s = msg;
275 for (std::wsregex_iterator it(s.cbegin(), s.cend(), regCheck); it != end; ++it)
277 // (*it)[0] is the matched string
278 std::wstring matchedString = (*it)[0];
279 ptrdiff_t matchpos = it->position(0);
280 for (std::wsregex_iterator it2(matchedString.cbegin(), matchedString.cend(), regBugID); it2 != end; ++it2)
282 //ATLTRACE(L"matched id : %s\n", (*it2)[0].str().c_str());
283 ptrdiff_t matchposID = it2->position(0);
284 CHARRANGE range = { static_cast<LONG>(matchpos + matchposID), static_cast<LONG>(matchpos+matchposID + (*it2)[0].str().size()) };
285 result.push_back(range);
289 catch (std::exception&) {}
291 else
295 AutoUpdateRegex();
296 const std::wsregex_iterator end;
297 std::wstring s = msg;
298 for (std::wsregex_iterator it(s.cbegin(), s.cend(), regCheck); it != end; ++it)
300 const std::wsmatch match = *it;
301 // we define group 1 as the whole issue text and
302 // group 2 as the bug ID
303 if (match.size() >= 2)
305 //ATLTRACE(L"matched id : %s\n", std::wstring(match[1]).c_str());
306 CHARRANGE range = { static_cast<LONG>(match[1].first - s.cbegin()), static_cast<LONG>(match[1].second - s.cbegin()) };
307 result.push_back(range);
311 catch (std::exception&) {}
314 else if (result.empty() && (!sMessage.IsEmpty()))
316 CString sBugLine;
317 CString sFirstPart;
318 CString sLastPart;
319 BOOL bTop = FALSE;
320 if (nBugIdPos < 0)
321 return result;
323 sFirstPart = sMessage.Left(nBugIdPos);
324 sLastPart = sMessage.Mid(nBugIdPos + 7);
325 CString sMsg = msg;
326 sMsg.TrimRight('\n');
327 if (sMsg.ReverseFind('\n')>=0)
329 if (bAppend)
330 sBugLine = sMsg.Mid(sMsg.ReverseFind('\n')+1);
331 else
333 sBugLine = sMsg.Left(sMsg.Find('\n'));
334 bTop = TRUE;
337 else
338 sBugLine = sMsg;
339 if (sBugLine.Left(sFirstPart.GetLength()).Compare(sFirstPart)!=0)
340 sBugLine.Empty();
341 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
342 sBugLine.Empty();
343 if (sBugLine.IsEmpty())
345 if (sMsg.Find('\n')>=0)
346 sBugLine = sMsg.Left(sMsg.Find('\n'));
347 if (sBugLine.Left(sFirstPart.GetLength()).Compare(sFirstPart)!=0)
348 sBugLine.Empty();
349 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
350 sBugLine.Empty();
351 bTop = TRUE;
353 if (sBugLine.IsEmpty())
354 return result;
356 CString sBugIDPart = sBugLine.Mid(sFirstPart.GetLength(), sBugLine.GetLength() - sFirstPart.GetLength() - sLastPart.GetLength());
357 if (sBugIDPart.IsEmpty())
358 return result;
360 //the bug id part can contain several bug id's, separated by commas
361 if (!bTop)
362 offset1 = sMsg.GetLength() - sBugLine.GetLength() + sFirstPart.GetLength();
363 else
364 offset1 = sFirstPart.GetLength();
365 sBugIDPart.Trim(L',');
366 while (sBugIDPart.Find(',')>=0)
368 offset2 = offset1 + sBugIDPart.Find(',');
369 CHARRANGE range = { static_cast<LONG>(offset1), static_cast<LONG>(offset2) };
370 result.push_back(range);
371 sBugIDPart = sBugIDPart.Mid(sBugIDPart.Find(',')+1);
372 offset1 = offset2 + 1;
374 offset2 = offset1 + sBugIDPart.GetLength();
375 CHARRANGE range = { static_cast<LONG>(offset1), static_cast<LONG>(offset2) };
376 result.push_back(range);
379 return result;
382 BOOL ProjectProperties::FindBugID(const CString& msg, CWnd * pWnd)
384 std::vector<CHARRANGE> positions = FindBugIDPositions(msg);
385 CCommonAppUtils::SetCharFormat(pWnd, CFM_LINK, CFE_LINK, positions);
387 return positions.empty() ? FALSE : TRUE;
390 std::set<CString> ProjectProperties::FindBugIDs (const CString& msg)
392 std::vector<CHARRANGE> positions = FindBugIDPositions(msg);
393 std::set<CString> bugIDs;
395 for (const auto& pos : positions)
396 bugIDs.insert(msg.Mid(pos.cpMin, pos.cpMax - pos.cpMin));
398 return bugIDs;
401 CString ProjectProperties::FindBugID(const CString& msg)
403 CString sRet;
404 if (!sCheckRe.IsEmpty() || (nBugIdPos >= 0))
406 std::vector<CHARRANGE> positions = FindBugIDPositions(msg);
407 std::set<CString, num_compare> bugIDs;
408 for (const auto& pos : positions)
409 bugIDs.insert(msg.Mid(pos.cpMin, pos.cpMax - pos.cpMin));
411 for (const auto& id : bugIDs)
413 sRet += id;
414 sRet += L' ';
416 sRet.Trim();
419 return sRet;
422 bool ProjectProperties::MightContainABugID()
424 return !sCheckRe.IsEmpty() || (nBugIdPos >= 0);
427 CString ProjectProperties::GetBugIDUrl(const CString& sBugID)
429 CString ret = sUrl;
430 if (sUrl.IsEmpty())
431 return ret;
432 if (!sMessage.IsEmpty() || !sCheckRe.IsEmpty())
433 ReplaceBugIDPlaceholder(ret, sBugID);
434 return ret;
437 void ProjectProperties::ReplaceBugIDPlaceholder(CString& url, const CString& sBugID)
439 CString parameter;
440 DWORD size = INTERNET_MAX_URL_LENGTH;
441 if (SysInfo::Instance().IsWin8OrLater())
442 UrlEscape(sBugID, CStrBuf(parameter, size + 1), &size, URL_ESCAPE_SEGMENT_ONLY | URL_ESCAPE_PERCENT | URL_ESCAPE_AS_UTF8 | URL_ESCAPE_ASCII_URI_COMPONENT);
443 else
445 UrlEscape(sBugID, CStrBuf(parameter, size + 1), &size, URL_ESCAPE_SEGMENT_ONLY | URL_ESCAPE_PERCENT | URL_ESCAPE_AS_UTF8);
446 parameter.Replace(L"!", L"%21");
447 parameter.Replace(L"$", L"%24");
448 parameter.Replace(L"'", L"%27");
449 parameter.Replace(L"(", L"%28");
450 parameter.Replace(L")", L"%29");
451 parameter.Replace(L"*", L"%2A");
452 parameter.Replace(L"+", L"%2B");
453 parameter.Replace(L",", L"%2C");
454 parameter.Replace(L":", L"%3A");
455 parameter.Replace(L";", L"%3B");
456 parameter.Replace(L"=", L"%3D");
457 parameter.Replace(L"@", L"%40");
459 url.Replace(L"%BUGID%", parameter);
462 BOOL ProjectProperties::CheckBugID(const CString& sID)
464 if (bNumber)
466 // check if the revision actually _is_ a number
467 // or a list of numbers separated by colons
468 int len = sID.GetLength();
469 for (int i=0; i<len; ++i)
471 wchar_t c = sID.GetAt(i);
472 if ((c < '0')&&(c != ',')&&(c != ' '))
473 return FALSE;
474 if (c > '9')
475 return FALSE;
478 return TRUE;
481 BOOL ProjectProperties::HasBugID(const CString& sMsg)
483 if (!sCheckRe.IsEmpty())
487 AutoUpdateRegex();
488 return std::regex_search(static_cast<LPCWSTR>(sMsg), regCheck);
490 catch (std::exception&) {}
492 return FALSE;