1
// TortoiseGitMerge - a Diff/Patch program
3 // Copyright (C) 2009-2013, 2015-2023 - 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"
28 #include "StringUtils.h"
33 static char THIS_FILE
[] = __FILE__
;
44 void CPatch::FreeMemory()
46 m_arFileDiffs
.clear();
49 BOOL
CPatch::ParsePatchFile(CFileTextLines
&PatchLines
)
52 EOL ending
= EOL::NoEnding
;
56 std::unique_ptr
<Chunks
> chunks
;
57 std::unique_ptr
<Chunk
> chunk
;
58 int nAddLineCount
= 0;
59 int nRemoveLineCount
= 0;
60 int nContextLineCount
= 0;
61 std::map
<CString
, int> filenamesToPatch
;
62 for ( ; nIndex
< PatchLines
.GetCount(); ++nIndex
)
64 sLine
= PatchLines
.GetAt(nIndex
);
65 ending
= PatchLines
.GetLineEnding(nIndex
);
66 if (ending
!= EOL::NoEnding
)
67 ending
= EOL::AutoLine
;
74 if (CStringUtils::StartsWith(sLine
, L
"diff "))
78 //this is a new file diff, so add the last one to
80 if (!chunks
->chunks
.empty())
81 m_arFileDiffs
.emplace_back(std::move(chunks
));
83 chunks
= std::make_unique
<Chunks
>();
92 if (CStringUtils::StartsWith(sLine
, L
"index "))
94 int dotstart
= sLine
.Find(L
"..", static_cast<int>(wcslen(L
"index ")));
95 if (dotstart
> 0 && chunks
)
97 chunks
->sRevision
= sLine
.Mid(static_cast<int>(wcslen(L
"index ")), dotstart
- static_cast<int>(wcslen(L
"index ")));
98 int end
= sLine
.Find(L
' ', dotstart
+ 2);
100 chunks
->sRevision2
= sLine
.Mid(dotstart
+ 2, end
- (dotstart
+ 2));
106 if (CStringUtils::StartsWith(sLine
, L
"--- "))
112 //this is a new file diff, so add the last one to
114 if (!chunks
->chunks
.empty())
115 m_arFileDiffs
.emplace_back(std::move(chunks
));
117 chunks
= std::make_unique
<Chunks
>();
120 sLine
= sLine
.Mid(static_cast<int>(wcslen(L
"---"))); //remove the "---"
122 //at the end of the filepath there's a revision number...
123 int bracket
= sLine
.ReverseFind('(');
125 // some patch files can have another '(' char, especially ones created in Chinese OS
126 bracket
= sLine
.ReverseFind(0xff08);
130 if (chunks
->sFilePath
.IsEmpty())
131 chunks
->sFilePath
= sLine
.Trim();
134 chunks
->sFilePath
= sLine
.Left(bracket
-1).Trim();
136 if (chunks
->sFilePath
.Find('\t')>=0)
137 chunks
->sFilePath
= chunks
->sFilePath
.Left(chunks
->sFilePath
.Find('\t'));
138 if (CStringUtils::StartsWith(chunks
->sFilePath
, L
"\"") && CStringUtils::EndsWith(chunks
->sFilePath
, L
'"'))
139 chunks
->sFilePath
= CStringUtils::UnescapeGitQuotePath(chunks
->sFilePath
.Mid(1, chunks
->sFilePath
.GetLength() - 1));
140 if (CStringUtils::StartsWith(chunks
->sFilePath
, L
"a/"))
141 chunks
->sFilePath
=chunks
->sFilePath
.Mid(static_cast<int>(wcslen(L
"a/")));
143 if (CStringUtils::StartsWith(chunks
->sFilePath
, L
"b/"))
144 chunks
->sFilePath
=chunks
->sFilePath
.Mid(static_cast<int>(wcslen(L
"b/")));
147 chunks
->sFilePath
.Replace(L
'/', L
'\\');
149 if (chunks
->sFilePath
== L
"\\dev\\null" || chunks
->sFilePath
== L
"/dev/null")
150 chunks
->sFilePath
= L
"NUL";
156 if (CStringUtils::StartsWith(sLine
, L
"@@"))
173 if (!CStringUtils::StartsWith(sLine
, L
"+++"))
175 // no starting "+++" found
176 m_sErrorMessage
.Format(IDS_ERR_PATCH_NOADDFILELINE
, nIndex
);
179 sLine
= sLine
.Mid(static_cast<int>(wcslen(L
"---"))); //remove the "---"
182 //at the end of the filepath there's a revision number...
183 int bracket
= sLine
.ReverseFind('(');
185 // some patch files can have another '(' char, especially ones created in Chinese OS
186 bracket
= sLine
.ReverseFind(0xff08);
189 chunks
->sFilePath2
= sLine
.Trim();
191 chunks
->sFilePath2
= sLine
.Left(bracket
-1).Trim();
192 if (chunks
->sFilePath2
.Find('\t')>=0)
193 chunks
->sFilePath2
= chunks
->sFilePath2
.Left(chunks
->sFilePath2
.Find('\t'));
194 if (CStringUtils::StartsWith(chunks
->sFilePath2
, L
"\"") && chunks
->sFilePath2
.ReverseFind(L
'"') == chunks
->sFilePath2
.GetLength() - 1)
195 chunks
->sFilePath2
= CStringUtils::UnescapeGitQuotePath(chunks
->sFilePath2
.Mid(1, chunks
->sFilePath2
.GetLength() - 1));
196 if (CStringUtils::StartsWith(chunks
->sFilePath2
, L
"a/"))
197 chunks
->sFilePath2
=chunks
->sFilePath2
.Mid(static_cast<int>(wcslen(L
"a/")));
199 if (CStringUtils::StartsWith(chunks
->sFilePath2
, L
"b/"))
200 chunks
->sFilePath2
=chunks
->sFilePath2
.Mid(static_cast<int>(wcslen(L
"b/")));
202 chunks
->sFilePath2
.Replace(L
'/', L
'\\');
203 chunks
->sFilePath2
.Replace(L
'/', L
'\\');
205 if (chunks
->sFilePath2
== L
"\\dev\\null" || chunks
->sFilePath2
== L
"/dev/null")
206 chunks
->sFilePath2
= L
"NUL";
214 //start of a new chunk
215 if (!CStringUtils::StartsWith(sLine
, L
"@@"))
217 //chunk doesn't start with "@@"
218 //so there's garbage in between two file diffs
222 break; //skip the garbage
225 //@@ -xxx,xxx +xxx,xxx @@
226 sLine
= sLine
.Mid(static_cast<int>(wcslen(L
"@@")));
227 sLine
= sLine
.Trim();
228 chunk
= std::make_unique
<Chunk
>();
229 CString sRemove
= sLine
.Left(sLine
.Find(' '));
230 CString sAdd
= sLine
.Mid(sLine
.Find(' '));
231 chunk
->lRemoveStart
= abs(_wtol(sRemove
));
232 if (sRemove
.Find(',')>=0)
234 sRemove
= sRemove
.Mid(sRemove
.Find(',')+1);
235 chunk
->lRemoveLength
= _wtol(sRemove
);
239 chunk
->lRemoveStart
= 0;
240 chunk
->lRemoveLength
= abs(_wtol(sRemove
));
242 chunk
->lAddStart
= _wtol(sAdd
);
243 if (sAdd
.Find(',')>=0)
245 sAdd
= sAdd
.Mid(sAdd
.Find(',')+1);
246 chunk
->lAddLength
= _wtol(sAdd
);
250 chunk
->lAddStart
= 1;
251 chunk
->lAddLength
= _wtol(sAdd
);
257 case 5: //[ |+|-] <sourceline>
259 //this line is either a context line (with a ' ' in front)
260 //a line added (with a '+' in front)
261 //or a removed line (with a '-' in front)
266 type
= sLine
.GetAt(0);
269 //it's a context line - we don't use them here right now
270 //but maybe in the future the patch algorithm can be
271 //extended to use those in case the file to patch has
272 //already changed and no base file is around...
273 chunk
->arLines
.Add(RemoveUnicodeBOM(sLine
.Mid(static_cast<int>(wcslen(L
" ")))));
274 chunk
->arLinesStates
.Add(PATCHSTATE_CONTEXT
);
275 chunk
->arEOLs
.push_back(ending
);
278 else if (type
== '\\')
280 //it's a context line (sort of):
281 //warnings start with a '\' char (e.g. "\ No newline at end of file")
282 //so just ignore this...
284 else if (type
== '-')
287 chunk
->arLines
.Add(RemoveUnicodeBOM(sLine
.Mid(static_cast<int>(wcslen(L
"-")))));
288 if (chunk
->lRemoveStart
== 1 && nRemoveLineCount
== 0)
290 if (HasUnicodeBOM(sLine
.Mid(static_cast<int>(wcslen(L
"-")))))
291 chunks
->oldHasBom
= 1;
293 chunks
->oldHasBom
= 0;
295 chunk
->arLinesStates
.Add(PATCHSTATE_REMOVED
);
296 chunk
->arEOLs
.push_back(ending
);
299 else if (type
== '+')
302 chunk
->arLines
.Add(RemoveUnicodeBOM(sLine
.Mid(static_cast<int>(wcslen(L
"+")))));
303 if (chunk
->lAddStart
== 1 && nAddLineCount
== 0)
305 if (HasUnicodeBOM(sLine
.Mid(static_cast<int>(wcslen(L
"-")))))
306 chunks
->newHasBom
= 1;
308 chunks
->newHasBom
= 0;
310 chunk
->arLinesStates
.Add(PATCHSTATE_ADDED
);
311 chunk
->arEOLs
.push_back(ending
);
316 //none of those lines! what the hell happened here?
317 m_sErrorMessage
.Format(IDS_ERR_PATCH_UNKNOWNLINETYPE
, nIndex
);
320 if ((chunk
->lAddLength
== (nAddLineCount
+ nContextLineCount
)) &&
321 chunk
->lRemoveLength
== (nRemoveLineCount
+ nContextLineCount
))
325 chunks
->chunks
.emplace_back(std::move(chunk
));
328 nContextLineCount
= 0;
329 nRemoveLineCount
= 0;
337 } // for ( ;nIndex<m_PatchLines.GetCount(); nIndex++)
340 m_sErrorMessage
.LoadString(IDS_ERR_PATCH_CHUNKMISMATCH
);
344 m_arFileDiffs
.emplace_back(chunks
.release());
346 for (size_t i
= 0; i
< m_arFileDiffs
.size(); ++i
)
348 if (filenamesToPatch
[m_arFileDiffs
[i
]->sFilePath
] > 1 && m_arFileDiffs
[i
]->sFilePath
!= L
"NUL")
350 m_sErrorMessage
.Format(IDS_ERR_PATCH_FILENAMENOTUNIQUE
, static_cast<LPCWSTR
>(m_arFileDiffs
[i
]->sFilePath
));
354 ++filenamesToPatch
[m_arFileDiffs
[i
]->sFilePath
];
355 if (m_arFileDiffs
[i
]->sFilePath
!= m_arFileDiffs
[i
]->sFilePath2
)
357 if (filenamesToPatch
[m_arFileDiffs
[i
]->sFilePath2
] > 1 && m_arFileDiffs
[i
]->sFilePath2
!= L
"NUL")
359 m_sErrorMessage
.Format(IDS_ERR_PATCH_FILENAMENOTUNIQUE
, static_cast<LPCWSTR
>(m_arFileDiffs
[i
]->sFilePath
));
363 ++filenamesToPatch
[m_arFileDiffs
[i
]->sFilePath2
];
374 BOOL
CPatch::OpenUnifiedDiffFile(const CString
& filename
)
376 #ifndef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
377 CCrashReport::Instance().AddFile2(filename
, nullptr, L
"unified diff file", CR_AF_MAKE_FILE_COPY
);
380 CFileTextLines PatchLines
;
381 if (!PatchLines
.Load(filename
))
383 m_sErrorMessage
= PatchLines
.GetErrorString();
388 //now we got all the lines of the patch file
389 //in our array - parsing can start...
390 return ParsePatchFile(PatchLines
);
393 CString
CPatch::GetFilename(int nIndex
)
395 if (nIndex
< 0 || nIndex
>= static_cast<int>(m_arFileDiffs
.size()))
398 return Strip(m_arFileDiffs
[nIndex
]->sFilePath
);
401 CString
CPatch::GetRevision(int nIndex
)
403 if (nIndex
< 0 || nIndex
>= static_cast<int>(m_arFileDiffs
.size()))
406 return m_arFileDiffs
[nIndex
]->sRevision
;
409 CString
CPatch::GetFilename2(int nIndex
)
411 if (nIndex
< 0 || nIndex
>= static_cast<int>(m_arFileDiffs
.size()))
414 return Strip(m_arFileDiffs
[nIndex
]->sFilePath2
);
417 CString
CPatch::GetRevision2(int nIndex
)
419 if (nIndex
< 0 || nIndex
>= static_cast<int>(m_arFileDiffs
.size()))
422 return m_arFileDiffs
[nIndex
]->sRevision2
;
425 int CPatch::PatchFile(const int strip
, int nIndex
, const CString
& sPatchPath
, const CString
& sSavePath
, const CString
& sBaseFile
, const bool force
)
428 CString sPath
= GetFullPath(sPatchPath
, nIndex
);
429 if (PathIsDirectory(sPath
))
431 m_sErrorMessage
.Format(IDS_ERR_PATCH_INVALIDPATCHFILE
, static_cast<LPCWSTR
>(sPath
));
436 m_sErrorMessage
.Format(IDS_ERR_PATCH_FILENOTINPATCH
, static_cast<LPCWSTR
>(sPath
));
440 if (!force
&& sPath
== L
"NUL" && PathFileExists(GetFullPath(sPatchPath
, nIndex
, 1)))
443 if (GetFullPath(sPatchPath
, nIndex
, 1) == L
"NUL" && !PathFileExists(sPath
))
447 CString sPatchFile
= sBaseFile
.IsEmpty() ? sPath
: sBaseFile
;
448 #ifndef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
449 if (PathFileExists(sPatchFile
))
451 CCrashReport::Instance().AddFile2(sPatchFile
, nullptr, L
"File to patch", CR_AF_MAKE_FILE_COPY
);
454 CFileTextLines PatchLines
;
455 CFileTextLines PatchLinesResult
;
456 PatchLines
.Load(sPatchFile
);
457 PatchLinesResult
= PatchLines
; //.Copy(PatchLines);
458 PatchLines
.CopySettings(&PatchLinesResult
);
460 auto chunks
= m_arFileDiffs
[nIndex
].get();
462 for (size_t i
= 0; i
< chunks
->chunks
.size(); ++i
)
464 auto chunk
= chunks
->chunks
[i
].get();
465 LONG lRemoveLine
= chunk
->lRemoveStart
;
466 LONG lAddLine
= chunk
->lAddStart
;
467 for (int j
= 0; j
< chunk
->arLines
.GetCount(); ++j
)
469 CString sPatchLine
= chunk
->arLines
.GetAt(j
);
470 EOL ending
= chunk
->arEOLs
[j
];
471 int nPatchState
= static_cast<int>(chunk
->arLinesStates
.GetAt(j
));
474 case PATCHSTATE_REMOVED
:
476 if ((lAddLine
> PatchLines
.GetCount())||(PatchLines
.GetCount()==0))
478 m_sErrorMessage
.FormatMessage(IDS_ERR_PATCH_DOESNOTMATCH
, L
"", static_cast<LPCWSTR
>(sPatchLine
));
483 if ((sPatchLine
.Compare(PatchLines
.GetAt(lAddLine
-1))!=0)&&(!HasExpandedKeyWords(sPatchLine
)))
485 m_sErrorMessage
.FormatMessage(IDS_ERR_PATCH_DOESNOTMATCH
, static_cast<LPCWSTR
>(sPatchLine
), static_cast<LPCWSTR
>(PatchLines
.GetAt(lAddLine
-1)));
488 if (lAddLine
> PatchLines
.GetCount())
490 m_sErrorMessage
.FormatMessage(IDS_ERR_PATCH_DOESNOTMATCH
, static_cast<LPCWSTR
>(sPatchLine
), L
"");
493 PatchLines
.RemoveAt(lAddLine
-1);
496 case PATCHSTATE_ADDED
:
500 // check context after insertions in order to avoid double insertions
501 bool insertOk
= !(lAddLine
< PatchLines
.GetCount());
503 for (; k
< chunk
->arLines
.GetCount(); ++k
)
505 if (static_cast<int>(chunk
->arLinesStates
.GetAt(k
)) == PATCHSTATE_ADDED
)
507 if (PatchLines
.GetCount() >= lAddLine
&& chunk
->arLines
.GetAt(k
).Compare(PatchLines
.GetAt(lAddLine
- 1)) == 0)
515 PatchLines
.InsertAt(lAddLine
-1, sPatchLine
, ending
);
520 if (k
>= chunk
->arLines
.GetCount())
522 m_sErrorMessage
.FormatMessage(IDS_ERR_PATCH_DOESNOTMATCH
, static_cast<LPCWSTR
>(PatchLines
.GetAt(lAddLine
) - 1), static_cast<LPCWSTR
>(chunk
->arLines
.GetAt(k
)));
527 case PATCHSTATE_CONTEXT
:
529 if (lAddLine
> PatchLines
.GetCount())
531 m_sErrorMessage
.FormatMessage(IDS_ERR_PATCH_DOESNOTMATCH
, L
"", static_cast<LPCWSTR
>(sPatchLine
));
536 if (lRemoveLine
== 0)
538 if ((sPatchLine
.Compare(PatchLines
.GetAt(lAddLine
-1))!=0) &&
539 (!HasExpandedKeyWords(sPatchLine
)) &&
540 (lRemoveLine
<= PatchLines
.GetCount()) &&
541 (sPatchLine
.Compare(PatchLines
.GetAt(lRemoveLine
-1))!=0))
543 if ((lAddLine
< PatchLines
.GetCount())&&(sPatchLine
.Compare(PatchLines
.GetAt(lAddLine
))==0))
545 else if (((lAddLine
+ 1) < PatchLines
.GetCount())&&(sPatchLine
.Compare(PatchLines
.GetAt(lAddLine
+1))==0))
547 else if ((lRemoveLine
< PatchLines
.GetCount())&&(sPatchLine
.Compare(PatchLines
.GetAt(lRemoveLine
))==0))
551 m_sErrorMessage
.FormatMessage(IDS_ERR_PATCH_DOESNOTMATCH
, static_cast<LPCWSTR
>(sPatchLine
), static_cast<LPCWSTR
>(PatchLines
.GetAt(lAddLine
-1)));
562 } // switch (nPatchState)
563 } // for (j=0; j<chunk->arLines.GetCount(); j++)
564 } // for (int i=0; i<chunks->chunks.GetCount(); i++)
565 if ((chunks
->oldHasBom
== 0 || (chunks
->chunks
.size() == 1 && chunks
->chunks
.at(0).get()->lRemoveStart
== 0 && chunks
->chunks
.at(0).get()->lRemoveLength
== 0)) && chunks
->newHasBom
== 1 && PatchLines
.GetUnicodeType() != CFileTextLines::UnicodeType::UTF8BOM
)
567 auto saveParams
= PatchLines
.GetSaveParams();
568 saveParams
.m_UnicodeType
= CFileTextLines::UnicodeType::UTF8BOM
;
569 PatchLines
.SetSaveParams(saveParams
);
571 else if (chunks
->oldHasBom
== 1 && chunks
->newHasBom
== 0 && PatchLines
.GetUnicodeType() == CFileTextLines::UnicodeType::UTF8BOM
)
573 auto saveParams
= PatchLines
.GetSaveParams();
574 saveParams
.m_UnicodeType
= CFileTextLines::UnicodeType::UTF8
;
575 PatchLines
.SetSaveParams(saveParams
);
577 if (!sSavePath
.IsEmpty())
579 PatchLines
.Save(sSavePath
, false);
584 BOOL
CPatch::HasExpandedKeyWords(const CString
& line
) const
586 if (line
.Find(L
"$LastChangedDate") >= 0)
588 if (line
.Find(L
"$Date") >= 0)
590 if (line
.Find(L
"$LastChangedRevision") >= 0)
592 if (line
.Find(L
"$Rev") >= 0)
594 if (line
.Find(L
"$LastChangedBy") >= 0)
596 if (line
.Find(L
"$Author") >= 0)
598 if (line
.Find(L
"$HeadURL") >= 0)
600 if (line
.Find(L
"$URL") >= 0)
602 if (line
.Find(L
"$Id") >= 0)
607 CString
CPatch::CheckPatchPath(const CString
& path
)
609 //first check if the path already matches
610 if (CountMatches(path
) > (GetNumberOfFiles()/3))
612 //now go up the tree and try again
613 CString upperpath
= path
;
614 while (upperpath
.ReverseFind('\\')>0)
616 upperpath
= upperpath
.Left(upperpath
.ReverseFind('\\'));
617 if (CountMatches(upperpath
) > (GetNumberOfFiles()/3))
620 //still no match found. So try sub folders
621 CDirFileEnum
filefinder(path
);
622 while (auto file
= filefinder
.NextFile())
624 if (!file
->IsDirectory())
626 CString subpath
= file
->GetFilePath();
627 if (GitAdminDir::IsAdminDirPath(subpath
))
629 if (CountMatches(subpath
) > (GetNumberOfFiles()/3))
633 // if a patch file only contains newly added files
634 // we can't really find the correct path.
635 // But: we can compare paths strings without the filenames
636 // and check if at least those match
638 while (upperpath
.ReverseFind('\\')>0)
640 upperpath
= upperpath
.Left(upperpath
.ReverseFind('\\'));
641 if (CountDirMatches(upperpath
) > (GetNumberOfFiles()/3))
648 int CPatch::CountMatches(const CString
& path
)
651 for (int i
=0; i
<GetNumberOfFiles(); ++i
)
653 CString temp
= GetFilename(i
);
654 temp
.Replace('/', '\\');
655 if (PathIsRelative(temp
))
656 temp
= path
+ L
'\\' + temp
;
657 if (PathFileExists(temp
))
663 int CPatch::CountDirMatches(const CString
& path
)
666 for (int i
=0; i
<GetNumberOfFiles(); ++i
)
668 CString temp
= GetFilename(i
);
669 temp
.Replace('/', '\\');
670 if (PathIsRelative(temp
))
671 temp
= path
+ L
'\\' + temp
;
672 // remove the filename
673 temp
= temp
.Left(temp
.ReverseFind('\\'));
674 if (PathFileExists(temp
))
680 CString
CPatch::Strip(const CString
& filename
) const
682 CString s
= filename
;
685 // Remove windows drive letter "c:"
686 if ( s
.GetLength()>2 && s
[1]==':')
691 for (int nStrip
= 1; nStrip
<= m_nStrip
; ++nStrip
)
693 // "/home/ts/my-working-copy/dir/file.txt"
694 // "home/ts/my-working-copy/dir/file.txt"
695 // "ts/my-working-copy/dir/file.txt"
696 // "my-working-copy/dir/file.txt"
698 s
= s
.Mid(s
.FindOneOf(L
"/\\") + 1);
704 CString
CPatch::GetFullPath(const CString
& sPath
, int nIndex
, int fileno
/* = 0*/)
708 temp
= GetFilename(nIndex
);
710 temp
= GetFilename2(nIndex
);
712 temp
.Replace('/', '\\');
716 if (PathIsRelative(temp
))
718 if (sPath
.Right(1) != L
"\\")
719 temp
= sPath
+ L
'\\' + temp
;
727 bool CPatch::HasUnicodeBOM(const CString
& str
) const
731 if (str
[0] == 0xFEFF)
736 CString
CPatch::RemoveUnicodeBOM(const CString
& str
) const
740 if (str
[0] == 0xFEFF)