Do not alter CGitHeadFileList after it was loaded and stored
[TortoiseGit.git] / src / TortoiseProc / ProjectProperties.cpp
blob3d42cd14a8a932564efbd03699f2b1ec7b52e7de
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.
19 #include "StdAfx.h"
20 #include "TortoiseProc.h"
21 #include "UnicodeUtils.h"
22 #include "ProjectProperties.h"
23 #include "AppUtils.h"
24 //#include "GitProperties.h"
25 #include "TGitPath.h"
26 #include "git.h"
28 using namespace std;
31 struct num_compare
33 bool operator() (const CString& lhs, const CString& rhs) const
35 return StrCmpLogicalW(lhs, rhs) < 0;
39 ProjectProperties::ProjectProperties(void)
40 : regExNeedUpdate (true)
41 , nBugIdPos(-1)
43 bNumber = TRUE;
44 bWarnIfNoIssue = FALSE;
45 nLogWidthMarker = 0;
46 nMinLogSize = 0;
47 nMinLockMsgSize = 0;
48 bFileListInEnglish = TRUE;
49 bAppend = TRUE;
50 lProjectLanguage = 0;
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]))
64 return TRUE;
67 return FALSE;
70 BOOL ProjectProperties::GetStringProps(CString &prop,TCHAR *key,bool bRemoveCR)
72 CString cmd,output;
73 output.Empty();
75 output = g_Git.GetConfigValue(key,CP_UTF8,NULL, bRemoveCR);
77 if(output.IsEmpty())
79 return FALSE;
82 prop = output;
84 return TRUE;
88 BOOL ProjectProperties::GetBOOLProps(BOOL &b,TCHAR *key)
90 CString str,low;
91 if(!GetStringProps(str,key))
92 return FALSE;
94 low = str.MakeLower().Trim();
95 if(low == _T("true") || low == _T("on") || low == _T("yes") || StrToInt(low) != 0)
96 b=true;
97 else
98 b=false;
100 return true;
104 BOOL ProjectProperties::ReadProps(CTGitPath path)
106 CString sPropVal;
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);
119 sCheckRe = sPropVal;
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))
132 CString val;
133 val = sPropVal;
134 if (!val.IsEmpty())
135 nLogWidthMarker = _ttoi(val) + 2; // HACK, + 2 needed
138 if (GetStringProps(sPropVal, PROJECTPROPNAME_PROJECTLANGUAGE))
140 CString val;
141 val = sPropVal;
142 if (!val.IsEmpty())
144 LPTSTR strEnd;
145 lProjectLanguage = _tcstol(val, &strEnd, 0);
149 return TRUE;
152 CString ProjectProperties::GetBugIDFromLog(CString& msg)
154 CString sBugID;
156 if (!sMessage.IsEmpty())
158 CString sBugLine;
159 CString sFirstPart;
160 CString sLastPart;
161 BOOL bTop = FALSE;
162 if (nBugIdPos < 0)
163 return sBugID;
164 sFirstPart = sMessage.Left(nBugIdPos);
165 sLastPart = sMessage.Mid(nBugIdPos + 7);
166 msg.TrimRight('\n');
167 if (msg.ReverseFind('\n')>=0)
169 if (bAppend)
170 sBugLine = msg.Mid(msg.ReverseFind('\n')+1);
171 else
173 sBugLine = msg.Left(msg.Find('\n'));
174 bTop = TRUE;
177 else
179 if (bNumber)
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;
188 break;
191 if (bOnlyNumbers)
192 sBugLine = msg;
194 else
195 sBugLine = msg;
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)
202 sBugLine.Empty();
203 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
204 sBugLine.Empty();
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)
210 sBugLine.Empty();
211 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
212 sBugLine.Empty();
213 bTop = TRUE;
215 if (sBugLine.IsEmpty())
216 return sBugID;
217 sBugID = sBugLine.Mid(sFirstPart.GetLength(), sBugLine.GetLength() - sFirstPart.GetLength() - sLastPart.GetLength());
218 if (bTop)
220 msg = msg.Mid(sBugLine.GetLength());
221 msg.TrimLeft('\n');
223 else
225 msg = msg.Left(msg.GetLength()-sBugLine.GetLength());
226 msg.TrimRight('\n');
229 return sBugID;
232 void ProjectProperties::AutoUpdateRegex()
234 if (regExNeedUpdate)
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)
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())
261 // match with two regex strings (without grouping!)
264 AutoUpdateRegex();
265 const tr1::wsregex_iterator end;
266 wstring s = msg;
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);
281 catch (exception) {}
283 else
287 AutoUpdateRegex();
288 const tr1::wsregex_iterator end;
289 wstring s = msg;
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);
303 catch (exception) {}
306 else if (result.empty() && (!sMessage.IsEmpty()))
308 CString sBugLine;
309 CString sFirstPart;
310 CString sLastPart;
311 BOOL bTop = FALSE;
312 if (nBugIdPos < 0)
313 return result;
315 sFirstPart = sMessage.Left(nBugIdPos);
316 sLastPart = sMessage.Mid(nBugIdPos + 7);
317 CString sMsg = msg;
318 sMsg.TrimRight('\n');
319 if (sMsg.ReverseFind('\n')>=0)
321 if (bAppend)
322 sBugLine = sMsg.Mid(sMsg.ReverseFind('\n')+1);
323 else
325 sBugLine = sMsg.Left(sMsg.Find('\n'));
326 bTop = TRUE;
329 else
330 sBugLine = sMsg;
331 if (sBugLine.Left(sFirstPart.GetLength()).Compare(sFirstPart)!=0)
332 sBugLine.Empty();
333 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
334 sBugLine.Empty();
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)
340 sBugLine.Empty();
341 if (sBugLine.Right(sLastPart.GetLength()).Compare(sLastPart)!=0)
342 sBugLine.Empty();
343 bTop = TRUE;
345 if (sBugLine.IsEmpty())
346 return result;
348 CString sBugIDPart = sBugLine.Mid(sFirstPart.GetLength(), sBugLine.GetLength() - sFirstPart.GetLength() - sLastPart.GetLength());
349 if (sBugIDPart.IsEmpty())
350 return result;
352 //the bug id part can contain several bug id's, separated by commas
353 if (!bTop)
354 offset1 = sMsg.GetLength() - sBugLine.GetLength() + sFirstPart.GetLength();
355 else
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);
371 return result;
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));
392 return bugIDs;
395 CString ProjectProperties::FindBugID(const CString& msg)
397 CString sRet;
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)
409 sRet += *it;
410 sRet += _T(" ");
412 sRet.Trim();
415 return sRet;
418 bool ProjectProperties::MightContainABugID()
420 return !sCheckRe.IsEmpty() || (nBugIdPos >= 0);
423 CString ProjectProperties::GetBugIDUrl(const CString& sBugID)
425 CString ret;
426 if (sUrl.IsEmpty())
427 return ret;
428 if (!sMessage.IsEmpty() || !sCheckRe.IsEmpty())
430 ret = sUrl;
431 ret.Replace(_T("%BUGID%"), sBugID);
433 return ret;
436 BOOL ProjectProperties::CheckBugID(const CString& sID)
438 if (bNumber)
440 // check if the revision actually _is_ a number
441 // or a list of numbers separated by colons
442 TCHAR c = 0;
443 int len = sID.GetLength();
444 for (int i=0; i<len; ++i)
446 c = sID.GetAt(i);
447 if ((c < '0')&&(c != ',')&&(c != ' '))
449 return FALSE;
451 if (c > '9')
452 return FALSE;
455 return TRUE;
458 BOOL ProjectProperties::HasBugID(const CString& sMessage)
460 if (!sCheckRe.IsEmpty())
464 AutoUpdateRegex();
465 return tr1::regex_search((LPCTSTR)sMessage, regCheck);
467 catch (exception) {}
469 return FALSE;
472 #ifdef DEBUG
473 static class PropTest
475 public:
476 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"));
490 sRet.Trim();
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")));
499 sRet.Trim();
500 ATLASSERT(sRet.Compare(_T("666 7463"))==0);
501 sRet = props.FindBugID(_T("This is a test for Issue #850,#1234,#1345"));
502 sRet.Trim();
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"));
508 sRet.Trim();
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"));
514 sRet.Trim();
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")));
519 } PropTest;
520 #endif