Fix typos
[TortoiseGit.git] / src / TortoiseProc / ProjectProperties.cpp
blob2a4746271907c9b0a29eb40f74106defe5da3197
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2021, 2023-2024 - 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"
27 #include "git2/sys/errors.h"
29 struct num_compare
31 bool operator() (const CString& lhs, const CString& rhs) const
33 return StrCmpLogicalW(lhs, rhs) < 0;
37 ProjectProperties::ProjectProperties()
41 int ProjectProperties::ReadProps()
43 CAutoConfig gitconfig(true);
44 CAutoRepository repo(g_Git.GetGitRepository());
45 CString adminDirPath;
46 if (GitAdminDir::GetAdminDirPath(g_Git.m_CurrentDir, adminDirPath))
47 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
49 if (!GitAdminDir::IsBareRepo(g_Git.m_CurrentDir))
50 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
51 else
53 CString tmpFile = CTempFiles::Instance().GetTempFilePath(true).GetWinPathString();
54 CTGitPath path(L".tgitconfig");
55 if (g_Git.GetOneFile(L"HEAD", path, tmpFile) == 0)
56 git_config_add_file_ondisk(gitconfig, CGit::GetGitPathStringA(tmpFile), GIT_CONFIG_LEVEL_LOCAL, nullptr, FALSE); // this needs to have the second highest priority
59 git_config_add_file_ondisk(gitconfig, CGit::GetGitPathStringA(g_Git.GetGitGlobalConfig()), GIT_CONFIG_LEVEL_GLOBAL, repo, FALSE);
60 git_config_add_file_ondisk(gitconfig,CGit::GetGitPathStringA(g_Git.GetGitGlobalXDGConfig()), GIT_CONFIG_LEVEL_XDG, repo, FALSE);
61 git_config_add_file_ondisk(gitconfig, CGit::GetGitPathStringA(g_Git.GetGitSystemConfig()), GIT_CONFIG_LEVEL_SYSTEM, repo, FALSE);
62 git_error_clear();
64 CString sPropVal;
66 gitconfig.GetString(BUGTRAQPROPNAME_LABEL, sLabel);
67 gitconfig.GetString(BUGTRAQPROPNAME_MESSAGE, sMessage);
68 nBugIdPos = sMessage.Find(L"%BUGID%");
69 gitconfig.GetString(BUGTRAQPROPNAME_URL, sUrl);
71 gitconfig.GetBOOL(BUGTRAQPROPNAME_WARNIFNOISSUE, bWarnIfNoIssue);
72 gitconfig.GetBOOL(BUGTRAQPROPNAME_NUMBER, bNumber);
73 gitconfig.GetBOOL(BUGTRAQPROPNAME_APPEND, bAppend);
75 gitconfig.GetString(BUGTRAQPROPNAME_PROVIDERUUID, sProviderUuid);
76 gitconfig.GetString(BUGTRAQPROPNAME_PROVIDERUUID64, sProviderUuid64);
77 gitconfig.GetString(BUGTRAQPROPNAME_PROVIDERPARAMS, sProviderParams);
79 gitconfig.GetBOOL(PROJECTPROPNAME_WARNNOSIGNEDOFFBY, bWarnNoSignedOffBy);
80 gitconfig.GetString(PROJECTPROPNAME_ICON, sIcon);
82 gitconfig.GetString(BUGTRAQPROPNAME_LOGREGEX, sPropVal);
84 sCheckRe = sPropVal;
85 if (sCheckRe.Find('\n')>=0)
87 sBugIDRe = sCheckRe.Mid(sCheckRe.Find('\n')).Trim();
88 sCheckRe = sCheckRe.Left(sCheckRe.Find('\n')).Trim();
90 if (!sCheckRe.IsEmpty())
91 sCheckRe = sCheckRe.Trim();
93 if (gitconfig.GetString(PROJECTPROPNAME_LOGWIDTHLINE, sPropVal) == 0)
95 CString val;
96 val = sPropVal;
97 if (!val.IsEmpty())
98 nLogWidthMarker = _wtoi(val);
101 if (gitconfig.GetString(PROJECTPROPNAME_PROJECTLANGUAGE, sPropVal) == 0)
103 CString val;
104 val = sPropVal;
105 if (val == L"-1")
106 lProjectLanguage = -1;
107 if (!val.IsEmpty())
109 LPWSTR strEnd;
110 lProjectLanguage = wcstol(val, &strEnd, 0);
114 if (gitconfig.GetString(PROJECTPROPNAME_LOGMINSIZE, sPropVal) == 0)
116 CString val;
117 val = sPropVal;
118 if (!val.IsEmpty())
119 nMinLogSize = _wtoi(val);
122 FetchHookString(gitconfig, PROJECTPROPNAME_STARTCOMMITHOOK, sStartCommitHook);
123 FetchHookString(gitconfig, PROJECTPROPNAME_PRECOMMITHOOK, sPreCommitHook);
124 FetchHookString(gitconfig, PROJECTPROPNAME_POSTCOMMITHOOK, sPostCommitHook);
125 FetchHookString(gitconfig, PROJECTPROPNAME_PREPUSHHOOK, sPrePushHook);
126 FetchHookString(gitconfig, PROJECTPROPNAME_POSTPUSHHOOK, sPostPushHook);
127 FetchHookString(gitconfig, PROJECTPROPNAME_PREREBASEHOOK, sPreRebaseHook);
129 return 0;
132 CString ProjectProperties::GetBugIDFromLog(CString& msg)
134 CString sBugID;
136 if (!sMessage.IsEmpty())
138 CString sBugLine;
139 CString sFirstPart;
140 CString sLastPart;
141 BOOL bTop = FALSE;
142 if (nBugIdPos < 0)
143 return sBugID;
144 sFirstPart = sMessage.Left(nBugIdPos);
145 sLastPart = sMessage.Mid(nBugIdPos + 7);
146 msg.TrimRight('\n');
147 if (msg.ReverseFind('\n')>=0)
149 if (bAppend)
150 sBugLine = msg.Mid(msg.ReverseFind('\n')+1);
151 else
153 sBugLine = msg.Left(msg.Find('\n'));
154 bTop = TRUE;
157 else
159 if (bNumber)
161 // find out if the message consists only of numbers
162 bool bOnlyNumbers = true;
163 for (int i=0; i<msg.GetLength(); ++i)
165 if (!_istdigit(msg[i]))
167 bOnlyNumbers = false;
168 break;
171 if (bOnlyNumbers)
172 sBugLine = msg;
174 else
175 sBugLine = msg;
177 if (sBugLine.IsEmpty() && (msg.ReverseFind('\n') < 0))
178 sBugLine = msg.Mid(msg.ReverseFind('\n')+1);
179 if (sBugLine.Left(sFirstPart.GetLength()).Compare(sFirstPart)!=0)
180 sBugLine.Empty();
181 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
182 sBugLine.Empty();
183 if (sBugLine.IsEmpty())
185 if (msg.Find('\n')>=0)
186 sBugLine = msg.Left(msg.Find('\n'));
187 if (sBugLine.Left(sFirstPart.GetLength()).Compare(sFirstPart)!=0)
188 sBugLine.Empty();
189 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
190 sBugLine.Empty();
191 bTop = TRUE;
193 if (sBugLine.IsEmpty())
194 return sBugID;
195 sBugID = sBugLine.Mid(sFirstPart.GetLength(), sBugLine.GetLength() - sFirstPart.GetLength() - sLastPart.GetLength());
196 if (bTop)
198 msg = msg.Mid(sBugLine.GetLength());
199 msg.TrimLeft('\n');
201 else
203 msg = msg.Left(msg.GetLength()-sBugLine.GetLength());
204 msg.TrimRight('\n');
207 return sBugID;
210 void ProjectProperties::AutoUpdateRegex()
212 if (regExNeedUpdate)
216 regCheck = std::wregex(sCheckRe);
217 regBugID = std::wregex(sBugIDRe);
219 catch (std::exception&)
223 regExNeedUpdate = false;
227 void ProjectProperties::FetchHookString(CAutoConfig& gitconfig, const CString& sBase, CString& sHook)
229 sHook.Empty();
230 CString sVal;
231 gitconfig.GetString(sBase + L"cmdline", sVal);
232 if (sVal.IsEmpty())
233 return;
234 sHook += sVal + L'\n';
235 bool boolval = false;
236 gitconfig.GetBool(sBase + L"wait", boolval);
237 if (boolval)
238 sHook += L"true\n";
239 else
240 sHook += L"false\n";
241 boolval = false;
242 gitconfig.GetBool(sBase + L"show", boolval);
243 if (boolval)
244 sHook += L"show";
245 else
246 sHook += L"hide";
249 std::vector<CHARRANGE> ProjectProperties::FindBugIDPositions(const CString& msg)
251 size_t offset1 = 0;
252 size_t offset2 = 0;
253 std::vector<CHARRANGE> result;
255 // first use the checkre string to find bug ID's in the message
256 if (!sCheckRe.IsEmpty())
258 if (!sBugIDRe.IsEmpty())
260 // match with two regex strings (without grouping!)
263 AutoUpdateRegex();
264 const std::wsregex_iterator end;
265 std::wstring s { static_cast<LPCWSTR>(msg) };
266 for (std::wsregex_iterator it(s.cbegin(), s.cend(), regCheck); it != end; ++it)
268 // (*it)[0] is the matched string
269 std::wstring matchedString = (*it)[0];
270 ptrdiff_t matchpos = it->position(0);
271 for (std::wsregex_iterator it2(matchedString.cbegin(), matchedString.cend(), regBugID); it2 != end; ++it2)
273 //ATLTRACE(L"matched id : %s\n", (*it2)[0].str().c_str());
274 ptrdiff_t matchposID = it2->position(0);
275 CHARRANGE range = { static_cast<LONG>(matchpos + matchposID), static_cast<LONG>(matchpos+matchposID + (*it2)[0].str().size()) };
276 result.push_back(range);
280 catch (std::exception&) {}
282 else
286 AutoUpdateRegex();
287 const std::wsregex_iterator end;
288 std::wstring s { static_cast<LPCWSTR>(msg) };
289 for (std::wsregex_iterator it(s.cbegin(), s.cend(), regCheck); it != end; ++it)
291 const std::wsmatch match = *it;
292 // we define group 1 as the whole issue text and
293 // group 2 as the bug ID
294 if (match.size() >= 2)
296 //ATLTRACE(L"matched id : %s\n", std::wstring(match[1]).c_str());
297 CHARRANGE range = { static_cast<LONG>(match[1].first - s.cbegin()), static_cast<LONG>(match[1].second - s.cbegin()) };
298 result.push_back(range);
302 catch (std::exception&) {}
305 else if (result.empty() && (!sMessage.IsEmpty()))
307 CString sBugLine;
308 CString sFirstPart;
309 CString sLastPart;
310 BOOL bTop = FALSE;
311 if (nBugIdPos < 0)
312 return result;
314 sFirstPart = sMessage.Left(nBugIdPos);
315 sLastPart = sMessage.Mid(nBugIdPos + 7);
316 CString sMsg = msg;
317 sMsg.TrimRight('\n');
318 if (sMsg.ReverseFind('\n')>=0)
320 if (bAppend)
321 sBugLine = sMsg.Mid(sMsg.ReverseFind('\n')+1);
322 else
324 sBugLine = sMsg.Left(sMsg.Find('\n'));
325 bTop = TRUE;
328 else
329 sBugLine = sMsg;
330 if (sBugLine.Left(sFirstPart.GetLength()).Compare(sFirstPart)!=0)
331 sBugLine.Empty();
332 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
333 sBugLine.Empty();
334 if (sBugLine.IsEmpty())
336 if (sMsg.Find('\n')>=0)
337 sBugLine = sMsg.Left(sMsg.Find('\n'));
338 if (sBugLine.Left(sFirstPart.GetLength()).Compare(sFirstPart)!=0)
339 sBugLine.Empty();
340 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
341 sBugLine.Empty();
342 bTop = TRUE;
344 if (sBugLine.IsEmpty())
345 return result;
347 CString sBugIDPart = sBugLine.Mid(sFirstPart.GetLength(), sBugLine.GetLength() - sFirstPart.GetLength() - sLastPart.GetLength());
348 if (sBugIDPart.IsEmpty())
349 return result;
351 //the bug id part can contain several bug id's, separated by commas
352 if (!bTop)
353 offset1 = sMsg.GetLength() - sBugLine.GetLength() + sFirstPart.GetLength();
354 else
355 offset1 = sFirstPart.GetLength();
356 sBugIDPart.Trim(L',');
357 while (sBugIDPart.Find(',')>=0)
359 offset2 = offset1 + sBugIDPart.Find(',');
360 CHARRANGE range = { static_cast<LONG>(offset1), static_cast<LONG>(offset2) };
361 result.push_back(range);
362 sBugIDPart = sBugIDPart.Mid(sBugIDPart.Find(',')+1);
363 offset1 = offset2 + 1;
365 offset2 = offset1 + sBugIDPart.GetLength();
366 CHARRANGE range = { static_cast<LONG>(offset1), static_cast<LONG>(offset2) };
367 result.push_back(range);
370 return result;
373 BOOL ProjectProperties::FindBugID(const CString& msg, CWnd * pWnd)
375 std::vector<CHARRANGE> positions = FindBugIDPositions(msg);
376 CCommonAppUtils::SetCharFormat(pWnd, CFM_LINK, CFE_LINK, positions);
378 return positions.empty() ? FALSE : TRUE;
381 std::set<CString> ProjectProperties::FindBugIDs (const CString& msg)
383 std::vector<CHARRANGE> positions = FindBugIDPositions(msg);
384 std::set<CString> bugIDs;
386 for (const auto& pos : positions)
387 bugIDs.insert(msg.Mid(pos.cpMin, pos.cpMax - pos.cpMin));
389 return bugIDs;
392 CString ProjectProperties::FindBugID(const CString& msg)
394 CString sRet;
395 if (!sCheckRe.IsEmpty() || (nBugIdPos >= 0))
397 std::vector<CHARRANGE> positions = FindBugIDPositions(msg);
398 std::set<CString, num_compare> bugIDs;
399 for (const auto& pos : positions)
400 bugIDs.insert(msg.Mid(pos.cpMin, pos.cpMax - pos.cpMin));
402 for (const auto& id : bugIDs)
404 sRet += id;
405 sRet += L' ';
407 sRet.Trim();
410 return sRet;
413 bool ProjectProperties::MightContainABugID()
415 return !sCheckRe.IsEmpty() || (nBugIdPos >= 0);
418 CString ProjectProperties::GetBugIDUrl(const CString& sBugID)
420 CString ret = sUrl;
421 if (sUrl.IsEmpty())
422 return ret;
423 if (!sMessage.IsEmpty() || !sCheckRe.IsEmpty())
424 ReplaceBugIDPlaceholder(ret, sBugID);
425 return ret;
428 void ProjectProperties::ReplaceBugIDPlaceholder(CString& url, const CString& sBugID)
430 CString parameter;
431 DWORD size = INTERNET_MAX_URL_LENGTH;
432 if (SysInfo::Instance().IsWin8OrLater())
433 UrlEscape(sBugID, CStrBuf(parameter, size + 1), &size, URL_ESCAPE_SEGMENT_ONLY | URL_ESCAPE_PERCENT | URL_ESCAPE_AS_UTF8 | URL_ESCAPE_ASCII_URI_COMPONENT);
434 else
436 UrlEscape(sBugID, CStrBuf(parameter, size + 1), &size, URL_ESCAPE_SEGMENT_ONLY | URL_ESCAPE_PERCENT | URL_ESCAPE_AS_UTF8);
437 parameter.Replace(L"!", L"%21");
438 parameter.Replace(L"$", L"%24");
439 parameter.Replace(L"'", L"%27");
440 parameter.Replace(L"(", L"%28");
441 parameter.Replace(L")", L"%29");
442 parameter.Replace(L"*", L"%2A");
443 parameter.Replace(L"+", L"%2B");
444 parameter.Replace(L",", L"%2C");
445 parameter.Replace(L":", L"%3A");
446 parameter.Replace(L";", L"%3B");
447 parameter.Replace(L"=", L"%3D");
448 parameter.Replace(L"@", L"%40");
450 url.Replace(L"%BUGID%", parameter);
453 BOOL ProjectProperties::CheckBugID(const CString& sID)
455 if (bNumber)
457 // check if the revision actually _is_ a number
458 // or a list of numbers separated by colons
459 int len = sID.GetLength();
460 for (int i=0; i<len; ++i)
462 wchar_t c = sID.GetAt(i);
463 if ((c < '0')&&(c != ',')&&(c != ' '))
464 return FALSE;
465 if (c > '9')
466 return FALSE;
469 return TRUE;
472 BOOL ProjectProperties::HasBugID(const CString& sMsg)
474 if (!sCheckRe.IsEmpty())
478 AutoUpdateRegex();
479 return std::regex_search(static_cast<LPCWSTR>(sMsg), regCheck);
481 catch (std::exception&) {}
483 return FALSE;