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.
20 #include "ProjectProperties.h"
21 #include "CommonAppUtils.h"
23 #include "UnicodeUtils.h"
27 #include "git2/sys/errors.h"
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());
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
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
);
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
);
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)
98 nLogWidthMarker
= _wtoi(val
);
101 if (gitconfig
.GetString(PROJECTPROPNAME_PROJECTLANGUAGE
, sPropVal
) == 0)
106 lProjectLanguage
= -1;
110 lProjectLanguage
= wcstol(val
, &strEnd
, 0);
114 if (gitconfig
.GetString(PROJECTPROPNAME_LOGMINSIZE
, sPropVal
) == 0)
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
);
132 CString
ProjectProperties::GetBugIDFromLog(CString
& msg
)
136 if (!sMessage
.IsEmpty())
144 sFirstPart
= sMessage
.Left(nBugIdPos
);
145 sLastPart
= sMessage
.Mid(nBugIdPos
+ 7);
147 if (msg
.ReverseFind('\n')>=0)
150 sBugLine
= msg
.Mid(msg
.ReverseFind('\n')+1);
153 sBugLine
= msg
.Left(msg
.Find('\n'));
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;
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)
181 if (sBugLine
.Right(sLastPart
.GetLength()).Compare(sLastPart
)!=0)
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)
189 if (sBugLine
.Right(sLastPart
.GetLength()).Compare(sLastPart
)!=0)
193 if (sBugLine
.IsEmpty())
195 sBugID
= sBugLine
.Mid(sFirstPart
.GetLength(), sBugLine
.GetLength() - sFirstPart
.GetLength() - sLastPart
.GetLength());
198 msg
= msg
.Mid(sBugLine
.GetLength());
203 msg
= msg
.Left(msg
.GetLength()-sBugLine
.GetLength());
210 void ProjectProperties::AutoUpdateRegex()
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
)
231 gitconfig
.GetString(sBase
+ L
"cmdline", sVal
);
234 sHook
+= sVal
+ L
'\n';
235 bool boolval
= false;
236 gitconfig
.GetBool(sBase
+ L
"wait", boolval
);
242 gitconfig
.GetBool(sBase
+ L
"show", boolval
);
249 std::vector
<CHARRANGE
> ProjectProperties::FindBugIDPositions(const CString
& msg
)
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!)
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
&) {}
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()))
314 sFirstPart
= sMessage
.Left(nBugIdPos
);
315 sLastPart
= sMessage
.Mid(nBugIdPos
+ 7);
317 sMsg
.TrimRight('\n');
318 if (sMsg
.ReverseFind('\n')>=0)
321 sBugLine
= sMsg
.Mid(sMsg
.ReverseFind('\n')+1);
324 sBugLine
= sMsg
.Left(sMsg
.Find('\n'));
330 if (sBugLine
.Left(sFirstPart
.GetLength()).Compare(sFirstPart
)!=0)
332 if (sBugLine
.Right(sLastPart
.GetLength()).Compare(sLastPart
)!=0)
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)
340 if (sBugLine
.Right(sLastPart
.GetLength()).Compare(sLastPart
)!=0)
344 if (sBugLine
.IsEmpty())
347 CString sBugIDPart
= sBugLine
.Mid(sFirstPart
.GetLength(), sBugLine
.GetLength() - sFirstPart
.GetLength() - sLastPart
.GetLength());
348 if (sBugIDPart
.IsEmpty())
351 //the bug id part can contain several bug id's, separated by commas
353 offset1
= sMsg
.GetLength() - sBugLine
.GetLength() + sFirstPart
.GetLength();
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
);
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
));
392 CString
ProjectProperties::FindBugID(const CString
& msg
)
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
)
413 bool ProjectProperties::MightContainABugID()
415 return !sCheckRe
.IsEmpty() || (nBugIdPos
>= 0);
418 CString
ProjectProperties::GetBugIDUrl(const CString
& sBugID
)
423 if (!sMessage
.IsEmpty() || !sCheckRe
.IsEmpty())
424 ReplaceBugIDPlaceholder(ret
, sBugID
);
428 void ProjectProperties::ReplaceBugIDPlaceholder(CString
& url
, const CString
& sBugID
)
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
);
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
)
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
!= ' '))
472 BOOL
ProjectProperties::HasBugID(const CString
& sMsg
)
474 if (!sCheckRe
.IsEmpty())
479 return std::regex_search(static_cast<LPCWSTR
>(sMsg
), regCheck
);
481 catch (std::exception
&) {}