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