1 // TortoiseMerge - a Diff/Patch program
\r
3 // Copyright (C) 2004-2009 - TortoiseSVN
\r
5 // This program is free software; you can redistribute it and/or
\r
6 // modify it under the terms of the GNU General Public License
\r
7 // as published by the Free Software Foundation; either version 2
\r
8 // of the License, or (at your option) any later version.
\r
10 // This program is distributed in the hope that it will be useful,
\r
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
\r
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
\r
13 // GNU General Public License for more details.
\r
15 // You should have received a copy of the GNU General Public License
\r
16 // along with this program; if not, write to the Free Software Foundation,
\r
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
\r
20 #include "Resource.h"
\r
21 #include "UnicodeUtils.h"
\r
22 #include "DirFileEnum.h"
\r
23 #include "TortoiseMerge.h"
\r
25 #include "GitAdminDir.h"
\r
29 #define new DEBUG_NEW
\r
31 static char THIS_FILE[] = __FILE__;
\r
34 CPatch::CPatch(void)
\r
37 m_IsGitPatch = false;
\r
40 CPatch::~CPatch(void)
\r
45 void CPatch::FreeMemory()
\r
47 for (int i=0; i<m_arFileDiffs.GetCount(); i++)
\r
49 Chunks * chunks = m_arFileDiffs.GetAt(i);
\r
50 for (int j=0; j<chunks->chunks.GetCount(); j++)
\r
52 delete chunks->chunks.GetAt(j);
\r
54 chunks->chunks.RemoveAll();
\r
57 m_arFileDiffs.RemoveAll();
\r
60 BOOL CPatch::OpenUnifiedDiffFile(const CString& filename)
\r
63 EOL ending = EOL_NOENDING;
\r
65 INT_PTR nLineCount = 0;
\r
66 g_crasher.AddFile((LPCSTR)(LPCTSTR)filename, (LPCSTR)(LPCTSTR)_T("unified diff file"));
\r
68 CFileTextLines PatchLines;
\r
69 if (!PatchLines.Load(filename))
\r
71 m_sErrorMessage = PatchLines.GetErrorString();
\r
74 m_UnicodeType = PatchLines.GetUnicodeType();
\r
76 nLineCount = PatchLines.GetCount();
\r
77 //now we got all the lines of the patch file
\r
78 //in our array - parsing can start...
\r
80 for(nIndex=0;PatchLines.GetCount();nIndex++)
\r
82 sLine = PatchLines.GetAt(nIndex);
\r
83 if(sLine.Left(10).Compare(_T("diff --git")) == 0)
\r
85 this->m_IsGitPatch=true;
\r
90 //first, skip possible garbage at the beginning
\r
91 //garbage is finished when a line starts with "Index: "
\r
92 //and the next line consists of only "=" characters
\r
93 for (nIndex=0; nIndex<PatchLines.GetCount(); nIndex++)
\r
95 sLine = PatchLines.GetAt(nIndex);
\r
96 if (sLine.Left(4).Compare(_T("--- "))==0)
\r
98 if ((nIndex+1)<PatchLines.GetCount())
\r
100 sLine = PatchLines.GetAt(nIndex+1);
\r
102 if(sLine.IsEmpty()&&m_IsGitPatch)
\r
105 sLine.Replace(_T("="), _T(""));
\r
106 if (sLine.IsEmpty())
\r
110 if ((PatchLines.GetCount()-nIndex) < 2)
\r
112 //no file entry found.
\r
113 m_sErrorMessage.LoadString(IDS_ERR_PATCH_NOINDEX);
\r
117 //from this point on we have the real unified diff data
\r
119 Chunks * chunks = NULL;
\r
120 Chunk * chunk = NULL;
\r
121 int nAddLineCount = 0;
\r
122 int nRemoveLineCount = 0;
\r
123 int nContextLineCount = 0;
\r
124 for ( ;nIndex<PatchLines.GetCount(); nIndex++)
\r
126 sLine = PatchLines.GetAt(nIndex);
\r
127 ending = PatchLines.GetLineEnding(nIndex);
\r
128 if (ending != EOL_NOENDING)
\r
129 ending = EOL_AUTOLINE;
\r
132 if ((sLine.Left(4).Compare(_T("--- "))==0)&&((sLine.Find('\t') >= 0)||this->m_IsGitPatch))
\r
137 //this is a new file diff, so add the last one to
\r
139 m_arFileDiffs.Add(chunks);
\r
141 chunks = new Chunks();
\r
143 int nTab = sLine.Find('\t');
\r
148 nTab=sLine.GetLength();
\r
154 chunks->sFilePath = sLine.Mid(filestart, nTab-filestart).Trim();
\r
160 case 0: //Index: <filepath>
\r
163 if ((nIndex+1)<PatchLines.GetCount())
\r
165 nextLine = PatchLines.GetAt(nIndex+1);
\r
166 if (!nextLine.IsEmpty())
\r
168 nextLine.Replace(_T("="), _T(""));
\r
169 if (nextLine.IsEmpty())
\r
173 //this is a new file diff, so add the last one to
\r
175 m_arFileDiffs.Add(chunks);
\r
177 chunks = new Chunks();
\r
178 int nColon = sLine.Find(':');
\r
181 chunks->sFilePath = sLine.Mid(nColon+1).Trim();
\r
182 if (chunks->sFilePath.Find('\t')>=0)
\r
183 chunks->sFilePath.Left(chunks->sFilePath.Find('\t')).TrimRight();
\r
184 if (chunks->sFilePath.Right(9).Compare(_T("(deleted)"))==0)
\r
185 chunks->sFilePath.Left(chunks->sFilePath.GetLength()-9).TrimRight();
\r
186 if (chunks->sFilePath.Right(7).Compare(_T("(added)"))==0)
\r
187 chunks->sFilePath.Left(chunks->sFilePath.GetLength()-7).TrimRight();
\r
199 if (chunks == NULL)
\r
202 //Index: <filepath>
\r
203 //was not found at the start of a file diff!
\r
210 case 1: //====================
\r
212 sLine.Replace(_T("="), _T(""));
\r
213 if (sLine.IsEmpty())
\r
215 // if the next line is already the start of the chunk,
\r
216 // then the patch/diff file was not created by svn. But we
\r
217 // still try to use it
\r
218 if (PatchLines.GetCount() > (nIndex + 1))
\r
221 if (PatchLines.GetAt(nIndex+1).Left(2).Compare(_T("@@"))==0)
\r
231 //=========================
\r
233 m_sErrorMessage.Format(IDS_ERR_PATCH_NOEQUATIONCHARLINE, nIndex);
\r
238 case 2: //--- <filepath>
\r
240 if (sLine.Left(3).Compare(_T("---"))!=0)
\r
242 //no starting "---" found
\r
243 //seems to be either garbage or just
\r
244 //a binary file. So start over...
\r
254 sLine = sLine.Mid(3); //remove the "---"
\r
255 sLine =sLine.Trim();
\r
256 //at the end of the filepath there's a revision number...
\r
257 int bracket = sLine.ReverseFind('(');
\r
259 // some patch files can have another '(' char, especially ones created in Chinese OS
\r
260 bracket = sLine.ReverseFind(0xff08);
\r
261 CString num = sLine.Mid(bracket); //num = "(revision xxxxx)"
\r
262 num = num.Mid(num.Find(' '));
\r
263 num = num.Trim(_T(" )"));
\r
264 // here again, check for the Chinese bracket
\r
265 num = num.Trim(0xff09);
\r
266 chunks->sRevision = num;
\r
269 if (chunks->sFilePath.IsEmpty())
\r
270 chunks->sFilePath = sLine.Trim();
\r
273 chunks->sFilePath = sLine.Left(bracket-1).Trim();
\r
274 if (chunks->sFilePath.Find('\t')>=0)
\r
276 chunks->sFilePath = chunks->sFilePath.Left(chunks->sFilePath.Find('\t'));
\r
281 case 3: //+++ <filepath>
\r
283 if (sLine.Left(3).Compare(_T("+++"))!=0)
\r
285 //no starting "+++" found
\r
286 m_sErrorMessage.Format(IDS_ERR_PATCH_NOADDFILELINE, nIndex);
\r
289 sLine = sLine.Mid(3); //remove the "---"
\r
290 sLine =sLine.Trim();
\r
291 //at the end of the filepath there's a revision number...
\r
292 int bracket = sLine.ReverseFind('(');
\r
294 // some patch files can have another '(' char, especially ones created in Chinese OS
\r
295 bracket = sLine.ReverseFind(0xff08);
\r
296 CString num = sLine.Mid(bracket); //num = "(revision xxxxx)"
\r
297 num = num.Mid(num.Find(' '));
\r
298 num = num.Trim(_T(" )"));
\r
299 // here again, check for the Chinese bracket
\r
300 num = num.Trim(0xff09);
\r
301 chunks->sRevision2 = num;
\r
303 chunks->sFilePath2 = sLine.Trim();
\r
305 chunks->sFilePath2 = sLine.Left(bracket-1).Trim();
\r
306 if (chunks->sFilePath2.Find('\t')>=0)
\r
308 chunks->sFilePath2 = chunks->sFilePath2.Left(chunks->sFilePath2.Find('\t'));
\r
313 case 4: //@@ -xxx,xxx +xxx,xxx @@
\r
315 //start of a new chunk
\r
316 if (sLine.Left(2).Compare(_T("@@"))!=0)
\r
318 //chunk doesn't start with "@@"
\r
319 //so there's garbage in between two file diffs
\r
327 for (int i=0; i<chunks->chunks.GetCount(); i++)
\r
329 delete chunks->chunks.GetAt(i);
\r
331 chunks->chunks.RemoveAll();
\r
336 break; //skip the garbage
\r
338 sLine = sLine.Mid(2);
\r
339 sLine = sLine.Trim();
\r
340 chunk = new Chunk();
\r
341 CString sRemove = sLine.Left(sLine.Find(' '));
\r
342 CString sAdd = sLine.Mid(sLine.Find(' '));
\r
343 chunk->lRemoveStart = (-_ttol(sRemove));
\r
344 if (sRemove.Find(',')>=0)
\r
346 sRemove = sRemove.Mid(sRemove.Find(',')+1);
\r
347 chunk->lRemoveLength = _ttol(sRemove);
\r
351 chunk->lRemoveStart = 0;
\r
352 chunk->lRemoveLength = (-_ttol(sRemove));
\r
354 chunk->lAddStart = _ttol(sAdd);
\r
355 if (sAdd.Find(',')>=0)
\r
357 sAdd = sAdd.Mid(sAdd.Find(',')+1);
\r
358 chunk->lAddLength = _ttol(sAdd);
\r
362 chunk->lAddStart = 1;
\r
363 chunk->lAddLength = _ttol(sAdd);
\r
368 case 5: //[ |+|-] <sourceline>
\r
370 //this line is either a context line (with a ' ' in front)
\r
371 //a line added (with a '+' in front)
\r
372 //or a removed line (with a '-' in front)
\r
374 if (sLine.IsEmpty())
\r
377 type = sLine.GetAt(0);
\r
380 //it's a context line - we don't use them here right now
\r
381 //but maybe in the future the patch algorithm can be
\r
382 //extended to use those in case the file to patch has
\r
383 //already changed and no base file is around...
\r
384 chunk->arLines.Add(RemoveUnicodeBOM(sLine.Mid(1)));
\r
385 chunk->arLinesStates.Add(PATCHSTATE_CONTEXT);
\r
386 chunk->arEOLs.push_back(ending);
\r
387 nContextLineCount++;
\r
389 else if (type == '\\')
\r
391 //it's a context line (sort of):
\r
392 //warnings start with a '\' char (e.g. "\ No newline at end of file")
\r
393 //so just ignore this...
\r
395 else if (type == '-')
\r
398 chunk->arLines.Add(RemoveUnicodeBOM(sLine.Mid(1)));
\r
399 chunk->arLinesStates.Add(PATCHSTATE_REMOVED);
\r
400 chunk->arEOLs.push_back(ending);
\r
401 nRemoveLineCount++;
\r
403 else if (type == '+')
\r
406 chunk->arLines.Add(RemoveUnicodeBOM(sLine.Mid(1)));
\r
407 chunk->arLinesStates.Add(PATCHSTATE_ADDED);
\r
408 chunk->arEOLs.push_back(ending);
\r
413 //none of those lines! what the hell happened here?
\r
414 m_sErrorMessage.Format(IDS_ERR_PATCH_UNKOWNLINETYPE, nIndex);
\r
417 if ((chunk->lAddLength == (nAddLineCount + nContextLineCount)) &&
\r
418 chunk->lRemoveLength == (nRemoveLineCount + nContextLineCount))
\r
420 //chunk is finished
\r
422 chunks->chunks.Add(chunk);
\r
427 nContextLineCount = 0;
\r
428 nRemoveLineCount = 0;
\r
435 } // switch (state)
\r
436 } // for ( ;nIndex<m_PatchLines.GetCount(); nIndex++)
\r
439 m_sErrorMessage.LoadString(IDS_ERR_PATCH_CHUNKMISMATCH);
\r
443 m_arFileDiffs.Add(chunks);
\r
450 for (int i=0; i<chunks->chunks.GetCount(); i++)
\r
452 delete chunks->chunks.GetAt(i);
\r
454 chunks->chunks.RemoveAll();
\r
461 CString CPatch::GetFilename(int nIndex)
\r
465 if (nIndex < m_arFileDiffs.GetCount())
\r
467 Chunks * c = m_arFileDiffs.GetAt(nIndex);
\r
468 CString filepath = Strip(c->sFilePath);
\r
474 CString CPatch::GetRevision(int nIndex)
\r
478 if (nIndex < m_arFileDiffs.GetCount())
\r
480 Chunks * c = m_arFileDiffs.GetAt(nIndex);
\r
481 return c->sRevision;
\r
486 CString CPatch::GetFilename2(int nIndex)
\r
490 if (nIndex < m_arFileDiffs.GetCount())
\r
492 Chunks * c = m_arFileDiffs.GetAt(nIndex);
\r
493 CString filepath = Strip(c->sFilePath2);
\r
499 CString CPatch::GetRevision2(int nIndex)
\r
503 if (nIndex < m_arFileDiffs.GetCount())
\r
505 Chunks * c = m_arFileDiffs.GetAt(nIndex);
\r
506 return c->sRevision2;
\r
511 BOOL CPatch::PatchFile(const CString& sPath, const CString& sSavePath, const CString& sBaseFile)
\r
513 if (PathIsDirectory(sPath))
\r
515 m_sErrorMessage.Format(IDS_ERR_PATCH_INVALIDPATCHFILE, (LPCTSTR)sPath);
\r
518 // find the entry in the patch file which matches the full path given in sPath.
\r
520 // use the longest path that matches
\r
522 for (int i=0; i<GetNumberOfFiles(); i++)
\r
524 CString temppath = sPath;
\r
525 CString temp = GetFilename(i);
\r
526 temppath.Replace('/', '\\');
\r
527 temp.Replace('/', '\\');
\r
528 if (temppath.Mid(temppath.GetLength()-temp.GetLength()-1, 1).CompareNoCase(_T("\\"))==0)
\r
530 temppath = temppath.Right(temp.GetLength());
\r
531 if ((temp.CompareNoCase(temppath)==0))
\r
533 if (nMaxMatch < temp.GetLength())
\r
535 nMaxMatch = temp.GetLength();
\r
540 else if (temppath.CompareNoCase(temp)==0)
\r
542 if ((nIndex < 0)&&(! temp.IsEmpty()))
\r
550 m_sErrorMessage.Format(IDS_ERR_PATCH_FILENOTINPATCH, (LPCTSTR)sPath);
\r
555 CString sPatchFile = sBaseFile.IsEmpty() ? sPath : sBaseFile;
\r
556 if (PathFileExists(sPatchFile))
\r
558 g_crasher.AddFile((LPCSTR)(LPCTSTR)sPatchFile, (LPCSTR)(LPCTSTR)_T("File to patch"));
\r
560 CFileTextLines PatchLines;
\r
561 CFileTextLines PatchLinesResult;
\r
562 PatchLines.Load(sPatchFile);
\r
563 PatchLinesResult = PatchLines; //.Copy(PatchLines);
\r
564 PatchLines.CopySettings(&PatchLinesResult);
\r
566 Chunks * chunks = m_arFileDiffs.GetAt(nIndex);
\r
568 for (int i=0; i<chunks->chunks.GetCount(); i++)
\r
570 Chunk * chunk = chunks->chunks.GetAt(i);
\r
571 LONG lRemoveLine = chunk->lRemoveStart;
\r
572 LONG lAddLine = chunk->lAddStart;
\r
573 for (int j=0; j<chunk->arLines.GetCount(); j++)
\r
575 CString sPatchLine = chunk->arLines.GetAt(j);
\r
576 EOL ending = chunk->arEOLs[j];
\r
577 if ((m_UnicodeType != CFileTextLines::UTF8)&&(m_UnicodeType != CFileTextLines::UTF8BOM))
\r
579 if ((PatchLines.GetUnicodeType()==CFileTextLines::UTF8)||(m_UnicodeType == CFileTextLines::UTF8BOM))
\r
581 // convert the UTF-8 contents in CString sPatchLine into a CStringA
\r
582 sPatchLine = CUnicodeUtils::GetUnicode(CStringA(sPatchLine));
\r
585 int nPatchState = (int)chunk->arLinesStates.GetAt(j);
\r
586 switch (nPatchState)
\r
588 case PATCHSTATE_REMOVED:
\r
590 if ((lAddLine > PatchLines.GetCount())||(PatchLines.GetCount()==0))
\r
592 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, _T(""), (LPCTSTR)sPatchLine);
\r
597 if ((sPatchLine.Compare(PatchLines.GetAt(lAddLine-1))!=0)&&(!HasExpandedKeyWords(sPatchLine)))
\r
599 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, (LPCTSTR)sPatchLine, (LPCTSTR)PatchLines.GetAt(lAddLine-1));
\r
602 if (lAddLine > PatchLines.GetCount())
\r
604 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, (LPCTSTR)sPatchLine, _T(""));
\r
607 PatchLines.RemoveAt(lAddLine-1);
\r
610 case PATCHSTATE_ADDED:
\r
614 PatchLines.InsertAt(lAddLine-1, sPatchLine, ending);
\r
618 case PATCHSTATE_CONTEXT:
\r
620 if (lAddLine > PatchLines.GetCount())
\r
622 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, _T(""), (LPCTSTR)sPatchLine);
\r
627 if (lRemoveLine == 0)
\r
629 if ((sPatchLine.Compare(PatchLines.GetAt(lAddLine-1))!=0) &&
\r
630 (!HasExpandedKeyWords(sPatchLine)) &&
\r
631 (lRemoveLine <= PatchLines.GetCount()) &&
\r
632 (sPatchLine.Compare(PatchLines.GetAt(lRemoveLine-1))!=0))
\r
634 if ((lAddLine < PatchLines.GetCount())&&(sPatchLine.Compare(PatchLines.GetAt(lAddLine))==0))
\r
636 else if (((lAddLine + 1) < PatchLines.GetCount())&&(sPatchLine.Compare(PatchLines.GetAt(lAddLine+1))==0))
\r
638 else if ((lRemoveLine < PatchLines.GetCount())&&(sPatchLine.Compare(PatchLines.GetAt(lRemoveLine))==0))
\r
642 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, (LPCTSTR)sPatchLine, (LPCTSTR)PatchLines.GetAt(lAddLine-1));
\r
653 } // switch (nPatchState)
\r
654 } // for (j=0; j<chunk->arLines.GetCount(); j++)
\r
655 } // for (int i=0; i<chunks->chunks.GetCount(); i++)
\r
656 if (!sSavePath.IsEmpty())
\r
658 PatchLines.Save(sSavePath, false);
\r
663 BOOL CPatch::HasExpandedKeyWords(const CString& line)
\r
665 if (line.Find(_T("$LastChangedDate"))>=0)
\r
667 if (line.Find(_T("$Date"))>=0)
\r
669 if (line.Find(_T("$LastChangedRevision"))>=0)
\r
671 if (line.Find(_T("$Rev"))>=0)
\r
673 if (line.Find(_T("$LastChangedBy"))>=0)
\r
675 if (line.Find(_T("$Author"))>=0)
\r
677 if (line.Find(_T("$HeadURL"))>=0)
\r
679 if (line.Find(_T("$URL"))>=0)
\r
681 if (line.Find(_T("$Id"))>=0)
\r
686 CString CPatch::CheckPatchPath(const CString& path)
\r
688 //first check if the path already matches
\r
689 if (CountMatches(path) > (GetNumberOfFiles()/3))
\r
691 //now go up the tree and try again
\r
692 CString upperpath = path;
\r
693 while (upperpath.ReverseFind('\\')>0)
\r
695 upperpath = upperpath.Left(upperpath.ReverseFind('\\'));
\r
696 if (CountMatches(upperpath) > (GetNumberOfFiles()/3))
\r
699 //still no match found. So try sub folders
\r
700 bool isDir = false;
\r
702 CDirFileEnum filefinder(path);
\r
703 while (filefinder.NextFile(subpath, &isDir))
\r
707 if (g_GitAdminDir.IsAdminDirPath(subpath))
\r
709 if (CountMatches(subpath) > (GetNumberOfFiles()/3))
\r
713 // if a patch file only contains newly added files
\r
714 // we can't really find the correct path.
\r
715 // But: we can compare paths strings without the filenames
\r
716 // and check if at least those match
\r
718 while (upperpath.ReverseFind('\\')>0)
\r
720 upperpath = upperpath.Left(upperpath.ReverseFind('\\'));
\r
721 if (CountDirMatches(upperpath) > (GetNumberOfFiles()/3))
\r
728 int CPatch::CountMatches(const CString& path)
\r
731 for (int i=0; i<GetNumberOfFiles(); ++i)
\r
733 CString temp = GetFilename(i);
\r
734 temp.Replace('/', '\\');
\r
735 if (PathIsRelative(temp))
\r
736 temp = path + _T("\\")+ temp;
\r
737 if (PathFileExists(temp))
\r
743 int CPatch::CountDirMatches(const CString& path)
\r
746 for (int i=0; i<GetNumberOfFiles(); ++i)
\r
748 CString temp = GetFilename(i);
\r
749 temp.Replace('/', '\\');
\r
750 if (PathIsRelative(temp))
\r
751 temp = path + _T("\\")+ temp;
\r
752 // remove the filename
\r
753 temp = temp.Left(temp.ReverseFind('\\'));
\r
754 if (PathFileExists(temp))
\r
760 BOOL CPatch::StripPrefixes(const CString& path)
\r
762 int nSlashesMax = 0;
\r
763 for (int i=0; i<GetNumberOfFiles(); i++)
\r
765 CString filename = GetFilename(i);
\r
766 filename.Replace('/','\\');
\r
767 int nSlashes = filename.Replace('\\','/');
\r
768 nSlashesMax = max(nSlashesMax,nSlashes);
\r
771 for (int nStrip=1;nStrip<nSlashesMax;nStrip++)
\r
774 if ( CountMatches(path) > GetNumberOfFiles()/3 )
\r
776 // Use current m_nStrip
\r
781 // Stripping doesn't help so reset it again
\r
786 CString CPatch::Strip(const CString& filename)
\r
788 CString s = filename;
\r
791 // Remove windows drive letter "c:"
\r
792 if ( s.GetLength()>2 && s[1]==':')
\r
797 for (int nStrip=1;nStrip<=m_nStrip;nStrip++)
\r
799 // "/home/ts/my-working-copy/dir/file.txt"
\r
800 // "home/ts/my-working-copy/dir/file.txt"
\r
801 // "ts/my-working-copy/dir/file.txt"
\r
802 // "my-working-copy/dir/file.txt"
\r
804 s = s.Mid(s.FindOneOf(_T("/\\"))+1);
\r
810 CString CPatch::RemoveUnicodeBOM(const CString& str)
\r
812 if (str.GetLength()==0)
\r
814 if (str[0] == 0xFEFF)
\r