Fixed issue #3261: Review/Apply single patch does not work anymore if patch does...
[TortoiseGit.git] / src / Git / GitPatch.cpp
blob9f99b1cc45cf82b18c448c84535800ab94407633
1 // TortoiseGitMerge - a Diff/Patch program
3 // Copyright (C) 2012-2013, 2015-2018 - 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()
34 : m_nStrip(0)
35 , m_bSuccessfullyPatched(false)
36 , m_nRejected(0)
37 , m_pProgDlg(nullptr)
38 , m_patch()
42 GitPatch::~GitPatch()
46 int GitPatch::Init(const CString& patchfile, const CString& targetpath, CSysProgressDlg *pPprogDlg)
48 CTGitPath target = CTGitPath(targetpath);
49 if (patchfile.IsEmpty() || targetpath.IsEmpty())
51 m_errorStr.LoadString(IDS_ERR_PATCHPATHS);
52 return 0;
55 m_errorStr.Empty();
56 m_patchfile = patchfile;
57 m_targetpath = targetpath;
58 m_testPath.Empty();
60 m_patchfile.Replace('\\', '/');
61 m_targetpath.Replace('\\', '/');
63 if (pPprogDlg)
65 pPprogDlg->SetTitle(IDS_APPNAME);
66 pPprogDlg->FormatNonPathLine(1, IDS_PATCH_PROGTITLE);
67 pPprogDlg->SetShowProgressBar(false);
68 pPprogDlg->ShowModeless(AfxGetMainWnd());
69 m_pProgDlg = pPprogDlg;
72 m_filePaths.clear();
73 m_nRejected = 0;
74 m_nStrip = 0;
76 // Read and try to apply patch
77 if (m_patch.OpenUnifiedDiffFile(m_patchfile) == FALSE)
79 m_errorStr = m_patch.GetErrorMessage();
80 m_filePaths.clear();
81 return 0;
83 if (!ApplyPatches())
85 m_filePaths.clear();
86 return 0;
89 m_pProgDlg = nullptr;
91 if ((m_nRejected > ((int)m_filePaths.size() / 3)) && !m_testPath.IsEmpty())
93 ++m_nStrip;
94 bool found = false;
95 for (m_nStrip = 0; m_nStrip < STRIP_LIMIT; ++m_nStrip)
97 for (const auto& filepath : m_filePaths)
99 if (Strip(filepath.path).IsEmpty())
101 found = true;
102 m_nStrip--;
103 break;
106 if (found)
107 break;
111 if (m_nStrip == STRIP_LIMIT)
112 m_filePaths.clear();
113 else if (m_nStrip > 0)
115 m_filePaths.clear();
116 m_nRejected = 0;
118 if (!ApplyPatches())
119 m_filePaths.clear();
121 return (int)m_filePaths.size();
124 bool GitPatch::ApplyPatches()
126 for (int i = 0; i < m_patch.GetNumberOfFiles(); ++i)
128 if (!PatchFile(i, m_targetpath))
129 return false;
132 return true;
135 bool GitPatch::PatchFile(int nIndex, CString &datapath)
137 CString sFilePath = m_patch.GetFullPath(datapath, nIndex);
138 CString sTempFile = CTempFiles::Instance().GetTempFilePathString();
140 PathRejects pr;
141 m_testPath = m_patch.GetFilename2(nIndex);
142 pr.path = m_patch.GetFilename2(nIndex);
143 if (pr.path == L"NUL")
144 pr.path = m_patch.GetFilename(nIndex);
146 if (m_pProgDlg)
147 m_pProgDlg->FormatPathLine(2, IDS_PATCH_PATHINGFILE, (LPCTSTR)pr.path);
149 //first, do a "dry run" of patching against the file in place...
150 if (!m_patch.PatchFile(m_nStrip, nIndex, datapath, sTempFile))
152 //patching not successful, so retrieve the
153 //base file from version control and try
154 //again...
155 CString sVersion = m_patch.GetRevision(nIndex);
157 CString sBaseFile;
158 if ((sVersion.GetLength() >= 7 && wcsncmp(sVersion, GIT_REV_ZERO, sVersion.GetLength()) == 0) || sFilePath == L"NUL")
159 sBaseFile = CTempFiles::Instance().GetTempFilePathString();
160 else
162 if (sVersion.IsEmpty())
164 m_errorStr.Format(IDS_ERR_MAINFRAME_FILECONFLICTNOVERSION, (LPCTSTR)sFilePath);
165 return false; // cannot apply patch which does not apply cleanly w/o git information in patch file.
167 sBaseFile = CTempFiles::Instance().GetTempFilePathString();
168 if (!CAppUtils::GetVersionedFile(sFilePath, sVersion, sBaseFile, m_pProgDlg))
170 m_errorStr.FormatMessage(IDS_ERR_MAINFRAME_FILEVERSIONNOTFOUND, (LPCTSTR)sVersion, (LPCTSTR)sFilePath);
172 return false;
176 if (m_pProgDlg)
177 m_pProgDlg->FormatPathLine(2, IDS_PATCH_PATHINGFILE, (LPCTSTR)pr.path);
179 int patchtry = m_patch.PatchFile(m_nStrip, nIndex, datapath, sTempFile, sBaseFile, true);
181 if (patchtry == TRUE)
183 pr.rejects = 0;
184 pr.basePath = sBaseFile;
186 else
188 pr.rejects = 1;
189 // rejectsPath should hold the absolute path to the reject files, but we do not support reject files ATM; also see changes FilePatchesDlg
190 pr.rejectsPath = m_patch.GetErrorMessage();
193 TRACE(L"comparing %s and %s\nagainst the base file %s\n", (LPCTSTR)sTempFile, (LPCTSTR)sFilePath, (LPCTSTR)sBaseFile);
195 else
197 //"dry run" was successful, so save the patched file somewhere...
198 pr.rejects = 0;
199 TRACE(L"comparing %s\nwith the patched result %s\n", (LPCTSTR)sFilePath, (LPCTSTR)sTempFile);
202 pr.resultPath = sTempFile;
203 pr.content = true;
204 pr.props = false;
205 // only add this entry if it hasn't been added already
206 bool bExists = false;
207 for (auto it = m_filePaths.crbegin(); it != m_filePaths.crend(); ++it)
209 if (it->path.Compare(pr.path) == 0)
211 bExists = true;
212 break;
215 if (!bExists)
216 m_filePaths.push_back(pr);
217 m_nRejected += pr.rejects;
219 return true;
222 CString GitPatch::GetPatchRejects(int nIndex) const
224 if (nIndex < 0)
225 return L"";
226 if (nIndex < (int)m_filePaths.size())
227 return m_filePaths[nIndex].rejectsPath;
229 return L"";
232 bool GitPatch::PatchPath(const CString& path)
234 m_errorStr.Empty();
236 m_patchfile.Replace(L'\\', L'/');
237 m_targetpath.Replace(L'\\', L'/');
239 m_filetopatch = path.Mid(m_targetpath.GetLength()+1);
240 m_filetopatch.Replace(L'\\', L'/');
242 m_nRejected = 0;
244 m_errorStr = L"NOT IMPLEMENTED";
245 return false;
249 int GitPatch::GetPatchResult(const CString& sPath, CString& sSavePath, CString& sRejectPath, CString &sBasePath) const
251 for (const auto& filePath : m_filePaths)
253 if (Strip(filePath.path).CompareNoCase(sPath) == 0)
255 sSavePath = filePath.resultPath;
256 sBasePath = filePath.basePath;
257 if (filePath.rejects > 0)
258 sRejectPath = filePath.rejectsPath;
259 else
260 sRejectPath.Empty();
261 return filePath.rejects;
264 return -1;
267 CString GitPatch::CheckPatchPath(const CString& path)
269 // first check if the path already matches
270 if (CountMatches(path) > (GetNumberOfFiles() / 3))
271 return path;
273 CSysProgressDlg progress;
274 CString tmp;
275 progress.SetTitle(IDS_PATCH_SEARCHPATHTITLE);
276 progress.SetShowProgressBar(false);
277 tmp.LoadString(IDS_PATCH_SEARCHPATHLINE1);
278 progress.SetLine(1, tmp);
279 progress.ShowModeless(AfxGetMainWnd());
281 // now go up the tree and try again
282 CString upperpath = path;
283 int trimPos;
284 while ((trimPos = upperpath.ReverseFind(L'\\')) > 0)
286 upperpath.Truncate(trimPos);
287 progress.SetLine(2, upperpath, true);
288 if (progress.HasUserCancelled())
289 return path;
290 if (CountMatches(upperpath) > (GetNumberOfFiles()/3))
291 return upperpath;
293 // still no match found. So try sub folders
294 bool isDir = false;
295 CString subpath;
296 CDirFileEnum filefinder(path);
297 while (filefinder.NextFile(subpath, &isDir))
299 if (progress.HasUserCancelled())
300 return path;
301 if (!isDir)
302 continue;
303 if (GitAdminDir::IsAdminDirPath(subpath))
304 continue;
305 progress.SetLine(2, subpath, true);
306 if (CountMatches(subpath) > (GetNumberOfFiles()/3))
307 return subpath;
310 // if a patch file only contains newly added files
311 // we can't really find the correct path.
312 // But: we can compare paths strings without the filenames
313 // and check if at least those match
314 upperpath = path;
315 while ((trimPos = upperpath.ReverseFind(L'\\')) > 0)
317 upperpath.Truncate(trimPos);
318 progress.SetLine(2, upperpath, true);
319 if (progress.HasUserCancelled())
320 return path;
321 if (CountDirMatches(upperpath) > (GetNumberOfFiles()/3))
322 return upperpath;
325 return path;
328 int GitPatch::CountMatches(const CString& path) const
330 int matches = 0;
331 for (int i=0; i<GetNumberOfFiles(); ++i)
333 CString temp = GetStrippedPath(i);
334 temp.Replace('/', '\\');
335 if (PathIsRelative(temp) || ((temp.GetLength() > 1) && (temp[0] == L'\\') && (temp[1] != L'\\')))
336 temp = path + L'\\' + temp;
337 if (PathFileExists(temp))
338 ++matches;
340 return matches;
343 int GitPatch::CountDirMatches(const CString& path) const
345 int matches = 0;
346 for (int i=0; i<GetNumberOfFiles(); ++i)
348 CString temp = GetStrippedPath(i);
349 temp.Replace(L'/', L'\\');
350 if (PathIsRelative(temp))
351 temp = path + L'\\' + temp;
352 // remove the filename
353 temp.Truncate(max(0, temp.ReverseFind(L'\\')));
354 if (PathFileExists(temp))
355 ++matches;
357 return matches;
360 CString GitPatch::GetStrippedPath(int nIndex) const
362 if (nIndex < 0)
363 return L"";
364 if (nIndex < (int)m_filePaths.size())
366 CString filepath = Strip(GetFilePath(nIndex));
367 return filepath;
370 return L"";
373 CString GitPatch::Strip(const CString& filename) const
375 CString s = filename;
376 if ( m_nStrip>0 )
378 // Remove windows drive letter "c:"
379 if (s.GetLength() > 2 && s[1] == L':')
380 s = s.Mid(2);
382 for (int nStrip = 1; nStrip <= m_nStrip; ++nStrip)
384 // "/home/ts/my-working-copy/dir/file.txt"
385 // "home/ts/my-working-copy/dir/file.txt"
386 // "ts/my-working-copy/dir/file.txt"
387 // "my-working-copy/dir/file.txt"
388 // "dir/file.txt"
389 int p = s.FindOneOf(L"/\\");
390 if (p < 0)
392 s.Empty();
393 break;
395 s = s.Mid(p+1);
398 return s;
401 bool GitPatch::RemoveFile(const CString& /*path*/)
403 // Delete file in Git
404 // not necessary now, because TGit doesn't support the "missing file" status
405 return true;