merge with crew
authorBenoit Boissinot <benoit.boissinot@ens-lyon.org>
Sun, 19 Oct 2008 01:29:57 +0200
changeset 7156 ba897f6dc934
parent 7155 3250da71a769 (current diff)
parent 7153 353141d74ca8 (diff)
child 7159 a634e0065295
merge with crew
--- a/mercurial/patch.py	Sun Oct 19 01:28:35 2008 +0200
+++ b/mercurial/patch.py	Sun Oct 19 01:29:57 2008 +0200
@@ -143,23 +143,31 @@
 GP_FILTER = 1 << 1  # there's some copy/rename operation
 GP_BINARY = 1 << 2  # there's a binary patch
 
-def readgitpatch(fp, firstline=None):
+class patchmeta:
+    """Patched file metadata
+
+    'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
+    or COPY.  'path' is patched file path. 'oldpath' is set to the
+    origin file when 'op' is either COPY or RENAME, None otherwise. If
+    file mode is changed, 'mode' is a tuple (islink, isexec) where
+    'islink' is True if the file is a symlink and 'isexec' is True if
+    the file is executable. Otherwise, 'mode' is None.
+    """
+    def __init__(self, path):
+        self.path = path
+        self.oldpath = None
+        self.mode = None
+        self.op = 'MODIFY'
+        self.lineno = 0
+        self.binary = False
+
+    def setmode(self, mode):
+        islink = mode & 020000
+        isexec = mode & 0100
+        self.mode = (islink, isexec)
+
+def readgitpatch(lr):
     """extract git-style metadata about patches from <patchname>"""
-    class gitpatch:
-        "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
-        def __init__(self, path):
-            self.path = path
-            self.oldpath = None
-            self.mode = None
-            self.op = 'MODIFY'
-            self.lineno = 0
-            self.binary = False
-
-    def reader(fp, firstline):
-        if firstline is not None:
-            yield firstline
-        for line in fp:
-            yield line
 
     # Filter patch for git information
     gitre = re.compile('diff --git a/(.*) b/(.*)')
@@ -169,7 +177,7 @@
     dopatch = 0
 
     lineno = 0
-    for line in reader(fp, firstline):
+    for line in lr:
         lineno += 1
         if line.startswith('diff --git'):
             m = gitre.match(line)
@@ -177,7 +185,7 @@
                 if gp:
                     gitpatches.append(gp)
                 src, dst = m.group(1, 2)
-                gp = gitpatch(dst)
+                gp = patchmeta(dst)
                 gp.lineno = lineno
         elif gp:
             if line.startswith('--- '):
@@ -201,9 +209,9 @@
                 gp.op = 'DELETE'
             elif line.startswith('new file mode '):
                 gp.op = 'ADD'
-                gp.mode = int(line.rstrip()[-6:], 8)
+                gp.setmode(int(line.rstrip()[-6:], 8))
             elif line.startswith('new mode '):
-                gp.mode = int(line.rstrip()[-6:], 8)
+                gp.setmode(int(line.rstrip()[-6:], 8))
             elif line.startswith('GIT binary patch'):
                 dopatch |= GP_BINARY
                 gp.binary = True
@@ -215,88 +223,6 @@
 
     return (dopatch, gitpatches)
 
