1 // TortoiseGitMerge - a Diff/Patch program
3 // Copyright (C) 2009-2013, 2015-2016 - TortoiseGit
4 // Copyright (C) 2012-2013 - Sven Strickroth <email@cs-ware.de>
5 // Copyright (C) 2004-2009,2011-2014 - TortoiseSVN
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License
9 // as published by the Free Software Foundation; either version 2
10 // of the License, or (at your option) any later version.
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 // GNU General Public License for more details.
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software Foundation,
19 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 #include "UnicodeUtils.h"
24 #include "DirFileEnum.h"
25 #include "TortoiseMerge.h"
26 #include "GitAdminDir.h"
32 static char THIS_FILE
[] = __FILE__
;
37 , m_UnicodeType(CFileTextLines::AUTOTYPE
)
46 void CPatch::FreeMemory()
48 for (int i
=0; i
<m_arFileDiffs
.GetCount(); ++i
)
50 Chunks
* chunks
= m_arFileDiffs
.GetAt(i
);
51 for (int j
=0; j
<chunks
->chunks
.GetCount(); ++j
)
53 delete chunks
->chunks
.GetAt(j
);
55 chunks
->chunks
.RemoveAll();
58 m_arFileDiffs
.RemoveAll();
61 BOOL
CPatch::ParsePatchFile(CFileTextLines
&PatchLines
)
64 EOL ending
= EOL_NOENDING
;
68 Chunks
* chunks
= nullptr;
69 Chunk
* chunk
= nullptr;
70 int nAddLineCount
= 0;
71 int nRemoveLineCount
= 0;
72 int nContextLineCount
= 0;
73 std::map
<CString
, int> filenamesToPatch
;
74 for ( ; nIndex
< PatchLines
.GetCount(); ++nIndex
)
76 sLine
= PatchLines
.GetAt(nIndex
);
77 ending
= PatchLines
.GetLineEnding(nIndex
);
78 if (ending
!= EOL_NOENDING
)
79 ending
= EOL_AUTOLINE
;
86 if (wcsncmp(sLine
, L
"diff ", 5) == 0)
90 //this is a new file diff, so add the last one to
92 if (chunks
->chunks
.GetCount() > 0)
93 m_arFileDiffs
.Add(chunks
);
97 chunks
= new Chunks();
106 if (wcsncmp(sLine
, L
"index", 5) == 0)
108 int dotstart
=sLine
.Find(_T(".."));
109 if(dotstart
>=0 && chunks
)
111 chunks
->sRevision
= sLine
.Mid(dotstart
-7,7);
112 chunks
->sRevision2
= sLine
.Mid(dotstart
+2,7);
118 if (wcsncmp(sLine
, L
"--- ", 4) == 0)
124 //this is a new file diff, so add the last one to
126 if (chunks
->chunks
.GetCount() > 0)
127 m_arFileDiffs
.Add(chunks
);
131 chunks
= new Chunks();
134 sLine
= sLine
.Mid(3); //remove the "---"
136 //at the end of the filepath there's a revision number...
137 int bracket
= sLine
.ReverseFind('(');
139 // some patch files can have another '(' char, especially ones created in Chinese OS
140 bracket
= sLine
.ReverseFind(0xff08);
144 if (chunks
->sFilePath
.IsEmpty())
145 chunks
->sFilePath
= sLine
.Trim();
148 chunks
->sFilePath
= sLine
.Left(bracket
-1).Trim();
150 if (chunks
->sFilePath
.Find('\t')>=0)
151 chunks
->sFilePath
= chunks
->sFilePath
.Left(chunks
->sFilePath
.Find('\t'));
152 if (wcsncmp(chunks
->sFilePath
, L
"\"", 1) == 0 && chunks
->sFilePath
.ReverseFind(_T('"')) == chunks
->sFilePath
.GetLength() - 1)
153 chunks
->sFilePath
=chunks
->sFilePath
.Mid(1, chunks
->sFilePath
.GetLength() - 2);
154 if (wcsncmp(chunks
->sFilePath
, L
"a/", 2) == 0)
155 chunks
->sFilePath
=chunks
->sFilePath
.Mid(2);
157 if (wcsncmp(chunks
->sFilePath
, L
"b/", 2) == 0)
158 chunks
->sFilePath
=chunks
->sFilePath
.Mid(2);
161 chunks
->sFilePath
.Replace(_T('/'),_T('\\'));
163 if (chunks
->sFilePath
== _T("\\dev\\null") || chunks
->sFilePath
== _T("/dev/null"))
164 chunks
->sFilePath
= _T("NUL");
170 if (wcsncmp(sLine
,L
"@@", 2) == 0)
187 if (wcsncmp(sLine
, L
"+++", 3) != 0)
189 // no starting "+++" found
190 m_sErrorMessage
.Format(IDS_ERR_PATCH_NOADDFILELINE
, nIndex
);
193 sLine
= sLine
.Mid(3); //remove the "---"
196 //at the end of the filepath there's a revision number...
197 int bracket
= sLine
.ReverseFind('(');
199 // some patch files can have another '(' char, especially ones created in Chinese OS
200 bracket
= sLine
.ReverseFind(0xff08);
203 chunks
->sFilePath2
= sLine
.Trim();
205 chunks
->sFilePath2
= sLine
.Left(bracket
-1).Trim();
206 if (chunks
->sFilePath2
.Find('\t')>=0)
207 chunks
->sFilePath2
= chunks
->sFilePath2
.Left(chunks
->sFilePath2
.Find('\t'));
208 if (wcsncmp(chunks
->sFilePath2
, L
"\"", 1) == 0 && chunks
->sFilePath2
.ReverseFind(_T('"')) == chunks
->sFilePath2
.GetLength() - 1)
209 chunks
->sFilePath2
=chunks
->sFilePath2
.Mid(1, chunks
->sFilePath2
.GetLength() - 2);
210 if (wcsncmp(chunks
->sFilePath2
, L
"a/", 2) == 0)
211 chunks
->sFilePath2
=chunks
->sFilePath2
.Mid(2);
213 if (wcsncmp(chunks
->sFilePath2
, L
"b/", 2) == 0)
214 chunks
->sFilePath2
=chunks
->sFilePath2
.Mid(2);
216 chunks
->sFilePath2
.Replace(_T('/'),_T('\\'));
218 if (chunks
->sFilePath2
== _T("\\dev\\null") || chunks
->sFilePath2
== _T("/dev/null"))
219 chunks
->sFilePath2
= _T("NUL");
227 //start of a new chunk
228 if (wcsncmp(sLine
, L
"@@", 2) != 0)
230 //chunk doesn't start with "@@"
231 //so there's garbage in between two file diffs
239 for (int i
= 0; i
< chunks
->chunks
.GetCount(); ++i
)
241 delete chunks
->chunks
.GetAt(i
);
243 chunks
->chunks
.RemoveAll();
248 break; //skip the garbage
251 //@@ -xxx,xxx +xxx,xxx @@
252 sLine
= sLine
.Mid(2);
253 sLine
= sLine
.Trim();
255 CString sRemove
= sLine
.Left(sLine
.Find(' '));
256 CString sAdd
= sLine
.Mid(sLine
.Find(' '));
257 chunk
->lRemoveStart
= (-_ttol(sRemove
));
258 if (sRemove
.Find(',')>=0)
260 sRemove
= sRemove
.Mid(sRemove
.Find(',')+1);
261 chunk
->lRemoveLength
= _ttol(sRemove
);
265 chunk
->lRemoveStart
= 0;
266 chunk
->lRemoveLength
= (-_ttol(sRemove
));
268 chunk
->lAddStart
= _ttol(sAdd
);
269 if (sAdd
.Find(',')>=0)
271 sAdd
= sAdd
.Mid(sAdd
.Find(',')+1);
272 chunk
->lAddLength
= _ttol(sAdd
);
276 chunk
->lAddStart
= 1;
277 chunk
->lAddLength
= _ttol(sAdd
);
283 case 5: //[ |+|-] <sourceline>
285 //this line is either a context line (with a ' ' in front)
286 //a line added (with a '+' in front)
287 //or a removed line (with a '-' in front)
292 type
= sLine
.GetAt(0);
295 //it's a context line - we don't use them here right now
296 //but maybe in the future the patch algorithm can be
297 //extended to use those in case the file to patch has
298 //already changed and no base file is around...
299 chunk
->arLines
.Add(RemoveUnicodeBOM(sLine
.Mid(1)));
300 chunk
->arLinesStates
.Add(PATCHSTATE_CONTEXT
);
301 chunk
->arEOLs
.push_back(ending
);
304 else if (type
== '\\')
306 //it's a context line (sort of):
307 //warnings start with a '\' char (e.g. "\ No newline at end of file")
308 //so just ignore this...
310 else if (type
== '-')
313 chunk
->arLines
.Add(RemoveUnicodeBOM(sLine
.Mid(1)));
314 chunk
->arLinesStates
.Add(PATCHSTATE_REMOVED
);
315 chunk
->arEOLs
.push_back(ending
);
318 else if (type
== '+')
321 chunk
->arLines
.Add(RemoveUnicodeBOM(sLine
.Mid(1)));
322 chunk
->arLinesStates
.Add(PATCHSTATE_ADDED
);
323 chunk
->arEOLs
.push_back(ending
);
328 //none of those lines! what the hell happened here?
329 m_sErrorMessage
.Format(IDS_ERR_PATCH_UNKOWNLINETYPE
, nIndex
);
332 if ((chunk
->lAddLength
== (nAddLineCount
+ nContextLineCount
)) &&
333 chunk
->lRemoveLength
== (nRemoveLineCount
+ nContextLineCount
))
337 chunks
->chunks
.Add(chunk
);
342 nContextLineCount
= 0;
343 nRemoveLineCount
= 0;
351 } // for ( ;nIndex<m_PatchLines.GetCount(); nIndex++)
354 m_sErrorMessage
.LoadString(IDS_ERR_PATCH_CHUNKMISMATCH
);
358 m_arFileDiffs
.Add(chunks
);
360 for (int i
= 0; i
< m_arFileDiffs
.GetCount(); ++i
)
362 if (filenamesToPatch
[m_arFileDiffs
.GetAt(i
)->sFilePath
] > 1 && m_arFileDiffs
.GetAt(i
)->sFilePath
!= _T("NUL"))
364 m_sErrorMessage
.Format(IDS_ERR_PATCH_FILENAMENOTUNIQUE
, (LPCTSTR
)m_arFileDiffs
.GetAt(i
)->sFilePath
);
368 ++filenamesToPatch
[m_arFileDiffs
.GetAt(i
)->sFilePath
];
369 if (m_arFileDiffs
.GetAt(i
)->sFilePath
!= m_arFileDiffs
.GetAt(i
)->sFilePath2
)
371 if (filenamesToPatch
[m_arFileDiffs
.GetAt(i
)->sFilePath2
] > 1 && m_arFileDiffs
.GetAt(i
)->sFilePath2
!= _T("NUL"))
373 m_sErrorMessage
.Format(IDS_ERR_PATCH_FILENAMENOTUNIQUE
, (LPCTSTR
)m_arFileDiffs
.GetAt(i
)->sFilePath
);
377 ++filenamesToPatch
[m_arFileDiffs
.GetAt(i
)->sFilePath2
];
388 for (int i
= 0; i
< chunks
->chunks
.GetCount(); ++i
)
390 delete chunks
->chunks
.GetAt(i
);
392 chunks
->chunks
.RemoveAll();
399 BOOL
CPatch::OpenUnifiedDiffFile(const CString
& filename
)
401 CCrashReport::Instance().AddFile2(filename
, nullptr, _T("unified diff file"), CR_AF_MAKE_FILE_COPY
);
403 CFileTextLines PatchLines
;
404 if (!PatchLines
.Load(filename
))
406 m_sErrorMessage
= PatchLines
.GetErrorString();
411 //now we got all the lines of the patch file
412 //in our array - parsing can start...
413 return ParsePatchFile(PatchLines
);
416 CString
CPatch::GetFilename(int nIndex
)
420 if (nIndex
< m_arFileDiffs
.GetCount())
422 Chunks
* c
= m_arFileDiffs
.GetAt(nIndex
);
423 CString filepath
= Strip(c
->sFilePath
);
429 CString
CPatch::GetRevision(int nIndex
)
433 if (nIndex
< m_arFileDiffs
.GetCount())
435 Chunks
* c
= m_arFileDiffs
.GetAt(nIndex
);
441 CString
CPatch::GetFilename2(int nIndex
)
445 if (nIndex
< m_arFileDiffs
.GetCount())
447 Chunks
* c
= m_arFileDiffs
.GetAt(nIndex
);
448 CString filepath
= Strip(c
->sFilePath2
);
454 CString
CPatch::GetRevision2(int nIndex
)
458 if (nIndex
< m_arFileDiffs
.GetCount())
460 Chunks
* c
= m_arFileDiffs
.GetAt(nIndex
);
461 return c
->sRevision2
;
466 int CPatch::PatchFile(const int strip
, int nIndex
, const CString
& sPatchPath
, const CString
& sSavePath
, const CString
& sBaseFile
, const bool force
)
469 CString sPath
= GetFullPath(sPatchPath
, nIndex
);
470 if (PathIsDirectory(sPath
))
472 m_sErrorMessage
.Format(IDS_ERR_PATCH_INVALIDPATCHFILE
, (LPCTSTR
)sPath
);
477 m_sErrorMessage
.Format(IDS_ERR_PATCH_FILENOTINPATCH
, (LPCTSTR
)sPath
);
481 if (!force
&& sPath
== _T("NUL") && PathFileExists(GetFullPath(sPatchPath
, nIndex
, 1)))
484 if (GetFullPath(sPatchPath
, nIndex
, 1) == _T("NUL") && !PathFileExists(sPath
))
488 CString sPatchFile
= sBaseFile
.IsEmpty() ? sPath
: sBaseFile
;
489 if (PathFileExists(sPatchFile
))
491 CCrashReport::Instance().AddFile2(sPatchFile
, nullptr, _T("File to patch"), CR_AF_MAKE_FILE_COPY
);
493 CFileTextLines PatchLines
;
494 CFileTextLines PatchLinesResult
;
495 PatchLines
.Load(sPatchFile
);
496 PatchLinesResult
= PatchLines
; //.Copy(PatchLines);
497 PatchLines
.CopySettings(&PatchLinesResult
);
499 Chunks
* chunks
= m_arFileDiffs
.GetAt(nIndex
);
501 for (int i
= 0; i
< chunks
->chunks
.GetCount(); ++i
)
503 Chunk
* chunk
= chunks
->chunks
.GetAt(i
);
504 LONG lRemoveLine
= chunk
->lRemoveStart
;
505 LONG lAddLine
= chunk
->lAddStart
;
506 for (int j
= 0; j
< chunk
->arLines
.GetCount(); ++j
)
508 CString sPatchLine
= chunk
->arLines
.GetAt(j
);
509 EOL ending
= chunk
->arEOLs
[j
];
510 if ((m_UnicodeType
!= CFileTextLines::UTF8
)&&(m_UnicodeType
!= CFileTextLines::UTF8BOM
))
512 if ((PatchLines
.GetUnicodeType()==CFileTextLines::UTF8
)||(m_UnicodeType
== CFileTextLines::UTF8BOM
))
514 // convert the UTF-8 contents in CString sPatchLine into a CStringA
515 sPatchLine
= CUnicodeUtils::GetUnicode(CStringA(sPatchLine
));
518 int nPatchState
= (int)chunk
->arLinesStates
.GetAt(j
);
521 case PATCHSTATE_REMOVED
:
523 if ((lAddLine
> PatchLines
.GetCount())||(PatchLines
.GetCount()==0))
525 m_sErrorMessage
.Format(IDS_ERR_PATCH_DOESNOTMATCH
, _T(""), (LPCTSTR
)sPatchLine
);
530 if ((sPatchLine
.Compare(PatchLines
.GetAt(lAddLine
-1))!=0)&&(!HasExpandedKeyWords(sPatchLine
)))
532 m_sErrorMessage
.Format(IDS_ERR_PATCH_DOESNOTMATCH
, (LPCTSTR
)sPatchLine
, (LPCTSTR
)PatchLines
.GetAt(lAddLine
-1));
535 if (lAddLine
> PatchLines
.GetCount())
537 m_sErrorMessage
.Format(IDS_ERR_PATCH_DOESNOTMATCH
, (LPCTSTR
)sPatchLine
, _T(""));
540 PatchLines
.RemoveAt(lAddLine
-1);
543 case PATCHSTATE_ADDED
:
547 // check context after insertions in order to avoid double insertions
548 bool insertOk
= !(lAddLine
< PatchLines
.GetCount());
550 for (; k
< chunk
->arLines
.GetCount(); ++k
)
552 if ((int)chunk
->arLinesStates
.GetAt(k
) == PATCHSTATE_ADDED
)
554 if (PatchLines
.GetCount() >= lAddLine
&& chunk
->arLines
.GetAt(k
).Compare(PatchLines
.GetAt(lAddLine
- 1)) == 0)
562 PatchLines
.InsertAt(lAddLine
-1, sPatchLine
, ending
);
567 if (k
>= chunk
->arLines
.GetCount())
569 m_sErrorMessage
.Format(IDS_ERR_PATCH_DOESNOTMATCH
, (LPCTSTR
)PatchLines
.GetAt(lAddLine
- 1), (LPCTSTR
)chunk
->arLines
.GetAt(k
));
574 case PATCHSTATE_CONTEXT
:
576 if (lAddLine
> PatchLines
.GetCount())
578 m_sErrorMessage
.Format(IDS_ERR_PATCH_DOESNOTMATCH
, _T(""), (LPCTSTR
)sPatchLine
);
583 if (lRemoveLine
== 0)
585 if ((sPatchLine
.Compare(PatchLines
.GetAt(lAddLine
-1))!=0) &&
586 (!HasExpandedKeyWords(sPatchLine
)) &&
587 (lRemoveLine
<= PatchLines
.GetCount()) &&
588 (sPatchLine
.Compare(PatchLines
.GetAt(lRemoveLine
-1))!=0))
590 if ((lAddLine
< PatchLines
.GetCount())&&(sPatchLine
.Compare(PatchLines
.GetAt(lAddLine
))==0))
592 else if (((lAddLine
+ 1) < PatchLines
.GetCount())&&(sPatchLine
.Compare(PatchLines
.GetAt(lAddLine
+1))==0))
594 else if ((lRemoveLine
< PatchLines
.GetCount())&&(sPatchLine
.Compare(PatchLines
.GetAt(lRemoveLine
))==0))
598 m_sErrorMessage
.Format(IDS_ERR_PATCH_DOESNOTMATCH
, (LPCTSTR
)sPatchLine
, (LPCTSTR
)PatchLines
.GetAt(lAddLine
-1));
609 } // switch (nPatchState)
610 } // for (j=0; j<chunk->arLines.GetCount(); j++)
611 } // for (int i=0; i<chunks->chunks.GetCount(); i++)
612 if (!sSavePath
.IsEmpty())
614 PatchLines
.Save(sSavePath
, false);
619 BOOL
CPatch::HasExpandedKeyWords(const CString
& line
) const
621 if (line
.Find(_T("$LastChangedDate"))>=0)
623 if (line
.Find(_T("$Date"))>=0)
625 if (line
.Find(_T("$LastChangedRevision"))>=0)
627 if (line
.Find(_T("$Rev"))>=0)
629 if (line
.Find(_T("$LastChangedBy"))>=0)
631 if (line
.Find(_T("$Author"))>=0)
633 if (line
.Find(_T("$HeadURL"))>=0)
635 if (line
.Find(_T("$URL"))>=0)
637 if (line
.Find(_T("$Id"))>=0)
642 CString
CPatch::CheckPatchPath(const CString
& path
)
644 //first check if the path already matches
645 if (CountMatches(path
) > (GetNumberOfFiles()/3))
647 //now go up the tree and try again
648 CString upperpath
= path
;
649 while (upperpath
.ReverseFind('\\')>0)
651 upperpath
= upperpath
.Left(upperpath
.ReverseFind('\\'));
652 if (CountMatches(upperpath
) > (GetNumberOfFiles()/3))
655 //still no match found. So try sub folders
658 CDirFileEnum
filefinder(path
);
659 while (filefinder
.NextFile(subpath
, &isDir
))
663 if (GitAdminDir::IsAdminDirPath(subpath
))
665 if (CountMatches(subpath
) > (GetNumberOfFiles()/3))
669 // if a patch file only contains newly added files
670 // we can't really find the correct path.
671 // But: we can compare paths strings without the filenames
672 // and check if at least those match
674 while (upperpath
.ReverseFind('\\')>0)
676 upperpath
= upperpath
.Left(upperpath
.ReverseFind('\\'));
677 if (CountDirMatches(upperpath
) > (GetNumberOfFiles()/3))
684 int CPatch::CountMatches(const CString
& path
)
687 for (int i
=0; i
<GetNumberOfFiles(); ++i
)
689 CString temp
= GetFilename(i
);
690 temp
.Replace('/', '\\');
691 if (PathIsRelative(temp
))
692 temp
= path
+ _T("\\")+ temp
;
693 if (PathFileExists(temp
))
699 int CPatch::CountDirMatches(const CString
& path
)
702 for (int i
=0; i
<GetNumberOfFiles(); ++i
)
704 CString temp
= GetFilename(i
);
705 temp
.Replace('/', '\\');
706 if (PathIsRelative(temp
))
707 temp
= path
+ _T("\\")+ temp
;
708 // remove the filename
709 temp
= temp
.Left(temp
.ReverseFind('\\'));
710 if (PathFileExists(temp
))
716 BOOL
CPatch::StripPrefixes(const CString
& path
)
719 for (int i
= 0; i
< GetNumberOfFiles(); ++i
)
721 CString filename
= GetFilename(i
);
722 filename
.Replace('/','\\');
723 int nSlashes
= filename
.Replace('\\','/');
724 nSlashesMax
= max(nSlashesMax
,nSlashes
);
727 for (int nStrip
= 1; nStrip
< nSlashesMax
; ++nStrip
)
730 if ( CountMatches(path
) > GetNumberOfFiles()/3 )
732 // Use current m_nStrip
737 // Stripping doesn't help so reset it again
742 CString
CPatch::Strip(const CString
& filename
) const
744 CString s
= filename
;
747 // Remove windows drive letter "c:"
748 if ( s
.GetLength()>2 && s
[1]==':')
753 for (int nStrip
= 1; nStrip
<= m_nStrip
; ++nStrip
)
755 // "/home/ts/my-working-copy/dir/file.txt"
756 // "home/ts/my-working-copy/dir/file.txt"
757 // "ts/my-working-copy/dir/file.txt"
758 // "my-working-copy/dir/file.txt"
760 s
= s
.Mid(s
.FindOneOf(_T("/\\"))+1);
766 CString
CPatch::GetFullPath(const CString
& sPath
, int nIndex
, int fileno
/* = 0*/)
770 temp
= GetFilename(nIndex
);
772 temp
= GetFilename2(nIndex
);
774 temp
.Replace('/', '\\');
775 if(temp
== _T("NUL"))
778 if (PathIsRelative(temp
))
780 if (sPath
.Right(1).Compare(_T("\\")) != 0)
781 temp
= sPath
+ _T("\\") + temp
;
789 CString
CPatch::RemoveUnicodeBOM(const CString
& str
) const
793 if (str
[0] == 0xFEFF)