Updated libgit to version 2.46.2 based on Git for Windows sources
[TortoiseGit.git] / src / Git / GitPatch.cpp
blob70e557c5738e152a85cb9ffbbb0160fe27d65679
1 // TortoiseGitMerge - a Diff/Patch program
3 // Copyright (C) 2012-2013, 2015-2019, 2023 - TortoiseGit
4 // Copyright (C) 2010-2012 - TortoiseSVN
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "stdafx.h"
21 #include "resource.h"
22 #include "GitPatch.h"
23 #include "UnicodeUtils.h"
24 #include "SysProgressDlg.h"
25 #include "DirFileEnum.h"
26 #include "GitAdminDir.h"
27 #include "StringUtils.h"
29 #include "AppUtils.h"
31 #define STRIP_LIMIT 10
33 GitPatch::GitPatch()
37 GitPatch::~GitPatch()
41 int GitPatch::Init(const CString& patchfile, const CString& targetpath, CSysProgressDlg *pPprogDlg)
43 if (patchfile.IsEmpty() || targetpath.IsEmpty())
45 m_errorStr.LoadString(IDS_ERR_PATCHPATHS);
46 return 0;
49 m_errorStr.Empty();
50 m_patchfile = patchfile;
51 m_targetpath = targetpath;
52 m_testPath.Empty();
54 m_patchfile.Replace('\\', '/');
55 m_targetpath.Replace('\\', '/');
57 if (pPprogDlg)
59 pPprogDlg->SetTitle(IDS_APPNAME);
60 pPprogDlg->FormatNonPathLine(1, IDS_PATCH_PROGTITLE);
61 pPprogDlg->SetShowProgressBar(false);
62 pPprogDlg->ShowModeless(AfxGetMainWnd());
63 m_pProgDlg = pPprogDlg;
66 m_filePaths.clear();
67 m_nRejected = 0;
68 m_nStrip = 0;
70 // Read and try to apply patch
71 if (m_patch.OpenUnifiedDiffFile(m_patchfile) == FALSE)
73 m_errorStr = m_patch.GetErrorMessage();
74 m_filePaths.clear();
75 return 0;
77 if (!ApplyPatches())
79 m_filePaths.clear();
80 return 0;
83 m_pProgDlg = nullptr;
85 if ((m_nRejected > (static_cast<int>(m_filePaths.size()) / 3)) && !m_testPath.IsEmpty())
87 for (m_nStrip = 0; m_nStrip < STRIP_LIMIT; ++m_nStrip)
89 if (std::any_of(m_filePaths.cbegin(), m_filePaths.cend(), [this](auto& filepath) { return Strip(filepath.path).IsEmpty(); }))
91 --m_nStrip;
92 break;
97 if (m_nStrip == STRIP_LIMIT)
98 m_filePaths.clear();
99 else if (m_nStrip > 0)
101 m_filePaths.clear();
102 m_nRejected = 0;
104 if (!ApplyPatches())
105 m_filePaths.clear();
107 return static_cast<int>(m_filePaths.size());
110 bool GitPatch::ApplyPatches()
112 for (int i = 0; i < m_patch.GetNumberOfFiles(); ++i)
114 if (!PatchFile(i, m_targetpath))
115 return false;
118 return true;
121 bool GitPatch::PatchFile(int nIndex, CString &datapath)
123 CString sFilePath = m_patch.GetFullPath(datapath, nIndex);
124 CString sTempFile = CTempFiles::Instance().GetTempFilePathString();
126 PathRejects pr;
127 m_testPath = m_patch.GetFilename2(nIndex);
128 pr.path = m_patch.GetFilename2(nIndex);
129 if (pr.path == L"NUL")
130 pr.path = m_patch.GetFilename(nIndex);
132 if (m_pProgDlg)
133 m_pProgDlg->FormatPathLine(2, IDS_PATCH_PATHINGFILE, static_cast<LPCWSTR>(pr.path));
135 //first, do a "dry run" of patching against the file in place...
136 if (!m_patch.PatchFile(m_nStrip, nIndex, datapath, sTempFile))
138 //patching not successful, so retrieve the
139 //base file from version control and try
140 //again...
141 CString sVersion = m_patch.GetRevision(nIndex);
143 CString sBaseFile;
144 if ((sVersion.GetLength() >= 7 && wcsncmp(sVersion, GIT_REV_ZERO, sVersion.GetLength()) == 0) || sFilePath == L"NUL")
145 sBaseFile = CTempFiles::Instance().GetTempFilePathString();
146 else
148 if (sVersion.IsEmpty())
150 m_errorStr.Format(IDS_ERR_MAINFRAME_FILECONFLICTNOVERSION, static_cast<LPCWSTR>(sFilePath));
151 return false; // cannot apply patch which does not apply cleanly w/o git information in patch file.
153 sBaseFile = CTempFiles::Instance().GetTempFilePathString();
154 if (!CAppUtils::GetVersionedFile(sFilePath, sVersion, sBaseFile, m_pProgDlg))
156 m_errorStr.FormatMessage(IDS_ERR_MAINFRAME_FILEVERSIONNOTFOUND, static_cast<LPCWSTR>(sVersion), static_cast<LPCWSTR>(sFilePath));
158 return false;
162 if (m_pProgDlg)
163 m_pProgDlg->FormatPathLine(2, IDS_PATCH_PATHINGFILE, static_cast<LPCWSTR>(pr.path));
165 const int patchtry = m_patch.PatchFile(m_nStrip, nIndex, datapath, sTempFile, sBaseFile, true);
167 if (patchtry == TRUE)
169 pr.rejects = 0;
170 pr.basePath = sBaseFile;
172 else
174 pr.rejects = 1;
175 // rejectsPath should hold the absolute path to the reject files, but we do not support reject files ATM; also see changes FilePatchesDlg
176 pr.rejectsPath = m_patch.GetErrorMessage();
179 TRACE(L"comparing %s and %s\nagainst the base file %s\n", static_cast<LPCWSTR>(sTempFile), static_cast<LPCWSTR>(sFilePath), static_cast<LPCWSTR>(sBaseFile));
181 else
183 //"dry run" was successful, so save the patched file somewhere...
184 pr.rejects = 0;
185 TRACE(L"comparing %s\nwith the patched result %s\n", static_cast<LPCWSTR>(sFilePath), static_cast<LPCWSTR>(sTempFile));
188 pr.resultPath = sTempFile;
189 pr.content = true;
190 pr.props = false;
191 // only add this entry if it hasn't been added already
192 if (std::none_of(m_filePaths.crbegin(), m_filePaths.crend(), [&pr](auto& filepath) { return filepath.path.Compare(pr.path) == 0; }))
193 m_filePaths.push_back(pr);
194 m_nRejected += pr.rejects;
196 return true;
199 CString GitPatch::GetPatchRejects(int nIndex) const
201 if (nIndex < 0)
202 return L"";
203 if (nIndex < static_cast<int>(m_filePaths.size()))
204 return m_filePaths[nIndex].rejectsPath;
206 return L"";
209 bool GitPatch::PatchPath(const CString& path)
211 m_errorStr.Empty();
213 m_patchfile.Replace(L'\\', L'/');
214 m_targetpath.Replace(L'\\', L'/');
216 m_filetopatch = path.Mid(m_targetpath.GetLength()+1);
217 m_filetopatch.Replace(L'\\', L'/');
219 m_nRejected = 0;
221 m_errorStr = L"NOT IMPLEMENTED";
222 return false;
226 int GitPatch::GetPatchResult(const CString& sPath, CString& sSavePath, CString& sRejectPath, CString &sBasePath) const
228 auto it = std::find_if(m_filePaths.cbegin(), m_filePaths.cend(), [this, &sPath](auto& filePath) { return Strip(filePath.path).CompareNoCase(sPath) == 0; });
229 if (it != m_filePaths.cend())
231 sSavePath = (*it).resultPath;
232 sBasePath = (*it).basePath;
233 if ((*it).rejects > 0)
234 sRejectPath = (*it).rejectsPath;
235 else
236 sRejectPath.Empty();
237 return (*it).rejects;
239 return -1;
242 CString GitPatch::CheckPatchPath(const CString& path)
244 // first check if the path already matches
245 if (CountMatches(path) > (GetNumberOfFiles() / 3))
246 return path;
248 CSysProgressDlg progress;
249 CString tmp;
250 progress.SetTitle(IDS_PATCH_SEARCHPATHTITLE);
251 progress.SetShowProgressBar(false);
252 tmp.LoadString(IDS_PATCH_SEARCHPATHLINE1);
253 progress.SetLine(1, tmp);
254 progress.ShowModeless(AfxGetMainWnd());
256 // now go up the tree and try again
257 CString upperpath = path;
258 int trimPos;
259 while ((trimPos = upperpath.ReverseFind(L'\\')) > 0)
261 upperpath.Truncate(trimPos);
262 progress.SetLine(2, upperpath, true);
263 if (progress.HasUserCancelled())
264 return path;
265 if (CountMatches(upperpath) > (GetNumberOfFiles()/3))
266 return upperpath;
268 // still no match found. So try sub folders
269 CDirFileEnum filefinder(path);
270 while (auto file = filefinder.NextFile())
272 if (progress.HasUserCancelled())
273 return path;
274 if (!file->IsDirectory())
275 continue;
276 CString subpath = file->GetFilePath();
277 if (GitAdminDir::IsAdminDirPath(subpath))
278 continue;
279 progress.SetLine(2, subpath, true);
280 if (CountMatches(subpath) > (GetNumberOfFiles()/3))
281 return subpath;
284 // if a patch file only contains newly added files
285 // we can't really find the correct path.
286 // But: we can compare paths strings without the filenames
287 // and check if at least those match
288 upperpath = path;
289 while ((trimPos = upperpath.ReverseFind(L'\\')) > 0)
291 upperpath.Truncate(trimPos);
292 progress.SetLine(2, upperpath, true);
293 if (progress.HasUserCancelled())
294 return path;
295 if (CountDirMatches(upperpath) > (GetNumberOfFiles()/3))
296 return upperpath;
299 return path;
302 int GitPatch::CountMatches(const CString& path) const
304 int matches = 0;
305 for (int i=0; i<GetNumberOfFiles(); ++i)
307 CString temp = GetStrippedPath(i);
308 temp.Replace('/', '\\');
309 if (PathIsRelative(temp) || ((temp.GetLength() > 1) && (temp[0] == L'\\') && (temp[1] != L'\\')))
310 temp = path + L'\\' + temp;
311 if (PathFileExists(temp))
312 ++matches;
314 return matches;
317 int GitPatch::CountDirMatches(const CString& path) const
319 int matches = 0;
320 for (int i=0; i<GetNumberOfFiles(); ++i)
322 CString temp = GetStrippedPath(i);
323 temp.Replace(L'/', L'\\');
324 if (PathIsRelative(temp))
325 temp = path + L'\\' + temp;
326 // remove the filename
327 temp.Truncate(max(0, temp.ReverseFind(L'\\')));
328 if (PathFileExists(temp))
329 ++matches;
331 return matches;
334 CString GitPatch::GetStrippedPath(int nIndex) const
336 if (nIndex < 0)
337 return L"";
338 if (nIndex < static_cast<int>(m_filePaths.size()))
340 CString filepath = Strip(GetFilePath(nIndex));
341 return filepath;
344 return L"";
347 CString GitPatch::Strip(const CString& filename) const
349 CString s = filename;
350 if ( m_nStrip>0 )
352 // Remove windows drive letter "c:"
353 if (s.GetLength() > 2 && s[1] == L':')
354 s = s.Mid(2);
356 for (int nStrip = 1; nStrip <= m_nStrip; ++nStrip)
358 // "/home/ts/my-working-copy/dir/file.txt"
359 // "home/ts/my-working-copy/dir/file.txt"
360 // "ts/my-working-copy/dir/file.txt"
361 // "my-working-copy/dir/file.txt"
362 // "dir/file.txt"
363 const int p = s.FindOneOf(L"/\\");
364 if (p < 0)
366 s.Empty();
367 break;
369 s = s.Mid(p+1);
372 return s;
375 bool GitPatch::RemoveFile(const CString& /*path*/)
377 // Delete file in Git
378 // not necessary now, because TGit doesn't support the "missing file" status
379 return true;