-def patch(patchname, ui, strip=1, cwd=None, files={}):
-    """apply <patchname> to the working directory.
-    returns whether patch was applied with fuzz factor."""
-    patcher = ui.config('ui', 'patch')
-    args = []
-    try:
-        if patcher:
-            return externalpatch(patcher, args, patchname, ui, strip, cwd,
-                                 files)
-        else:
-            try:
-                return internalpatch(patchname, ui, strip, cwd, files)
-            except NoHunks:
-                patcher = util.find_exe('gpatch') or util.find_exe('patch')
-                ui.debug(_('no valid hunks found; trying with %r instead\n') %
-                         patcher)
-                if util.needbinarypatch():
-                    args.append('--binary')
-                return externalpatch(patcher, args, patchname, ui, strip, cwd,
-                                     files)
-    except PatchError, err:
-        s = str(err)
-        if s:
-            raise util.Abort(s)
-        else:
-            raise util.Abort(_('patch failed to apply'))
-
-def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
-    """use <patcher> to apply <patchname> to the working directory.
-    returns whether patch was applied with fuzz factor."""
-
-    fuzz = False
-    if cwd:
-        args.append('-d %s' % util.shellquote(cwd))
-    fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
-                                       util.shellquote(patchname)))
-
-    for line in fp:
-        line = line.rstrip()
-        ui.note(line + '\n')
-        if line.startswith('patching file '):
-            pf = util.parse_patch_output(line)
-            printed_file = False
-            files.setdefault(pf, (None, None))
-        elif line.find('with fuzz') >= 0:
-            fuzz = True
-            if not printed_file:
-                ui.warn(pf + '\n')
-                printed_file = True
-            ui.warn(line + '\n')
-        elif line.find('saving rejects to file') >= 0:
-            ui.warn(line + '\n')
-        elif line.find('FAILED') >= 0:
-            if not printed_file:
-                ui.warn(pf + '\n')
-                printed_file = True
-            ui.warn(line + '\n')
-    code = fp.close()
-    if code:
-        raise PatchError(_("patch command failed: %s") %
-                         util.explain_exit(code)[0])
-    return fuzz
-
-def internalpatch(patchobj, ui, strip, cwd, files={}):
-    """use builtin patch to apply <patchobj> to the working directory.
-    returns whether patch was applied with fuzz factor."""
-    try:
-        fp = file(patchobj, 'rb')
-    except TypeError:
-        fp = patchobj
-    if cwd:
-        curdir = os.getcwd()
-        os.chdir(cwd)
-    try:
-        ret = applydiff(ui, fp, files, strip=strip)
-    finally:
-        if cwd:
-            os.chdir(curdir)
-    if ret < 0:
-        raise PatchError
-    return ret > 0
-
 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
@@ -529,7 +455,7 @@
             self.lenb = int(self.lenb)
         self.starta = int(self.starta)
         self.startb = int(self.startb)
-        diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
+        diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
         # if we hit eof before finishing out the hunk, the last line will
         # be zero length.  Lets try to fix it up.
         while len(self.hunk[-1]) == 0:
@@ -743,17 +669,17 @@
     def new(self):
         return [self.text]
 
-    def extract(self, fp):
-        line = fp.readline()
+    def extract(self, lr):
+        line = lr.readline()
         self.hunk.append(line)
         while line and not line.startswith('literal '):
-            line = fp.readline()
+            line = lr.readline()
             self.hunk.append(line)
         if not line:
             raise PatchError(_('could not extract binary patch'))
         size = int(line[8:].rstrip())
         dec = []
-        line = fp.readline()
+        line = lr.readline()
         self.hunk.append(line)
         while len(line) > 1:
             l = line[0]
@@ -762,7 +688,7 @@
             else:
                 l = ord(l) - ord('a') + 27
             dec.append(base85.b85decode(line[1:-1])[:l])
-            line = fp.readline()
+            line = lr.readline()
             self.hunk.append(line)
         text = zlib.decompress(''.join(dec))
         if len(text) != size:
@@ -839,7 +765,8 @@
         self.buf = []
 
     def push(self, line):
-        self.buf.append(line)
+        if line is not None:
+            self.buf.append(line)
 
     def readline(self):
         if self.buf:
@@ -848,6 +775,39 @@
             return l
         return self.fp.readline()
 
+    def __iter__(self):
+        while 1:
+            l = self.readline()
+            if not l:
+                break
+            yield l
+
+def scangitpatch(lr, firstline):
+    """        
+    Git patches can emit:
+    - rename a to b
+    - change b
+    - copy a to c
+    - change c
+        
+    We cannot apply this sequence as-is, the renamed 'a' could not be
+    found for it would have been renamed already. And we cannot copy
+    from 'b' instead because 'b' would have been changed already. So
+    we scan the git patch for copy and rename commands so we can
+    perform the copies ahead of time.
+    """
+    pos = 0
+    try:
+        pos = lr.fp.tell()
+        fp = lr.fp
+    except IOError:
+        fp = cStringIO.StringIO(lr.fp.read())
+    gitlr = linereader(fp)
+    gitlr.push(firstline)
+    (dopatch, gitpatches) = readgitpatch(gitlr)
+    fp.seek(pos)
+    return dopatch, gitpatches
+
 def iterhunks(ui, fp, sourcefile=None):
     """Read a patch and yield the following events:
     - ("file", afile, bfile, firsthunk): select a new target file.
@@ -856,24 +816,6 @@
     - ("git", gitchanges): current diff is in git format, gitchanges
     maps filenames to gitpatch records. Unique event.
     """
