1 // TortoiseMerge - a Diff/Patch program
\r
3 // Copyright (C) 2004-2008 - 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
24 //#include "svn_wc.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
39 CPatch::~CPatch(void)
\r
44 void CPatch::FreeMemory()
\r
46 for (int i=0; i<m_arFileDiffs.GetCount(); i++)
\r
48 Chunks * chunks = m_arFileDiffs.GetAt(i);
\r
49 for (int j=0; j<chunks->chunks.GetCount(); j++)
\r
51 delete chunks->chunks.GetAt(j);
\r
53 chunks->chunks.RemoveAll();
\r
56 m_arFileDiffs.RemoveAll();
\r
59 BOOL CPatch::OpenUnifiedDiffFile(const CString& filename)
\r
62 EOL ending = EOL_NOENDING;
\r
64 INT_PTR nLineCount = 0;
\r
65 g_crasher.AddFile((LPCSTR)(LPCTSTR)filename, (LPCSTR)(LPCTSTR)_T("unified diff file"));
\r
67 CFileTextLines PatchLines;
\r
68 if (!PatchLines.Load(filename))
\r
70 m_sErrorMessage = PatchLines.GetErrorString();
\r
73 m_UnicodeType = PatchLines.GetUnicodeType();
\r
75 nLineCount = PatchLines.GetCount();
\r
76 //now we got all the lines of the patch file
\r
77 //in our array - parsing can start...
\r
79 //first, skip possible garbage at the beginning
\r
80 //garbage is finished when a line starts with "Index: "
\r
81 //and the next line consists of only "=" characters
\r
82 for (; nIndex<PatchLines.GetCount(); nIndex++)
\r
84 sLine = PatchLines.GetAt(nIndex);
\r
85 if (sLine.Left(4).Compare(_T("--- "))==0)
\r
87 if ((nIndex+1)<PatchLines.GetCount())
\r
89 sLine = PatchLines.GetAt(nIndex+1);
\r
90 sLine.Replace(_T("="), _T(""));
\r
91 if (sLine.IsEmpty())
\r
95 if ((PatchLines.GetCount()-nIndex) < 2)
\r
97 //no file entry found.
\r
98 m_sErrorMessage.LoadString(IDS_ERR_PATCH_NOINDEX);
\r
102 //from this point on we have the real unified diff data
\r
104 Chunks * chunks = NULL;
\r
105 Chunk * chunk = NULL;
\r
106 int nAddLineCount = 0;
\r
107 int nRemoveLineCount = 0;
\r
108 int nContextLineCount = 0;
\r
109 for ( ;nIndex<PatchLines.GetCount(); nIndex++)
\r
111 sLine = PatchLines.GetAt(nIndex);
\r
112 ending = PatchLines.GetLineEnding(nIndex);
\r
113 if (ending != EOL_NOENDING)
\r
114 ending = EOL_AUTOLINE;
\r
117 if ((sLine.Left(4).Compare(_T("--- "))==0)&&(sLine.Find('\t') >= 0))
\r
122 //this is a new file diff, so add the last one to
\r
124 m_arFileDiffs.Add(chunks);
\r
126 chunks = new Chunks();
\r
127 int nTab = sLine.Find('\t');
\r
130 chunks->sFilePath = sLine.Mid(4, nTab-4).Trim();
\r
136 case 0: //Index: <filepath>
\r
139 if ((nIndex+1)<PatchLines.GetCount())
\r
141 nextLine = PatchLines.GetAt(nIndex+1);
\r
142 if (!nextLine.IsEmpty())
\r
144 nextLine.Replace(_T("="), _T(""));
\r
145 if (nextLine.IsEmpty())
\r
149 //this is a new file diff, so add the last one to
\r
151 m_arFileDiffs.Add(chunks);
\r
153 chunks = new Chunks();
\r
154 int nColon = sLine.Find(':');
\r
157 chunks->sFilePath = sLine.Mid(nColon+1).Trim();
\r
158 if (chunks->sFilePath.Find('\t')>=0)
\r
159 chunks->sFilePath.Left(chunks->sFilePath.Find('\t')).TrimRight();
\r
160 if (chunks->sFilePath.Right(9).Compare(_T("(deleted)"))==0)
\r
161 chunks->sFilePath.Left(chunks->sFilePath.GetLength()-9).TrimRight();
\r
162 if (chunks->sFilePath.Right(7).Compare(_T("(added)"))==0)
\r
163 chunks->sFilePath.Left(chunks->sFilePath.GetLength()-7).TrimRight();
\r
175 if (chunks == NULL)
\r
178 //Index: <filepath>
\r
179 //was not found at the start of a file diff!
\r
186 case 1: //====================
\r
188 sLine.Replace(_T("="), _T(""));
\r
189 if (sLine.IsEmpty())
\r
191 // if the next line is already the start of the chunk,
\r
192 // then the patch/diff file was not created by svn. But we
\r
193 // still try to use it
\r
194 if (PatchLines.GetCount() > (nIndex + 1))
\r
197 if (PatchLines.GetAt(nIndex+1).Left(2).Compare(_T("@@"))==0)
\r
207 //=========================
\r
209 m_sErrorMessage.Format(IDS_ERR_PATCH_NOEQUATIONCHARLINE, nIndex);
\r
214 case 2: //--- <filepath>
\r
216 if (sLine.Left(3).Compare(_T("---"))!=0)
\r
218 //no starting "---" found
\r
219 //seems to be either garbage or just
\r
220 //a binary file. So start over...
\r
230 sLine = sLine.Mid(3); //remove the "---"
\r
231 sLine =sLine.Trim();
\r
232 //at the end of the filepath there's a revision number...
\r
233 int bracket = sLine.ReverseFind('(');
\r
235 // some patch files can have another '(' char, especially ones created in Chinese OS
\r
236 bracket = sLine.ReverseFind(0xff08);
\r
237 CString num = sLine.Mid(bracket); //num = "(revision xxxxx)"
\r
238 num = num.Mid(num.Find(' '));
\r
239 num = num.Trim(_T(" )"));
\r
240 // here again, check for the Chinese bracket
\r
241 num = num.Trim(0xff09);
\r
242 chunks->sRevision = num;
\r
245 if (chunks->sFilePath.IsEmpty())
\r
246 chunks->sFilePath = sLine.Trim();
\r
249 chunks->sFilePath = sLine.Left(bracket-1).Trim();
\r
250 if (chunks->sFilePath.Find('\t')>=0)
\r
252 chunks->sFilePath = chunks->sFilePath.Left(chunks->sFilePath.Find('\t'));
\r
257 case 3: //+++ <filepath>
\r
259 if (sLine.Left(3).Compare(_T("+++"))!=0)
\r
261 //no starting "+++" found
\r
262 m_sErrorMessage.Format(IDS_ERR_PATCH_NOADDFILELINE, nIndex);
\r
265 sLine = sLine.Mid(3); //remove the "---"
\r
266 sLine =sLine.Trim();
\r
267 //at the end of the filepath there's a revision number...
\r
268 int bracket = sLine.ReverseFind('(');
\r
270 // some patch files can have another '(' char, especially ones created in Chinese OS
\r
271 bracket = sLine.ReverseFind(0xff08);
\r
272 CString num = sLine.Mid(bracket); //num = "(revision xxxxx)"
\r
273 num = num.Mid(num.Find(' '));
\r
274 num = num.Trim(_T(" )"));
\r
275 // here again, check for the Chinese bracket
\r
276 num = num.Trim(0xff09);
\r
277 chunks->sRevision2 = num;
\r
279 chunks->sFilePath2 = sLine.Trim();
\r
281 chunks->sFilePath2 = sLine.Left(bracket-1).Trim();
\r
282 if (chunks->sFilePath2.Find('\t')>=0)
\r
284 chunks->sFilePath2 = chunks->sFilePath2.Left(chunks->sFilePath2.Find('\t'));
\r
289 case 4: //@@ -xxx,xxx +xxx,xxx @@
\r
291 //start of a new chunk
\r
292 if (sLine.Left(2).Compare(_T("@@"))!=0)
\r
294 //chunk doesn't start with "@@"
\r
295 //so there's garbage in between two file diffs
\r
303 for (int i=0; i<chunks->chunks.GetCount(); i++)
\r
305 delete chunks->chunks.GetAt(i);
\r
307 chunks->chunks.RemoveAll();
\r
312 break; //skip the garbage
\r
314 sLine = sLine.Mid(2);
\r
315 sLine = sLine.Trim();
\r
316 chunk = new Chunk();
\r
317 CString sRemove = sLine.Left(sLine.Find(' '));
\r
318 CString sAdd = sLine.Mid(sLine.Find(' '));
\r
319 chunk->lRemoveStart = (-_ttol(sRemove));
\r
320 if (sRemove.Find(',')>=0)
\r
322 sRemove = sRemove.Mid(sRemove.Find(',')+1);
\r
323 chunk->lRemoveLength = _ttol(sRemove);
\r
327 chunk->lRemoveStart = 0;
\r
328 chunk->lRemoveLength = (-_ttol(sRemove));
\r
330 chunk->lAddStart = _ttol(sAdd);
\r
331 if (sAdd.Find(',')>=0)
\r
333 sAdd = sAdd.Mid(sAdd.Find(',')+1);
\r
334 chunk->lAddLength = _ttol(sAdd);
\r
338 chunk->lAddStart = 1;
\r
339 chunk->lAddLength = _ttol(sAdd);
\r
344 case 5: //[ |+|-] <sourceline>
\r
346 //this line is either a context line (with a ' ' in front)
\r
347 //a line added (with a '+' in front)
\r
348 //or a removed line (with a '-' in front)
\r
350 if (sLine.IsEmpty())
\r
353 type = sLine.GetAt(0);
\r
356 //it's a context line - we don't use them here right now
\r
357 //but maybe in the future the patch algorithm can be
\r
358 //extended to use those in case the file to patch has
\r
359 //already changed and no base file is around...
\r
360 chunk->arLines.Add(RemoveUnicodeBOM(sLine.Mid(1)));
\r
361 chunk->arLinesStates.Add(PATCHSTATE_CONTEXT);
\r
362 chunk->arEOLs.push_back(ending);
\r
363 nContextLineCount++;
\r
365 else if (type == '\\')
\r
367 //it's a context line (sort of):
\r
368 //warnings start with a '\' char (e.g. "\ No newline at end of file")
\r
369 //so just ignore this...
\r
371 else if (type == '-')
\r
374 chunk->arLines.Add(RemoveUnicodeBOM(sLine.Mid(1)));
\r
375 chunk->arLinesStates.Add(PATCHSTATE_REMOVED);
\r
376 chunk->arEOLs.push_back(ending);
\r
377 nRemoveLineCount++;
\r
379 else if (type == '+')
\r
382 chunk->arLines.Add(RemoveUnicodeBOM(sLine.Mid(1)));
\r
383 chunk->arLinesStates.Add(PATCHSTATE_ADDED);
\r
384 chunk->arEOLs.push_back(ending);
\r
389 //none of those lines! what the hell happened here?
\r
390 m_sErrorMessage.Format(IDS_ERR_PATCH_UNKOWNLINETYPE, nIndex);
\r
393 if ((chunk->lAddLength == (nAddLineCount + nContextLineCount)) &&
\r
394 chunk->lRemoveLength == (nRemoveLineCount + nContextLineCount))
\r
396 //chunk is finished
\r
398 chunks->chunks.Add(chunk);
\r
403 nContextLineCount = 0;
\r
404 nRemoveLineCount = 0;
\r
411 } // switch (state)
\r
412 } // for ( ;nIndex<m_PatchLines.GetCount(); nIndex++)
\r
415 m_sErrorMessage.LoadString(IDS_ERR_PATCH_CHUNKMISMATCH);
\r
419 m_arFileDiffs.Add(chunks);
\r
426 for (int i=0; i<chunks->chunks.GetCount(); i++)
\r
428 delete chunks->chunks.GetAt(i);
\r
430 chunks->chunks.RemoveAll();
\r
437 CString CPatch::GetFilename(int nIndex)
\r
441 if (nIndex < m_arFileDiffs.GetCount())
\r
443 Chunks * c = m_arFileDiffs.GetAt(nIndex);
\r
444 CString filepath = Strip(c->sFilePath);
\r
450 CString CPatch::GetRevision(int nIndex)
\r
454 if (nIndex < m_arFileDiffs.GetCount())
\r
456 Chunks * c = m_arFileDiffs.GetAt(nIndex);
\r
457 return c->sRevision;
\r
462 CString CPatch::GetFilename2(int nIndex)
\r
466 if (nIndex < m_arFileDiffs.GetCount())
\r
468 Chunks * c = m_arFileDiffs.GetAt(nIndex);
\r
469 CString filepath = Strip(c->sFilePath2);
\r
475 CString CPatch::GetRevision2(int nIndex)
\r
479 if (nIndex < m_arFileDiffs.GetCount())
\r
481 Chunks * c = m_arFileDiffs.GetAt(nIndex);
\r
482 return c->sRevision2;
\r
487 BOOL CPatch::PatchFile(const CString& sPath, const CString& sSavePath, const CString& sBaseFile)
\r
489 if (PathIsDirectory(sPath))
\r
491 m_sErrorMessage.Format(IDS_ERR_PATCH_INVALIDPATCHFILE, (LPCTSTR)sPath);
\r
494 // find the entry in the patch file which matches the full path given in sPath.
\r
496 // use the longest path that matches
\r
498 for (int i=0; i<GetNumberOfFiles(); i++)
\r
500 CString temppath = sPath;
\r
501 CString temp = GetFilename(i);
\r
502 temppath.Replace('/', '\\');
\r
503 temp.Replace('/', '\\');
\r
504 if (temppath.Mid(temppath.GetLength()-temp.GetLength()-1, 1).CompareNoCase(_T("\\"))==0)
\r
506 temppath = temppath.Right(temp.GetLength());
\r
507 if ((temp.CompareNoCase(temppath)==0))
\r
509 if (nMaxMatch < temp.GetLength())
\r
511 nMaxMatch = temp.GetLength();
\r
516 else if (temppath.CompareNoCase(temp)==0)
\r
518 if ((nIndex < 0)&&(! temp.IsEmpty()))
\r
526 m_sErrorMessage.Format(IDS_ERR_PATCH_FILENOTINPATCH, (LPCTSTR)sPath);
\r
531 CString sPatchFile = sBaseFile.IsEmpty() ? sPath : sBaseFile;
\r
532 if (PathFileExists(sPatchFile))
\r
534 g_crasher.AddFile((LPCSTR)(LPCTSTR)sPatchFile, (LPCSTR)(LPCTSTR)_T("File to patch"));
\r
536 CFileTextLines PatchLines;
\r
537 CFileTextLines PatchLinesResult;
\r
538 PatchLines.Load(sPatchFile);
\r
539 PatchLinesResult = PatchLines; //.Copy(PatchLines);
\r
540 PatchLines.CopySettings(&PatchLinesResult);
\r
542 Chunks * chunks = m_arFileDiffs.GetAt(nIndex);
\r
544 for (int i=0; i<chunks->chunks.GetCount(); i++)
\r
546 Chunk * chunk = chunks->chunks.GetAt(i);
\r
547 LONG lRemoveLine = chunk->lRemoveStart;
\r
548 LONG lAddLine = chunk->lAddStart;
\r
549 for (int j=0; j<chunk->arLines.GetCount(); j++)
\r
551 CString sPatchLine = chunk->arLines.GetAt(j);
\r
552 EOL ending = chunk->arEOLs[j];
\r
553 if ((m_UnicodeType != CFileTextLines::UTF8)&&(m_UnicodeType != CFileTextLines::UTF8BOM))
\r
555 if ((PatchLines.GetUnicodeType()==CFileTextLines::UTF8)||(m_UnicodeType == CFileTextLines::UTF8BOM))
\r
557 // convert the UTF-8 contents in CString sPatchLine into a CStringA
\r
558 sPatchLine = CUnicodeUtils::GetUnicode(CStringA(sPatchLine));
\r
561 int nPatchState = (int)chunk->arLinesStates.GetAt(j);
\r
562 switch (nPatchState)
\r
564 case PATCHSTATE_REMOVED:
\r
566 if ((lAddLine > PatchLines.GetCount())||(PatchLines.GetCount()==0))
\r
568 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, _T(""), (LPCTSTR)sPatchLine);
\r
573 if ((sPatchLine.Compare(PatchLines.GetAt(lAddLine-1))!=0)&&(!HasExpandedKeyWords(sPatchLine)))
\r
575 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, (LPCTSTR)sPatchLine, (LPCTSTR)PatchLines.GetAt(lAddLine-1));
\r
578 if (lAddLine > PatchLines.GetCount())
\r
580 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, (LPCTSTR)sPatchLine, _T(""));
\r
583 PatchLines.RemoveAt(lAddLine-1);
\r
586 case PATCHSTATE_ADDED:
\r
590 PatchLines.InsertAt(lAddLine-1, sPatchLine, ending);
\r
594 case PATCHSTATE_CONTEXT:
\r
596 if (lAddLine > PatchLines.GetCount())
\r
598 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, _T(""), (LPCTSTR)sPatchLine);
\r
603 if (lRemoveLine == 0)
\r
605 if ((sPatchLine.Compare(PatchLines.GetAt(lAddLine-1))!=0) &&
\r
606 (!HasExpandedKeyWords(sPatchLine)) &&
\r
607 (lRemoveLine <= PatchLines.GetCount()) &&
\r
608 (sPatchLine.Compare(PatchLines.GetAt(lRemoveLine-1))!=0))
\r
610 if ((lAddLine < PatchLines.GetCount())&&(sPatchLine.Compare(PatchLines.GetAt(lAddLine))==0))
\r
612 else if ((lRemoveLine < PatchLines.GetCount())&&(sPatchLine.Compare(PatchLines.GetAt(lRemoveLine))==0))
\r
616 m_sErrorMessage.Format(IDS_ERR_PATCH_DOESNOTMATCH, (LPCTSTR)sPatchLine, (LPCTSTR)PatchLines.GetAt(lAddLine-1));
\r
627 } // switch (nPatchState)
\r
628 } // for (j=0; j<chunk->arLines.GetCount(); j++)
\r
629 } // for (int i=0; i<chunks->chunks.GetCount(); i++)
\r
630 if (!sSavePath.IsEmpty())
\r
632 PatchLines.Save(sSavePath, false);
\r
637 BOOL CPatch::HasExpandedKeyWords(const CString& line)
\r
639 if (line.Find(_T("$LastChangedDate"))>=0)
\r
641 if (line.Find(_T("$Date"))>=0)
\r
643 if (line.Find(_T("$LastChangedRevision"))>=0)
\r
645 if (line.Find(_T("$Rev"))>=0)
\r
647 if (line.Find(_T("$LastChangedBy"))>=0)
\r
649 if (line.Find(_T("$Author"))>=0)
\r
651 if (line.Find(_T("$HeadURL"))>=0)
\r
653 if (line.Find(_T("$URL"))>=0)
\r
655 if (line.Find(_T("$Id"))>=0)
\r
660 CString CPatch::CheckPatchPath(const CString& path)
\r
662 //first check if the path already matches
\r
663 if (CountMatches(path) > (GetNumberOfFiles()/3))
\r
665 //now go up the tree and try again
\r
666 CString upperpath = path;
\r
667 while (upperpath.ReverseFind('\\')>0)
\r
669 upperpath = upperpath.Left(upperpath.ReverseFind('\\'));
\r
670 if (CountMatches(upperpath) > (GetNumberOfFiles()/3))
\r
673 //still no match found. So try sub folders
\r
674 bool isDir = false;
\r
676 CDirFileEnum filefinder(path);
\r
677 while (filefinder.NextFile(subpath, &isDir))
\r
681 if (g_SVNAdminDir.IsAdminDirPath(subpath))
\r
683 if (CountMatches(subpath) > (GetNumberOfFiles()/3))
\r
687 // if a patch file only contains newly added files
\r
688 // we can't really find the correct path.
\r
689 // But: we can compare paths strings without the filenames
\r
690 // and check if at least those match
\r
692 while (upperpath.ReverseFind('\\')>0)
\r
694 upperpath = upperpath.Left(upperpath.ReverseFind('\\'));
\r
695 if (CountDirMatches(upperpath) > (GetNumberOfFiles()/3))
\r
702 int CPatch::CountMatches(const CString& path)
\r
705 for (int i=0; i<GetNumberOfFiles(); ++i)
\r
707 CString temp = GetFilename(i);
\r
708 temp.Replace('/', '\\');
\r
709 if (PathIsRelative(temp))
\r
710 temp = path + _T("\\")+ temp;
\r
711 if (PathFileExists(temp))
\r
717 int CPatch::CountDirMatches(const CString& path)
\r
720 for (int i=0; i<GetNumberOfFiles(); ++i)
\r
722 CString temp = GetFilename(i);
\r
723 temp.Replace('/', '\\');
\r
724 if (PathIsRelative(temp))
\r
725 temp = path + _T("\\")+ temp;
\r
726 // remove the filename
\r
727 temp = temp.Left(temp.ReverseFind('\\'));
\r
728 if (PathFileExists(temp))
\r
734 BOOL CPatch::StripPrefixes(const CString& path)
\r
736 int nSlashesMax = 0;
\r
737 for (int i=0; i<GetNumberOfFiles(); i++)
\r
739 CString filename = GetFilename(i);
\r
740 filename.Replace('/','\\');
\r
741 int nSlashes = filename.Replace('\\','/');
\r
742 nSlashesMax = max(nSlashesMax,nSlashes);
\r
745 for (int nStrip=1;nStrip<nSlashesMax;nStrip++)
\r
748 if ( CountMatches(path) > GetNumberOfFiles()/3 )
\r
750 // Use current m_nStrip
\r
755 // Stripping doesn't help so reset it again
\r
760 CString CPatch::Strip(const CString& filename)
\r
762 CString s = filename;
\r
765 // Remove windows drive letter "c:"
\r
766 if ( s.GetLength()>2 && s[1]==':')
\r
771 for (int nStrip=1;nStrip<=m_nStrip;nStrip++)
\r
773 // "/home/ts/my-working-copy/dir/file.txt"
\r
774 // "home/ts/my-working-copy/dir/file.txt"
\r
775 // "ts/my-working-copy/dir/file.txt"
\r
776 // "my-working-copy/dir/file.txt"
\r
778 s = s.Mid(s.FindOneOf(_T("/\\"))+1);
\r
784 CString CPatch::RemoveUnicodeBOM(const CString& str)
\r
786 if (str.GetLength()==0)
\r
788 if (str[0] == 0xFEFF)
\r