From 6f003d69c33aa3d058e82d8466aa215eb713ce9b Mon Sep 17 00:00:00 2001 From: Sven Strickroth Date: Wed, 23 Jan 2013 21:00:59 +0100 Subject: [PATCH] TortoiseGitMerge: Accept more patch files This patch enables TGitMerge to accept patches which are created using the *nix diff tool. Signed-off-by: Sven Strickroth --- src/Git/GitPatch.cpp | 8 +- src/Resources/TortoiseMergeENG.rc | 4 + src/TortoiseMerge/Patch.cpp | 529 ++++++++------------------------------ src/TortoiseMerge/Patch.h | 6 +- src/TortoiseMerge/resource.h | Bin 31108 -> 15651 bytes 5 files changed, 120 insertions(+), 427 deletions(-) rewrite src/TortoiseMerge/resource.h (99%) diff --git a/src/Git/GitPatch.cpp b/src/Git/GitPatch.cpp index a219f65f6..48104ba32 100644 --- a/src/Git/GitPatch.cpp +++ b/src/Git/GitPatch.cpp @@ -1,6 +1,6 @@ // TortoiseGitMerge - a Diff/Patch program -// Copyright (C) 2012 - TortoiseGit +// Copyright (C) 2012-2013 - TortoiseGit // Copyright (C) 2010-2012 - TortoiseSVN // This program is free software; you can redistribute it and/or @@ -162,6 +162,12 @@ bool GitPatch::PatchFile(int nIndex, CString &datapath) sBaseFile = CTempFiles::Instance().GetTempFilePathString(); else { + if (sVersion.IsEmpty()) + { + m_errorStr.Empty(); + m_errorStr.Format(IDS_ERR_MAINFRAME_FILECONFLICTNOVERSION, (LPCTSTR)sFilePath); + return false; // cannot apply patch which does not apply cleanly w/o git information in patch file. + } sBaseFile = CTempFiles::Instance().GetTempFilePathString(); if (!CAppUtils::GetVersionedFile(sFilePath, sVersion, sBaseFile, m_pProgDlg)) { diff --git a/src/Resources/TortoiseMergeENG.rc b/src/Resources/TortoiseMergeENG.rc index 5a4b0ae61..84d37f912 100644 --- a/src/Resources/TortoiseMergeENG.rc +++ b/src/Resources/TortoiseMergeENG.rc @@ -445,6 +445,10 @@ STRINGTABLE BEGIN IDR_MAINFRAME "TortoiseGitMerge" IDS_TITLE_REJECTEDHUNKS "Rejected patch hunks for '%s'" + IDS_ERR_PATCH_FILENAMENOTUNIQUE + "TortoiseGitMerge cannot process this patch file. The filename ""%s"" appears more than once." + IDS_ERR_MAINFRAME_FILECONFLICTNOVERSION + "The patch does not apply cleanly to %s and no version information is given.\nPatching is not possible!" END STRINGTABLE diff --git a/src/TortoiseMerge/Patch.cpp b/src/TortoiseMerge/Patch.cpp index 0990db3cf..b37f52460 100644 --- a/src/TortoiseMerge/Patch.cpp +++ b/src/TortoiseMerge/Patch.cpp @@ -1,7 +1,7 @@ // TortoiseGitMerge - a Diff/Patch program -// Copyright (C) 2009-2012 - TortoiseGit -// Copyright (C) 2012 - Sven Strickroth +// Copyright (C) 2009-2013 - TortoiseGit +// Copyright (C) 2012-2013 - Sven Strickroth // Copyright (C) 2004-2009,2011-2012 - TortoiseSVN // This program is free software; you can redistribute it and/or @@ -35,7 +35,6 @@ static char THIS_FILE[] = __FILE__; CPatch::CPatch(void) : m_nStrip(0) , m_UnicodeType(CFileTextLines::AUTOTYPE) - , m_IsGitPatch(false) { } @@ -59,17 +58,19 @@ void CPatch::FreeMemory() m_arFileDiffs.RemoveAll(); } -BOOL CPatch::ParserGitPatch(CFileTextLines &PatchLines,int nIndex) +BOOL CPatch::ParsePatchFile(CFileTextLines &PatchLines) { CString sLine; EOL ending = EOL_NOENDING; int state = 0; + int nIndex = 0; Chunks * chunks = NULL; Chunk * chunk = NULL; int nAddLineCount = 0; int nRemoveLineCount = 0; int nContextLineCount = 0; + std::map filenamesToPatch; for ( ;nIndexchunks.GetCount() > 0) + m_arFileDiffs.Add(chunks); + else + delete chunks; } chunks = new Chunks(); - + state = 1; + break; } - + } + // fallthrough! + case 1: + { //index if( sLine.Find(_T("index"))==0 ) { @@ -103,24 +111,24 @@ BOOL CPatch::ParserGitPatch(CFileTextLines &PatchLines,int nIndex) chunks->sRevision = sLine.Mid(dotstart-7,7); chunks->sRevision2 = sLine.Mid(dotstart+2,7); } + break; } //--- - if( sLine.Find(_T("--- "))==0 ) + if (sLine.Find(_T("--- ")) == 0) { - if (sLine.Left(3).Compare(_T("---"))!=0) + if (state == 0) { - //no starting "---" found - //seems to be either garbage or just - //a binary file. So start over... - state = 0; - nIndex--; if (chunks) { - delete chunks; - chunks = NULL; + //this is a new file diff, so add the last one to + //our array. + if (chunks->chunks.GetCount() > 0) + m_arFileDiffs.Add(chunks); + else + delete chunks; } - break; + chunks = new Chunks(); } sLine = sLine.Mid(3); //remove the "---" @@ -156,423 +164,43 @@ BOOL CPatch::ParserGitPatch(CFileTextLines &PatchLines,int nIndex) if (chunks->sFilePath == _T("\\dev\\null") || chunks->sFilePath == _T("/dev/null")) chunks->sFilePath = _T("NUL"); - } - - // +++ - if( sLine.Find(_T("+++ ")) == 0 ) - { - sLine = sLine.Mid(3); //remove the "---" - sLine =sLine.Trim(); - //at the end of the filepath there's a revision number... - int bracket = sLine.ReverseFind('('); - if (bracket < 0) - // some patch files can have another '(' char, especially ones created in Chinese OS - bracket = sLine.ReverseFind(0xff08); - - if (bracket < 0) - chunks->sFilePath2 = sLine.Trim(); - else - chunks->sFilePath2 = sLine.Left(bracket-1).Trim(); - if (chunks->sFilePath2.Find('\t')>=0) - { - chunks->sFilePath2 = chunks->sFilePath2.Left(chunks->sFilePath2.Find('\t')); - } - if (chunks->sFilePath2.Find(_T('"')) == 0 && chunks->sFilePath2.ReverseFind(_T('"')) == chunks->sFilePath2.GetLength() - 1) - chunks->sFilePath2=chunks->sFilePath2.Mid(1, chunks->sFilePath2.GetLength() - 2); - if( chunks->sFilePath2.Find(_T("a/")) == 0 ) - chunks->sFilePath2=chunks->sFilePath2.Mid(2); - - if( chunks->sFilePath2.Find(_T("b/")) == 0 ) - chunks->sFilePath2=chunks->sFilePath2.Mid(2); - - chunks->sFilePath2.Replace(_T('/'),_T('\\')); - - if (chunks->sFilePath2 == _T("\\dev\\null") || chunks->sFilePath2 == _T("/dev/null")) - chunks->sFilePath2 = _T("NUL"); - } - - //@@ -xxx,xxx +xxx,xxx @@ - if( sLine.Find(_T("@@")) == 0 ) - { - sLine = sLine.Mid(2); - sLine = sLine.Trim(); - chunk = new Chunk(); - CString sRemove = sLine.Left(sLine.Find(' ')); - CString sAdd = sLine.Mid(sLine.Find(' ')); - chunk->lRemoveStart = (-_ttol(sRemove)); - if (sRemove.Find(',')>=0) - { - sRemove = sRemove.Mid(sRemove.Find(',')+1); - chunk->lRemoveLength = _ttol(sRemove); - } - else - { - chunk->lRemoveStart = 0; - chunk->lRemoveLength = (-_ttol(sRemove)); - } - chunk->lAddStart = _ttol(sAdd); - if (sAdd.Find(',')>=0) - { - sAdd = sAdd.Mid(sAdd.Find(',')+1); - chunk->lAddLength = _ttol(sAdd); - } - else - { - chunk->lAddStart = 1; - chunk->lAddLength = _ttol(sAdd); - } - - state =5; - } - } - break; - - - case 5: //[ |+|-] - { - //this line is either a context line (with a ' ' in front) - //a line added (with a '+' in front) - //or a removed line (with a '-' in front) - TCHAR type; - if (sLine.IsEmpty()) - type = ' '; - else - type = sLine.GetAt(0); - if (type == ' ') - { - //it's a context line - we don't use them here right now - //but maybe in the future the patch algorithm can be - //extended to use those in case the file to patch has - //already changed and no base file is around... - chunk->arLines.Add(RemoveUnicodeBOM(sLine.Mid(1))); - chunk->arLinesStates.Add(PATCHSTATE_CONTEXT); - chunk->arEOLs.push_back(ending); - nContextLineCount++; - } - else if (type == '\\') - { - //it's a context line (sort of): - //warnings start with a '\' char (e.g. "\ No newline at end of file") - //so just ignore this... - } - else if (type == '-') - { - //a removed line - chunk->arLines.Add(RemoveUnicodeBOM(sLine.Mid(1))); - chunk->arLinesStates.Add(PATCHSTATE_REMOVED); - chunk->arEOLs.push_back(ending); - nRemoveLineCount++; - } - else if (type == '+') - { - //an added line - chunk->arLines.Add(RemoveUnicodeBOM(sLine.Mid(1))); - chunk->arLinesStates.Add(PATCHSTATE_ADDED); - chunk->arEOLs.push_back(ending); - nAddLineCount++; - } - else - { - //none of those lines! what the hell happened here? - m_sErrorMessage.Format(IDS_ERR_PATCH_UNKOWNLINETYPE, nIndex); - goto errorcleanup; - } - if ((chunk->lAddLength == (nAddLineCount + nContextLineCount)) && - chunk->lRemoveLength == (nRemoveLineCount + nContextLineCount)) - { - //chunk is finished - if (chunks) - chunks->chunks.Add(chunk); - else - delete chunk; - chunk = NULL; - nAddLineCount = 0; - nContextLineCount = 0; - nRemoveLineCount = 0; - state = 0; - } - } - break; - default: - ASSERT(FALSE); - } // switch (state) - } // for ( ;nIndexchunks.GetCount(); i++) - { - delete chunks->chunks.GetAt(i); - } - chunks->chunks.RemoveAll(); - delete chunks; - } - FreeMemory(); - return FALSE; -} - -BOOL CPatch::OpenUnifiedDiffFile(const CString& filename) -{ - CString sLine; - EOL ending = EOL_NOENDING; - INT_PTR nIndex = 0; - INT_PTR nLineCount = 0; - CCrashReport::Instance().AddFile2(filename, NULL, _T("unified diff file"), CR_AF_MAKE_FILE_COPY); - - CFileTextLines PatchLines; - if (!PatchLines.Load(filename)) - { - m_sErrorMessage = PatchLines.GetErrorString(); - return FALSE; - } - m_UnicodeType = PatchLines.GetUnicodeType(); - FreeMemory(); - nLineCount = PatchLines.GetCount(); - //now we got all the lines of the patch file - //in our array - parsing can start... - - for(nIndex=0;nIndexm_IsGitPatch=true; - break; - } - } - - //first, skip possible garbage at the beginning - //garbage is finished when a line starts with "Index: " - //and the next line consists of only "=" characters - if( !m_IsGitPatch ) - { - for (nIndex=0; nIndex= 0)||this->m_IsGitPatch)) - { - state = 2; - if (chunks) - { - //this is a new file diff, so add the last one to - //our array. - m_arFileDiffs.Add(chunks); - } - chunks = new Chunks(); - - int nTab = sLine.Find('\t'); - - int filestart = 4; - if(m_IsGitPatch) - { - nTab=sLine.GetLength(); - filestart = 6; - } - - if (nTab >= 0) - { - chunks->sFilePath = sLine.Mid(filestart, nTab-filestart).Trim(); - } - } - } - switch (state) - { - case 0: //Index: - { - CString nextLine; - if ((nIndex+1)= 0) - { - chunks->sFilePath = sLine.Mid(nColon+1).Trim(); - if (chunks->sFilePath.Find('\t')>=0) - chunks->sFilePath.Left(chunks->sFilePath.Find('\t')).TrimRight(); - if (chunks->sFilePath.Right(9).Compare(_T("(deleted)"))==0) - chunks->sFilePath.Left(chunks->sFilePath.GetLength()-9).TrimRight(); - if (chunks->sFilePath.Right(7).Compare(_T("(added)"))==0) - chunks->sFilePath.Left(chunks->sFilePath.GetLength()-7).TrimRight(); - } - state++; - } - } + state = 3; } if (state == 0) { - if (nIndex > 0) + if (sLine.Find(_T("@@")) == 0) { - nIndex--; - state = 4; - if (chunks == NULL) + if (chunks != NULL) { - //the line - //Index: - //was not found at the start of a file diff! - break; + nIndex--; + state = 4; } + else + break; } } } - break; - case 1: //==================== - { - sLine.Remove('='); - if (sLine.IsEmpty()) - { - // if the next line is already the start of the chunk, - // then the patch/diff file was not created by svn. But we - // still try to use it - if (PatchLines.GetCount() > (nIndex + 1)) - { + break; - if (PatchLines.GetAt((int)nIndex + 1).Left(2).Compare(_T("@@"))==0) - { - state += 2; - } - } - state++; - } - else - { - //the line - //========================= - //was not found - m_sErrorMessage.Format(IDS_ERR_PATCH_NOEQUATIONCHARLINE, nIndex); - goto errorcleanup; - } - } - break; - case 2: //--- - { - if (sLine.Left(3).Compare(_T("---"))!=0) - { - //no starting "---" found - //seems to be either garbage or just - //a binary file. So start over... - state = 0; - nIndex--; - if (chunks) - { - delete chunks; - chunks = NULL; - } - break; - } - sLine = sLine.Mid(3); //remove the "---" - sLine =sLine.Trim(); - //at the end of the filepath there's a revision number... - int bracket = sLine.ReverseFind('('); - if (bracket < 0) - // some patch files can have another '(' char, especially ones created in Chinese OS - bracket = sLine.ReverseFind(0xff08); - CString num = sLine.Mid(bracket); //num = "(revision xxxxx)" - num = num.Mid(num.Find(' ')); - num = num.Trim(_T(" )")); - // here again, check for the Chinese bracket - num = num.Trim(0xff09); - chunks->sRevision = num; - if (bracket < 0) - { - if (chunks->sFilePath.IsEmpty()) - chunks->sFilePath = sLine.Trim(); - } - else - chunks->sFilePath = sLine.Left(bracket-1).Trim(); - if (chunks->sFilePath.Find('\t')>=0) - { - chunks->sFilePath = chunks->sFilePath.Left(chunks->sFilePath.Find('\t')); - } - state++; - } - break; - case 3: //+++ + case 3: { - if (sLine.Left(3).Compare(_T("+++"))!=0) + // +++ + if (sLine.Left(3).Compare(_T("+++")) != 0) { - //no starting "+++" found + // no starting "+++" found m_sErrorMessage.Format(IDS_ERR_PATCH_NOADDFILELINE, nIndex); goto errorcleanup; } sLine = sLine.Mid(3); //remove the "---" sLine =sLine.Trim(); + //at the end of the filepath there's a revision number... int bracket = sLine.ReverseFind('('); if (bracket < 0) - // some patch files can have another '(' char, especially ones created in Chinese OS + // some patch files can have another '(' char, especially ones created in Chinese OS bracket = sLine.ReverseFind(0xff08); - CString num = sLine.Mid(bracket); //num = "(revision xxxxx)" - num = num.Mid(num.Find(' ')); - num = num.Trim(_T(" )")); - // here again, check for the Chinese bracket - num = num.Trim(0xff09); - chunks->sRevision2 = num; + if (bracket < 0) chunks->sFilePath2 = sLine.Trim(); else @@ -581,13 +209,27 @@ BOOL CPatch::OpenUnifiedDiffFile(const CString& filename) { chunks->sFilePath2 = chunks->sFilePath2.Left(chunks->sFilePath2.Find('\t')); } - state++; + if (chunks->sFilePath2.Find(_T('"')) == 0 && chunks->sFilePath2.ReverseFind(_T('"')) == chunks->sFilePath2.GetLength() - 1) + chunks->sFilePath2=chunks->sFilePath2.Mid(1, chunks->sFilePath2.GetLength() - 2); + if( chunks->sFilePath2.Find(_T("a/")) == 0 ) + chunks->sFilePath2=chunks->sFilePath2.Mid(2); + + if( chunks->sFilePath2.Find(_T("b/")) == 0 ) + chunks->sFilePath2=chunks->sFilePath2.Mid(2); + + chunks->sFilePath2.Replace(_T('/'),_T('\\')); + + if (chunks->sFilePath2 == _T("\\dev\\null") || chunks->sFilePath2 == _T("/dev/null")) + chunks->sFilePath2 = _T("NUL"); + + ++state; } - break; - case 4: //@@ -xxx,xxx +xxx,xxx @@ + break; + + case 4: { //start of a new chunk - if (sLine.Left(2).Compare(_T("@@"))!=0) + if (sLine.Left(2).Compare(_T("@@")) != 0) { //chunk doesn't start with "@@" //so there's garbage in between two file diffs @@ -595,7 +237,7 @@ BOOL CPatch::OpenUnifiedDiffFile(const CString& filename) if (chunk) { delete chunk; - chunk = 0; + chunk = nullptr; if (chunks) { for (int i=0; ichunks.GetCount(); i++) @@ -604,11 +246,13 @@ BOOL CPatch::OpenUnifiedDiffFile(const CString& filename) } chunks->chunks.RemoveAll(); delete chunks; - chunks = NULL; + chunks = nullptr; } } break; //skip the garbage } + + //@@ -xxx,xxx +xxx,xxx @@ sLine = sLine.Mid(2); sLine = sLine.Trim(); chunk = new Chunk(); @@ -638,8 +282,9 @@ BOOL CPatch::OpenUnifiedDiffFile(const CString& filename) } state++; } - break; - case 5: //[ |+|-] + break; + + case 5: //[ |+|-] { //this line is either a context line (with a ' ' in front) //a line added (with a '+' in front) @@ -715,7 +360,30 @@ BOOL CPatch::OpenUnifiedDiffFile(const CString& filename) } if (chunks) m_arFileDiffs.Add(chunks); + + for (int i = 0; i < m_arFileDiffs.GetCount(); ++i) + { + if (filenamesToPatch[m_arFileDiffs.GetAt(i)->sFilePath] > 1 && m_arFileDiffs.GetAt(i)->sFilePath != _T("NUL")) + { + m_sErrorMessage.Format(IDS_ERR_PATCH_FILENAMENOTUNIQUE, m_arFileDiffs.GetAt(i)->sFilePath); + FreeMemory(); + return FALSE; + } + ++filenamesToPatch[m_arFileDiffs.GetAt(i)->sFilePath]; + if (m_arFileDiffs.GetAt(i)->sFilePath != m_arFileDiffs.GetAt(i)->sFilePath2) + { + if (filenamesToPatch[m_arFileDiffs.GetAt(i)->sFilePath2] > 1 && m_arFileDiffs.GetAt(i)->sFilePath2 != _T("NUL")) + { + m_sErrorMessage.Format(IDS_ERR_PATCH_FILENAMENOTUNIQUE, m_arFileDiffs.GetAt(i)->sFilePath); + FreeMemory(); + return FALSE; + } + ++filenamesToPatch[m_arFileDiffs.GetAt(i)->sFilePath2]; + } + } + return TRUE; + errorcleanup: if (chunk) delete chunk; @@ -732,6 +400,23 @@ errorcleanup: return FALSE; } +BOOL CPatch::OpenUnifiedDiffFile(const CString& filename) +{ + CCrashReport::Instance().AddFile2(filename, NULL, _T("unified diff file"), CR_AF_MAKE_FILE_COPY); + + CFileTextLines PatchLines; + if (!PatchLines.Load(filename)) + { + m_sErrorMessage = PatchLines.GetErrorString(); + return FALSE; + } + FreeMemory(); + + //now we got all the lines of the patch file + //in our array - parsing can start... + return ParsePatchFile(PatchLines); +} + CString CPatch::GetFilename(int nIndex) { if (nIndex < 0) diff --git a/src/TortoiseMerge/Patch.h b/src/TortoiseMerge/Patch.h index 6d9f1ba00..a91819042 100644 --- a/src/TortoiseMerge/Patch.h +++ b/src/TortoiseMerge/Patch.h @@ -1,7 +1,7 @@ // TortoiseGitMerge - a Diff/Patch program // Copyright (C) 2006-2008 - TortoiseSVN -// Copyright (C) 2012 - Sven Strickroth +// Copyright (C) 2012-2013 - Sven Strickroth // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License @@ -61,7 +61,7 @@ protected: int CountDirMatches(const CString& path); CString RemoveUnicodeBOM(const CString& str); - BOOL ParserGitPatch(CFileTextLines &PatchLines,int nIndex); + BOOL ParsePatchFile(CFileTextLines &PatchLines); /** * Strips the filename by removing m_nStrip prefixes. @@ -99,6 +99,4 @@ protected: * stripped by 4 prefixes is interpreted as "dir/file.txt" */ int m_nStrip; -public: - bool m_IsGitPatch; }; diff --git a/src/TortoiseMerge/resource.h b/src/TortoiseMerge/resource.h dissimilarity index 99% index 0f5601ebe17a2ac1cde49ab4765893ed12d2b355..2a335c013edb1473fd904735fec923e705488880 100644 GIT binary patch literal 15651 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 G{{BA|hJNG# literal 31108 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