1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2011 - 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 "TortoiseProc.h"
21 #include "UnicodeUtils.h"
22 #include "ProjectProperties.h"
24 //#include "GitProperties.h"
33 bool operator() (const CString
& lhs
, const CString
& rhs
) const
35 return StrCmpLogicalW(lhs
, rhs
) < 0;
39 ProjectProperties::ProjectProperties(void)
40 : regExNeedUpdate (true)
44 bWarnIfNoIssue
= FALSE
;
48 bFileListInEnglish
= TRUE
;
53 ProjectProperties::~ProjectProperties(void)
58 BOOL
ProjectProperties::ReadPropsPathList(const CTGitPathList
& pathList
)
60 for(int nPath
= 0; nPath
< pathList
.GetCount(); nPath
++)
62 if (ReadProps(pathList
[nPath
]))
70 BOOL
ProjectProperties::GetStringProps(CString
&prop
,TCHAR
*key
,bool bRemoveCR
)
75 output
= g_Git
.GetConfigValue(key
,CP_UTF8
,NULL
, bRemoveCR
);
88 BOOL
ProjectProperties::GetBOOLProps(BOOL
&b
,TCHAR
*key
)
91 if(!GetStringProps(str
,key
))
94 low
= str
.MakeLower().Trim();
95 if(low
== _T("true") || low
== _T("on") || low
== _T("yes") || StrToInt(low
) != 0)
104 BOOL
ProjectProperties::ReadProps(CTGitPath path
)
108 GetStringProps(this->sLabel
,BUGTRAQPROPNAME_LABEL
);
109 GetStringProps(this->sMessage
,BUGTRAQPROPNAME_MESSAGE
);
110 nBugIdPos
= sMessage
.Find(L
"%BUGID%");
111 GetStringProps(this->sUrl
,BUGTRAQPROPNAME_URL
);
113 GetBOOLProps(this->bWarnIfNoIssue
,BUGTRAQPROPNAME_WARNIFNOISSUE
);
114 GetBOOLProps(this->bNumber
,BUGTRAQPROPNAME_NUMBER
);
115 GetBOOLProps(this->bAppend
,BUGTRAQPROPNAME_APPEND
);
117 GetStringProps(sPropVal
,BUGTRAQPROPNAME_LOGREGEX
,false);
120 if (sCheckRe
.Find('\n')>=0)
122 sBugIDRe
= sCheckRe
.Mid(sCheckRe
.Find('\n')).Trim();
123 sCheckRe
= sCheckRe
.Left(sCheckRe
.Find('\n')).Trim();
125 if (!sCheckRe
.IsEmpty())
127 sCheckRe
= sCheckRe
.Trim();
130 if (GetStringProps(sPropVal
, PROJECTPROPNAME_LOGWIDTHLINE
))
135 nLogWidthMarker
= _ttoi(val
) + 2; // HACK, + 2 needed
138 if (GetStringProps(sPropVal
, PROJECTPROPNAME_PROJECTLANGUAGE
))
145 lProjectLanguage
= _tcstol(val
, &strEnd
, 0);
152 CString
ProjectProperties::GetBugIDFromLog(CString
& msg
)
156 if (!sMessage
.IsEmpty())
164 sFirstPart
= sMessage
.Left(nBugIdPos
);
165 sLastPart
= sMessage
.Mid(nBugIdPos
+ 7);
167 if (msg
.ReverseFind('\n')>=0)
170 sBugLine
= msg
.Mid(msg
.ReverseFind('\n')+1);
173 sBugLine
= msg
.Left(msg
.Find('\n'));
181 // find out if the message consists only of numbers
182 bool bOnlyNumbers
= true;
183 for (int i
=0; i
<msg
.GetLength(); ++i
)
185 if (!_istdigit(msg
[i
]))
187 bOnlyNumbers
= false;
197 if (sBugLine
.IsEmpty() && (msg
.ReverseFind('\n') < 0))
199 sBugLine
= msg
.Mid(msg
.ReverseFind('\n')+1);
201 if (sBugLine
.Left(sFirstPart
.GetLength()).Compare(sFirstPart
)!=0)
203 if (sBugLine
.Right(sLastPart
.GetLength()).Compare(sLastPart
)!=0)
205 if (sBugLine
.IsEmpty())
207 if (msg
.Find('\n')>=0)
208 sBugLine
= msg
.Left(msg
.Find('\n'));
209 if (sBugLine
.Left(sFirstPart
.GetLength()).Compare(sFirstPart
)!=0)
211 if (sBugLine
.Right(sLastPart
.GetLength()).Compare(sLastPart
)!=0)
215 if (sBugLine
.IsEmpty())
217 sBugID
= sBugLine
.Mid(sFirstPart
.GetLength(), sBugLine
.GetLength() - sFirstPart
.GetLength() - sLastPart
.GetLength());
220 msg
= msg
.Mid(sBugLine
.GetLength());
225 msg
= msg
.Left(msg
.GetLength()-sBugLine
.GetLength());
232 void ProjectProperties::AutoUpdateRegex()
238 regCheck
= tr1::wregex(sCheckRe
);
239 regBugID
= tr1::wregex(sBugIDRe
);
241 catch (std::exception
)
245 regExNeedUpdate
= false;
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())
261 // match with two regex strings (without grouping!)
265 const tr1::wsregex_iterator end
;
267 for (tr1::wsregex_iterator
it(s
.begin(), s
.end(), regCheck
); it
!= end
; ++it
)
269 // (*it)[0] is the matched string
270 wstring matchedString
= (*it
)[0];
271 ptrdiff_t matchpos
= it
->position(0);
272 for (tr1::wsregex_iterator
it2(matchedString
.begin(), matchedString
.end(), regBugID
); it2
!= end
; ++it2
)
274 ATLTRACE(_T("matched id : %s\n"), (*it2
)[0].str().c_str());
275 ptrdiff_t matchposID
= it2
->position(0);
276 CHARRANGE range
= {(LONG
)(matchpos
+matchposID
), (LONG
)(matchpos
+matchposID
+(*it2
)[0].str().size())};
277 result
.push_back(range
);
288 const tr1::wsregex_iterator end
;
290 for (tr1::wsregex_iterator
it(s
.begin(), s
.end(), regCheck
); it
!= end
; ++it
)
292 const tr1::wsmatch match
= *it
;
293 // we define group 1 as the whole issue text and
294 // group 2 as the bug ID
295 if (match
.size() >= 2)
297 ATLTRACE(_T("matched id : %s\n"), wstring(match
[1]).c_str());
298 CHARRANGE range
= {(LONG
)(match
[1].first
-s
.begin()), (LONG
)(match
[1].second
-s
.begin())};
299 result
.push_back(range
);
306 else if (result
.empty() && (!sMessage
.IsEmpty()))
315 sFirstPart
= sMessage
.Left(nBugIdPos
);
316 sLastPart
= sMessage
.Mid(nBugIdPos
+ 7);
318 sMsg
.TrimRight('\n');
319 if (sMsg
.ReverseFind('\n')>=0)
322 sBugLine
= sMsg
.Mid(sMsg
.ReverseFind('\n')+1);
325 sBugLine
= sMsg
.Left(sMsg
.Find('\n'));
331 if (sBugLine
.Left(sFirstPart
.GetLength()).Compare(sFirstPart
)!=0)
333 if (sBugLine
.Right(sLastPart
.GetLength()).Compare(sLastPart
)!=0)
335 if (sBugLine
.IsEmpty())
337 if (sMsg
.Find('\n')>=0)
338 sBugLine
= sMsg
.Left(sMsg
.Find('\n'));
339 if (sBugLine
.Left(sFirstPart
.GetLength()).Compare(sFirstPart
)!=0)
341 if (sBugLine
.Right(sLastPart
.GetLength()).Compare(sLastPart
)!=0)
345 if (sBugLine
.IsEmpty())
348 CString sBugIDPart
= sBugLine
.Mid(sFirstPart
.GetLength(), sBugLine
.GetLength() - sFirstPart
.GetLength() - sLastPart
.GetLength());
349 if (sBugIDPart
.IsEmpty())
352 //the bug id part can contain several bug id's, separated by commas
354 offset1
= sMsg
.GetLength() - sBugLine
.GetLength() + sFirstPart
.GetLength();
356 offset1
= sFirstPart
.GetLength();
357 sBugIDPart
.Trim(_T(","));
358 while (sBugIDPart
.Find(',')>=0)
360 offset2
= offset1
+ sBugIDPart
.Find(',');
361 CHARRANGE range
= {(LONG
)offset1
, (LONG
)offset2
};
362 result
.push_back(range
);
363 sBugIDPart
= sBugIDPart
.Mid(sBugIDPart
.Find(',')+1);
364 offset1
= offset2
+ 1;
366 offset2
= offset1
+ sBugIDPart
.GetLength();
367 CHARRANGE range
= {(LONG
)offset1
, (LONG
)offset2
};
368 result
.push_back(range
);
374 BOOL
ProjectProperties::FindBugID(const CString
& msg
, CWnd
* pWnd
)
376 std::vector
<CHARRANGE
> positions
= FindBugIDPositions(msg
);
377 CAppUtils::SetCharFormat(pWnd
, CFM_LINK
, CFE_LINK
, positions
);
379 return positions
.empty() ? FALSE
: TRUE
;
382 std::set
<CString
> ProjectProperties::FindBugIDs (const CString
& msg
)
384 std::vector
<CHARRANGE
> positions
= FindBugIDPositions(msg
);
385 std::set
<CString
> bugIDs
;
387 for (std::vector
<CHARRANGE
>::iterator iter
= positions
.begin(), end
= positions
.end(); iter
!= end
; ++iter
)
389 bugIDs
.insert(msg
.Mid(iter
->cpMin
, iter
->cpMax
- iter
->cpMin
));
395 CString
ProjectProperties::FindBugID(const CString
& msg
)
398 if (!sCheckRe
.IsEmpty() || (nBugIdPos
>= 0))
400 std::vector
<CHARRANGE
> positions
= FindBugIDPositions(msg
);
401 std::set
<CString
, num_compare
> bugIDs
;
402 for (std::vector
<CHARRANGE
>::iterator iter
= positions
.begin(), end
= positions
.end(); iter
!= end
; ++iter
)
404 bugIDs
.insert(msg
.Mid(iter
->cpMin
, iter
->cpMax
- iter
->cpMin
));
407 for (std::set
<CString
, num_compare
>::iterator it
= bugIDs
.begin(); it
!= bugIDs
.end(); ++it
)
418 bool ProjectProperties::MightContainABugID()
420 return !sCheckRe
.IsEmpty() || (nBugIdPos
>= 0);
423 CString
ProjectProperties::GetBugIDUrl(const CString
& sBugID
)
428 if (!sMessage
.IsEmpty() || !sCheckRe
.IsEmpty())
431 ret
.Replace(_T("%BUGID%"), sBugID
);
436 BOOL
ProjectProperties::CheckBugID(const CString
& sID
)
440 // check if the revision actually _is_ a number
441 // or a list of numbers separated by colons
443 int len
= sID
.GetLength();
444 for (int i
=0; i
<len
; ++i
)
447 if ((c
< '0')&&(c
!= ',')&&(c
!= ' '))
458 BOOL
ProjectProperties::HasBugID(const CString
& sMessage
)
460 if (!sCheckRe
.IsEmpty())
465 return tr1::regex_search((LPCTSTR
)sMessage
, regCheck
);
473 static class PropTest
478 CString msg
= _T("this is a test logmessage: issue 222\nIssue #456, #678, 901 #456");
479 CString sUrl
= _T("http://tortoisesvn.tigris.org/issues/show_bug.cgi?id=%BUGID%");
480 CString sCheckRe
= _T("[Ii]ssue #?(\\d+)(,? ?#?(\\d+))+");
481 CString sBugIDRe
= _T("(\\d+)");
482 ProjectProperties props
;
483 props
.sCheckRe
= _T("PAF-[0-9]+");
484 props
.sUrl
= _T("http://tortoisesvn.tigris.org/issues/show_bug.cgi?id=%BUGID%");
485 CString sRet
= props
.FindBugID(_T("This is a test for PAF-88"));
486 ATLASSERT(sRet
.IsEmpty());
487 props
.sCheckRe
= _T("[Ii]ssue #?(\\d+)");
488 props
.regExNeedUpdate
= true;
489 sRet
= props
.FindBugID(_T("Testing issue #99"));
491 ATLASSERT(sRet
.Compare(_T("99"))==0);
492 props
.sCheckRe
= _T("[Ii]ssues?:?(\\s*(,|and)?\\s*#\\d+)+");
493 props
.sBugIDRe
= _T("(\\d+)");
494 props
.sUrl
= _T("http://tortoisesvn.tigris.org/issues/show_bug.cgi?id=%BUGID%");
495 props
.regExNeedUpdate
= true;
496 sRet
= props
.FindBugID(_T("This is a test for Issue #7463,#666"));
497 ATLASSERT(props
.HasBugID(_T("This is a test for Issue #7463,#666")));
498 ATLASSERT(!props
.HasBugID(_T("This is a test for Issue 7463,666")));
500 ATLASSERT(sRet
.Compare(_T("666 7463"))==0);
501 sRet
= props
.FindBugID(_T("This is a test for Issue #850,#1234,#1345"));
503 ATLASSERT(sRet
.Compare(_T("850 1234 1345"))==0);
504 props
.sCheckRe
= _T("^\\[(\\d+)\\].*");
505 props
.sUrl
= _T("http://tortoisesvn.tigris.org/issues/show_bug.cgi?id=%BUGID%");
506 props
.regExNeedUpdate
= true;
507 sRet
= props
.FindBugID(_T("[000815] some stupid programming error fixed"));
509 ATLASSERT(sRet
.Compare(_T("000815"))==0);
510 props
.sCheckRe
= _T("\\[\\[(\\d+)\\]\\]\\]");
511 props
.sUrl
= _T("http://tortoisesvn.tigris.org/issues/show_bug.cgi?id=%BUGID%");
512 props
.regExNeedUpdate
= true;
513 sRet
= props
.FindBugID(_T("test test [[000815]]] some stupid programming error fixed"));
515 ATLASSERT(sRet
.Compare(_T("000815"))==0);
516 ATLASSERT(props
.HasBugID(_T("test test [[000815]]] some stupid programming error fixed")));
517 ATLASSERT(!props
.HasBugID(_T("test test [000815]] some stupid programming error fixed")));