-
-    def scangitpatch(fp, firstline):
-        '''git patches can modify a file, then copy that file to
-        a new file, but expect the source to be the unmodified form.
-        So we scan the patch looking for that case so we can do
-        the copies ahead of time.'''
-
-        pos = 0
-        try:
-            pos = fp.tell()
-        except IOError:
-            fp = cStringIO.StringIO(fp.read())
-
-        (dopatch, gitpatches) = readgitpatch(fp, firstline)
-        fp.seek(pos)
-
-        return fp, dopatch, gitpatches
-
     changed = {}
     current_hunk = None
     afile = ""
@@ -911,7 +853,7 @@
             try:
                 if context == None and x.startswith('***************'):
                     context = True
-                gpatch = changed.get(bfile[2:], (None, None))[1]
+                gpatch = changed.get(bfile[2:])
                 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
                 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
                 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
@@ -924,12 +866,12 @@
                 emitfile = False
                 yield 'file', (afile, bfile, current_hunk)
         elif state == BFILE and x.startswith('GIT binary patch'):
-            current_hunk = binhunk(changed[bfile[2:]][1])
+            current_hunk = binhunk(changed[bfile[2:]])
             hunknum += 1
             if emitfile:
                 emitfile = False
                 yield 'file', (afile, bfile, current_hunk)
-            current_hunk.extract(fp)
+            current_hunk.extract(lr)
         elif x.startswith('diff --git'):
             # check for git diff, scanning the whole patch file if needed
             m = gitre.match(x)
@@ -937,14 +879,14 @@
                 afile, bfile = m.group(1, 2)
                 if not git:
                     git = True
-                    fp, dopatch, gitpatches = scangitpatch(fp, x)
+                    dopatch, gitpatches = scangitpatch(lr, x)
                     yield 'git', gitpatches
                     for gp in gitpatches:
-                        changed[gp.path] = (gp.op, gp)
+                        changed[gp.path] = gp
                 # else error?
                 # copy/rename + modify should modify target, not source
-                gitop = changed.get(bfile[2:], (None, None))[0]
-                if gitop in ('COPY', 'DELETE', 'RENAME'):
+                gp = changed.get(bfile[2:])
+                if gp and gp.op in ('COPY', 'DELETE', 'RENAME'):
                     afile = bfile
                     gitworkdone = True
             newfile = True
@@ -988,8 +930,7 @@
     if hunknum == 0 and dopatch and not gitworkdone:
         raise NoHunks
 
