changeset 14451:c78d41db6f88

patch: refactor file creation/removal detection The patcher has to know if a file is being created or removed to check if the target already exists, or to actually unlink the file when a hunk emptying it is applied. This was done by embedding the creation/removal information in the first (and only) hunk attached to the file. There are two problems with this approach: - creation/removal is really a property of the file being patched and not its hunk. - for regular patches, file creation cannot be deduced at parsing time: there are case where the *stripped* file paths must be compared. Modifying hunks after their creation is clumsy and prevent further refactorings related to copies handling. Instead, we delegate this job to selectfile() which has all the relevant information, and remove the hunk createfile() and rmfile() methods.
author Patrick Mezard <pmezard@gmail.com>
date Fri, 27 May 2011 21:50:09 +0200
parents d1a1578c5f78
children ee574cfd0c32
files hgext/keyword.py mercurial/patch.py
diffstat 2 files changed, 31 insertions(+), 47 deletions(-) [+]
line wrap: on
line diff
--- a/hgext/keyword.py	Fri May 27 15:59:52 2011 +0200
+++ b/hgext/keyword.py	Fri May 27 21:50:09 2011 +0200
@@ -595,11 +595,11 @@
                 wlock.release()
 
     # monkeypatches
-    def kwpatchfile_init(orig, self, ui, fname, backend, mode,
+    def kwpatchfile_init(orig, self, ui, fname, backend, mode, create, remove,
                          missing=False, eolmode=None):
         '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
         rejects or conflicts due to expanded keywords in working dir.'''
-        orig(self, ui, fname, backend, mode, missing, eolmode)
+        orig(self, ui, fname, backend, mode, create, remove, missing, eolmode)
         # shrink keywords read from working dir
         self.lines = kwt.shrinklines(self.fname, self.lines)
 
--- a/mercurial/patch.py	Fri May 27 15:59:52 2011 +0200
+++ b/mercurial/patch.py	Fri May 27 21:50:09 2011 +0200
@@ -504,7 +504,7 @@
 eolmodes = ['strict', 'crlf', 'lf', 'auto']
 
 class patchfile(object):
-    def __init__(self, ui, fname, backend, mode, missing=False,
+    def __init__(self, ui, fname, backend, mode, create, remove, missing=False,
                  eolmode='strict'):
         self.fname = fname
         self.eolmode = eolmode
@@ -515,6 +515,8 @@
         self.exists = False
         self.missing = missing
         self.mode = mode
+        self.create = create
+        self.remove = remove
         if not missing:
             try:
                 data, mode = self.backend.getfile(fname)
@@ -620,13 +622,13 @@
             self.rej.append(h)
             return -1
 
-        if self.exists and h.createfile():
+        if self.exists and self.create:
             self.ui.warn(_("file %s already exists\n") % self.fname)
             self.rej.append(h)
             return -1
 
         if isinstance(h, binhunk):
-            if h.rmfile():
+            if self.remove:
                 self.backend.unlink(self.fname)
             else:
                 self.lines[:] = h.new()
@@ -654,7 +656,7 @@
         # when the hunk cleanly applies at start + skew, so skip the
         # fast case code
         if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
-            if h.rmfile():
+            if self.remove:
                 self.backend.unlink(self.fname)
             else:
                 self.lines[start : start + h.lena] = h.new()
@@ -710,7 +712,7 @@
         return len(self.rej)
 
 class hunk(object):
-    def __init__(self, desc, num, lr, context, create=False, remove=False):
+    def __init__(self, desc, num, lr, context):
         self.number = num
         self.desc = desc
         self.hunk = [desc]
@@ -723,8 +725,6 @@
                 self.read_context_hunk(lr)
             else:
                 self.read_unified_hunk(lr)
-        self.create = create
-        self.remove = remove and not create
 
     def getnormalized(self):
         """Return a copy with line endings normalized to LF."""
@@ -738,7 +738,7 @@
             return nlines
 
         # Dummy object, it is rebuilt manually
-        nh = hunk(self.desc, self.number, None, None, False, False)
+        nh = hunk(self.desc, self.number, None, None)
         nh.number = self.number
         nh.desc = self.desc
         nh.hunk = self.hunk
@@ -748,8 +748,6 @@
         nh.startb = self.startb
         nh.lena = self.lena
         nh.lenb = self.lenb
-        nh.create = self.create
-        nh.remove = self.remove
         return nh
 
     def read_unified_hunk(self, lr):
@@ -891,12 +889,6 @@
     def complete(self):
         return len(self.a) == self.lena and len(self.b) == self.lenb
 
-    def createfile(self):
-        return self.starta == 0 and self.lena == 0 and self.create
-
-    def rmfile(self):
-        return self.startb == 0 and self.lenb == 0 and self.remove
-
     def fuzzit(self, l, fuzz, toponly):
         # this removes context lines from the top and bottom of list 'l'.  It
         # checks the hunk to make sure only context lines are removed, and then
@@ -942,18 +934,11 @@
 
 class binhunk:
     'A binary patch file. Only understands literals so far.'
-    def __init__(self, gitpatch, lr):
-        self.gitpatch = gitpatch
+    def __init__(self, lr):
         self.text = None
         self.hunk = ['GIT binary patch\n']
         self._read(lr)
 
-    def createfile(self):
-        return self.gitpatch.op == 'ADD'
-
-    def rmfile(self):
-        return self.gitpatch.op == 'DELETE'
-
     def complete(self):
         return self.text is not None
 
@@ -1020,10 +1005,14 @@
         # Git patches do not play games. Excluding copies from the
         # following heuristic avoids a lot of confusion
         fname = pathstrip(gp.path, strip - 1)[1]
-        missing = not hunk.createfile() and not backend.exists(fname)
-        return fname, missing
+        create = gp.op == 'ADD'
+        remove = gp.op == 'DELETE'
+        missing = not create and not backend.exists(fname)
+        return fname, missing, create, remove
     nulla = afile_orig == "/dev/null"
     nullb = bfile_orig == "/dev/null"
+    create = nulla and hunk.starta == 0 and hunk.lena == 0
+    remove = nullb and hunk.startb == 0 and hunk.lenb == 0
     abase, afile = pathstrip(afile_orig, strip)
     gooda = not nulla and backend.exists(afile)
     bbase, bfile = pathstrip(bfile_orig, strip)
@@ -1031,20 +1020,16 @@
         goodb = gooda
     else:
         goodb = not nullb and backend.exists(bfile)
-    createfunc = hunk.createfile
-    missing = not goodb and not gooda and not createfunc()
+    missing = not goodb and not gooda and not create
 
     # some diff programs apparently produce patches where the afile is
     # not /dev/null, but afile starts with bfile
     abasedir = afile[:afile.rfind('/') + 1]
     bbasedir = bfile[:bfile.rfind('/') + 1]
-    if missing and abasedir == bbasedir and afile.startswith(bfile):
-        # this isn't very pretty
-        hunk.create = True
-        if createfunc():
-            missing = False
-        else:
-            hunk.create = False
+    if (missing and abasedir == bbasedir and afile.startswith(bfile)
+        and hunk.starta == 0 and hunk.lena == 0):
+        create = True
+        missing = False
 
     # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
     # diff is between a file and its backup. In this case, the original
@@ -1065,7 +1050,7 @@
         else:
             raise PatchError(_("undefined source and destination files"))
 
-    return fname, missing
+    return fname, missing, create, remove
 
 def scangitpatch(lr, firstline):
     """
@@ -1125,13 +1110,11 @@
             if gitpatches and gitpatches[-1][0] == bfile:
                 gp = gitpatches.pop()[1]
             if x.startswith('GIT binary patch'):
-                h = binhunk(gp, lr)
+                h = binhunk(lr)
             else:
                 if context is None and x.startswith('***************'):
                     context = True
-                create = afile == '/dev/null' or gp and gp.op == 'ADD'
-                remove = bfile == '/dev/null' or gp and gp.op == 'DELETE'
-                h = hunk(x, hunknum + 1, lr, context, create, remove)
+                h = hunk(x, hunknum + 1, lr, context)
             hunknum += 1
             if emitfile:
                 emitfile = False
@@ -1250,10 +1233,11 @@
                 continue
             try:
                 mode = gp and gp.mode or None
-                current_file, missing = selectfile(backend, afile, bfile,
-                                                   first_hunk, strip, gp)
+                current_file, missing, create, remove = selectfile(
+                    backend, afile, bfile, first_hunk, strip, gp)
                 current_file = patcher(ui, current_file, backend, mode,
-                                       missing=missing, eolmode=eolmode)
+                                       create, remove, missing=missing,
+                                       eolmode=eolmode)
             except PatchError, inst:
                 ui.warn(str(inst) + '\n')
                 current_file = None
@@ -1386,8 +1370,8 @@
                         changed.add(pathstrip(gp.oldpath, strip - 1)[1])
                 if not first_hunk:
                     continue
-                current_file, missing = selectfile(backend, afile, bfile,
-                                                   first_hunk, strip, gp)
+                current_file, missing, create, remove = selectfile(
+                    backend, afile, bfile, first_hunk, strip, gp)
                 changed.add(current_file)
             elif state not in ('hunk', 'git'):
                 raise util.Abort(_('unsupported parser state: %s') % state)