-def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
-              rejmerge=None, updatedir=None):
+def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False):
     """reads a patch from fp and tries to apply it.  The dict 'changed' is
        filled in with all of the filenames changed by the patch.  Returns 0
        for a clean patch, -1 if any rejects were found and 1 if there was
@@ -1004,8 +945,6 @@
         if not current_file:
             return 0
         current_file.close()
-        if rejmerge:
-            rejmerge(current_file)
         return len(current_file.rej)
 
     for state, values in iterhunks(ui, fp, sourcefile):
@@ -1015,7 +954,7 @@
             current_hunk = values
             ret = current_file.apply(current_hunk, reverse)
             if ret >= 0:
-                changed.setdefault(current_file.fname, (None, None))
+                changed.setdefault(current_file.fname, None)
                 if ret > 0:
                     err = 1
         elif state == 'file':
@@ -1041,14 +980,12 @@
                     src, dst = [util.canonpath(cwd, cwd, x)
                                 for x in [gp.oldpath, gp.path]]
                     copyfile(src, dst)
-                changed[gp.path] = (gp.op, gp)
+                changed[gp.path] = gp
         else:
             raise util.Abort(_('unsupported parser state: %s') % state)
 
     rejects += closefile()
 
-    if updatedir and gitpatches:
-        updatedir(gitpatches)
     if rejects:
         return -1
     return err
@@ -1078,13 +1015,15 @@
     if cwd:
         cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
     for f in patches:
-        ctype, gp = patches[f]
-        if ctype == 'RENAME':
+        gp = patches[f]
+        if not gp:
+            continue
+        if gp.op == 'RENAME':
             copies.append((gp.oldpath, gp.path))
             removes[gp.oldpath] = 1
-        elif ctype == 'COPY':
+        elif gp.op == 'COPY':
             copies.append((gp.oldpath, gp.path))
-        elif ctype == 'DELETE':
+        elif gp.op == 'DELETE':
             removes[gp.path] = 1
     for src, dst in copies:
         repo.copy(src, dst)
@@ -1092,24 +1031,103 @@
     if removes:
         repo.remove(util.sort(removes), True)
     for f in patches:
-        ctype, gp = patches[f]
+        gp = patches[f]
         if gp and gp.mode:
-            flags = ''
-            if gp.mode & 0100:
-                flags = 'x'
-            elif gp.mode & 020000:
-                flags = 'l'
+            islink, isexec = gp.mode
             dst = os.path.join(repo.root, gp.path)
             # patch won't create empty files
-            if ctype == 'ADD' and not os.path.exists(dst):
+            if gp.op == 'ADD' and not os.path.exists(dst):
+                flags = (isexec and 'x' or '') + (islink and 'l' or '')
                 repo.wwrite(gp.path, '', flags)
             else:
-                util.set_flags(dst, 'l' in flags, 'x' in flags)
+                util.set_flags(dst, islink, isexec)
     cmdutil.addremove(repo, cfiles)
     files = patches.keys()
     files.extend([r for r in removes if r not in files])
     return util.sort(files)
 
+def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
+    """use <patcher> to apply <patchname> to the working directory.
+    returns whether patch was applied with fuzz factor."""
+
+    fuzz = False
+    if cwd:
+        args.append('-d %s' % util.shellquote(cwd))
+    fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
+                                       util.shellquote(patchname)))
+
+    for line in fp:
+        line = line.rstrip()
+        ui.note(line + '\n')
+        if line.startswith('patching file '):
+            pf = util.parse_patch_output(line)
+            printed_file = False
+            files.setdefault(pf, (None, None))
+        elif line.find('with fuzz') >= 0:
+            fuzz = True
+            if not printed_file:
+                ui.warn(pf + '\n')
+                printed_file = True
+            ui.warn(line + '\n')
+        elif line.find('saving rejects to file') >= 0:
+            ui.warn(line + '\n')
+        elif line.find('FAILED') >= 0:
+            if not printed_file:
+                ui.warn(pf + '\n')
+                printed_file = True
+            ui.warn(line + '\n')
+    code = fp.close()
+    if code:
+        raise PatchError(_("patch command failed: %s") %
+                         util.explain_exit(code)[0])
+    return fuzz
+
+def internalpatch(patchobj, ui, strip, cwd, files={}):
+    """use builtin patch to apply <patchobj> to the working directory.
+    returns whether patch was applied with fuzz factor."""
+    try:
+        fp = file(patchobj, 'rb')
+    except TypeError:
+        fp = patchobj
+    if cwd:
+        curdir = os.getcwd()
+        os.chdir(cwd)
+    try:
+        ret = applydiff(ui, fp, files, strip=strip)
+    finally:
+        if cwd:
+            os.chdir(curdir)
+    if ret < 0:
+        raise PatchError
+    return ret > 0
+
+def patch(patchname, ui, strip=1, cwd=None, files={}):
+    """apply <patchname> to the working directory.
+    returns whether patch was applied with fuzz factor."""
+    patcher = ui.config('ui', 'patch')
+    args = []
+    try:
+        if patcher:
+            return externalpatch(patcher, args, patchname, ui, strip, cwd,
+                                 files)
+        else:
+            try:
+                return internalpatch(patchname, ui, strip, cwd, files)
+            except NoHunks:
+                patcher = util.find_exe('gpatch') or util.find_exe('patch')
+                ui.debug(_('no valid hunks found; trying with %r instead\n') %
+                         patcher)
+                if util.needbinarypatch():
+                    args.append('--binary')
+                return externalpatch(patcher, args, patchname, ui, strip, cwd,
+                                     files)
+    except PatchError, err:
+        s = str(err)
+        if s:
+            raise util.Abort(s)
+        else:
+            raise util.Abort(_('patch failed to apply'))
+
 def b85diff(to, tn):
     '''print base85-encoded binary diff'''
     def gitindex(text):