merge git patch code.
authorVadim Gelfer <vadim.gelfer@gmail.com>
Sat, 12 Aug 2006 12:47:18 -0700
changeset 2865 71e78f2ca5ae
parent 2859 345bac2bc4ec (current diff)
parent 2864 e2b69dbb2daa (diff)
child 2866 2893e51407a4
merge git patch code.
mercurial/commands.py
mercurial/patch.py
mercurial/util.py
--- a/mercurial/commands.py	Sat Aug 12 12:30:02 2006 -0700
+++ b/mercurial/commands.py	Sat Aug 12 12:47:18 2006 -0700
@@ -10,7 +10,7 @@
 from i18n import gettext as _
 demandload(globals(), "os re sys signal shutil imp urllib pdb")
 demandload(globals(), "fancyopts ui hg util lock revlog templater bundlerepo")
-demandload(globals(), "fnmatch mdiff random signal tempfile time")
+demandload(globals(), "fnmatch mdiff patch random signal tempfile time")
 demandload(globals(), "traceback errno socket version struct atexit sets bz2")
 demandload(globals(), "archival cStringIO changegroup email.Parser")
 demandload(globals(), "hgweb.server sshserver")
@@ -1825,21 +1825,21 @@
     wlock = repo.wlock()
     lock = repo.lock()
 
-    for patch in patches:
-        pf = os.path.join(d, patch)
+    for p in patches:
+        pf = os.path.join(d, p)
 
         message = None
         user = None
         date = None
         hgpatch = False
 
-        p = email.Parser.Parser()
+        parser = email.Parser.Parser()
         if pf == '-':
-            msg = p.parse(sys.stdin)
+            msg = parser.parse(sys.stdin)
             ui.status(_("applying patch from stdin\n"))
         else:
-            msg = p.parse(file(pf))
-            ui.status(_("applying %s\n") % patch)
+            msg = parser.parse(file(pf))
+            ui.status(_("applying %s\n") % p)
 
         fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
         tmpfp = os.fdopen(fd, 'w')
@@ -1907,13 +1907,45 @@
             if not diffs_seen:
                 raise util.Abort(_('no diffs found'))
 
-            files = util.patch(strip, tmpname, ui, cwd=repo.root)
+            files = patch.patch(strip, tmpname, ui, cwd=repo.root)
+            removes = []
             if len(files) > 0:
-                cfiles = files
+                cfiles = files.keys()
+                copies = []
+                copts = {'after': False, 'force': False}
                 cwd = repo.getcwd()
                 if cwd:
-                    cfiles = [util.pathto(cwd, f) for f in files]
+                    cfiles = [util.pathto(cwd, f) for f in files.keys()]
+                for f in files:
+                    ctype, gp = files[f]
+                    if ctype == 'RENAME':
+                        copies.append((gp.oldpath, gp.path, gp.copymod))
+                        removes.append(gp.oldpath)
+                    elif ctype == 'COPY':
+                        copies.append((gp.oldpath, gp.path, gp.copymod))
+                    elif ctype == 'DELETE':
+                        removes.append(gp.path)
+                for src, dst, after in copies:
+                    absdst = os.path.join(repo.root, dst)
+                    if not after and os.path.exists(absdst):
+                        raise util.Abort(_('patch creates existing file %s') % dst)
+                    if cwd:
+                        src, dst = [util.pathto(cwd, f) for f in (src, dst)]
+                    copts['after'] = after
+                    errs, copied = docopy(ui, repo, (src, dst), copts, wlock=wlock)
+                    if errs:
+                        raise util.Abort(errs)
+                if removes:
+                    repo.remove(removes, True, wlock=wlock)
+                for f in files:
+                    ctype, gp = files[f]
+                    if gp and gp.mode:
+                        x = gp.mode & 0100 != 0
+                        dst = os.path.join(repo.root, gp.path)
+                        util.set_exec(dst, x)
                 addremove_lock(ui, repo, cfiles, {}, wlock=wlock)
+            files = files.keys()
+            files.extend([r for r in removes if r not in files])
             repo.commit(files, message, user, date, wlock=wlock, lock=lock)
         finally:
             os.unlink(tmpname)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/patch.py	Sat Aug 12 12:47:18 2006 -0700
@@ -0,0 +1,166 @@
+# patch.py - patch file parsing routines
+#
+# Copyright 2006 Brendan Cully <brendan@kublai.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+from demandload import demandload
+demandload(globals(), "util")
+demandload(globals(), "os re shutil tempfile")
+
+def readgitpatch(patchname):
+    """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.copymod = False
+            self.lineno = 0
+    
+    # Filter patch for git information
+    gitre = re.compile('diff --git a/(.*) b/(.*)')
+    pf = file(patchname)
+    gp = None
+    gitpatches = []
+    # Can have a git patch with only metadata, causing patch to complain
+    dopatch = False
+
+    lineno = 0
+    for line in pf:
+        lineno += 1
+        if line.startswith('diff --git'):
+            m = gitre.match(line)
+            if m:
+                if gp:
+                    gitpatches.append(gp)
+                src, dst = m.group(1,2)
+                gp = gitpatch(dst)
+                gp.lineno = lineno
+        elif gp:
+            if line.startswith('--- '):
+                if gp.op in ('COPY', 'RENAME'):
+                    gp.copymod = True
+                    dopatch = 'filter'
+                gitpatches.append(gp)
+                gp = None
+                if not dopatch:
+                    dopatch = True
+                continue
+            if line.startswith('rename from '):
+                gp.op = 'RENAME'
+                gp.oldpath = line[12:].rstrip()
+            elif line.startswith('rename to '):
+                gp.path = line[10:].rstrip()
+            elif line.startswith('copy from '):
+                gp.op = 'COPY'
+                gp.oldpath = line[10:].rstrip()
+            elif line.startswith('copy to '):
+                gp.path = line[8:].rstrip()
+            elif line.startswith('deleted file'):
+                gp.op = 'DELETE'
+            elif line.startswith('new file mode '):
+                gp.op = 'ADD'
+                gp.mode = int(line.rstrip()[-3:], 8)
+            elif line.startswith('new mode '):
+                gp.mode = int(line.rstrip()[-3:], 8)
+    if gp:
+        gitpatches.append(gp)
+
+    if not gitpatches:
+        dopatch = True
+
+    return (dopatch, gitpatches)
+
+def dogitpatch(patchname, gitpatches):
+    """Preprocess git patch so that vanilla patch can handle it"""
+    pf = file(patchname)
+    pfline = 1
+
+    fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
+    tmpfp = os.fdopen(fd, 'w')
+
+    try:
+        for i in range(len(gitpatches)):
+            p = gitpatches[i]
+            if not p.copymod:
+                continue
+
+            if os.path.exists(p.path):
+                raise util.Abort(_("cannot create %s: destination already exists") %
+                            p.path)
+
+            (src, dst) = [os.path.join(os.getcwd(), n)
+                          for n in (p.oldpath, p.path)]
+
+            targetdir = os.path.dirname(dst)
+            if not os.path.isdir(targetdir):
+                os.makedirs(targetdir)
+            try:
+                shutil.copyfile(src, dst)
+                shutil.copymode(src, dst)
+            except shutil.Error, inst:
+                raise util.Abort(str(inst))
+
+            # rewrite patch hunk
+            while pfline < p.lineno:
+                tmpfp.write(pf.readline())
+                pfline += 1
+            tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
+            line = pf.readline()
+            pfline += 1
+            while not line.startswith('--- a/'):
+                tmpfp.write(line)
+                line = pf.readline()
+                pfline += 1
+            tmpfp.write('--- a/%s\n' % p.path)
+
+        line = pf.readline()
+        while line:
+            tmpfp.write(line)
+            line = pf.readline()
+    except:
+        tmpfp.close()
+        os.unlink(patchname)
+        raise
+
+    tmpfp.close()
+    return patchname
+
+def patch(strip, patchname, ui, cwd=None):
+    """apply the patch <patchname> to the working directory.
+    a list of patched files is returned"""
+
+    (dopatch, gitpatches) = readgitpatch(patchname)
+
+    files = {}
+    if dopatch:
+        if dopatch == 'filter':
+            patchname = dogitpatch(patchname, gitpatches)
+        patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
+        args = []
+        if cwd:
+            args.append('-d %s' % util.shellquote(cwd))
+        fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
+                                           util.shellquote(patchname)))
+
+        if dopatch == 'filter':
+            False and os.unlink(patchname)
+
+        for line in fp:
+            line = line.rstrip()
+            ui.status("%s\n" % line)
+            if line.startswith('patching file '):
+                pf = util.parse_patch_output(line)
+                files.setdefault(pf, (None, None))
+        code = fp.close()
+        if code:
+            raise util.Abort(_("patch command failed: %s") % explain_exit(code)[0])
+
+    for gp in gitpatches:
+        files[gp.path] = (gp.op, gp)
+
+    return files
--- a/mercurial/util.py	Sat Aug 12 12:30:02 2006 -0700
+++ b/mercurial/util.py	Sat Aug 12 12:47:18 2006 -0700
@@ -95,27 +95,6 @@
             return p_name
     return default
 
-def patch(strip, patchname, ui, cwd=None):
-    """apply the patch <patchname> to the working directory.
-    a list of patched files is returned"""
-    patcher = find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
-    args = []
-    if cwd:
-        args.append('-d %s' % shellquote(cwd))
-    fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
-                                       shellquote(patchname)))
-    files = {}
-    for line in fp:
-        line = line.rstrip()
-        ui.status("%s\n" % line)
-        if line.startswith('patching file '):
-            pf = parse_patch_output(line)
-            files.setdefault(pf, 1)
-    code = fp.close()
-    if code:
-        raise Abort(_("patch command failed: %s") % explain_exit(code)[0])
-    return files.keys()
-
 def binary(s):
     """return true if a string is binary data using diff's heuristic"""
     if s and '\0' in s[:4096]:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-git-import	Sat Aug 12 12:47:18 2006 -0700
@@ -0,0 +1,122 @@
+#!/bin/sh
+
+hg init a
+cd a
+
+echo % new file
+hg import -mnew - <<EOF
+diff --git a/new b/new
+new file mode 100644
+index 0000000..7898192
+--- /dev/null
++++ b/new
+@@ -0,0 +1 @@
++a
+EOF
+
+echo % chmod +x
+hg import -msetx - <<EOF
+diff --git a/new b/new
+old mode 100644
+new mode 100755
+EOF
+
+test -x new || echo failed
+
+echo % copy
+hg import -mcopy - <<EOF
+diff --git a/new b/copy
+old mode 100755
+new mode 100644
+similarity index 100%
+copy from new
+copy to copy
+diff --git a/new b/copyx
+similarity index 100%
+copy from new
+copy to copyx
+EOF
+
+test -f copy -a ! -x copy || echo failed
+test -x copyx || echo failed
+cat copy
+hg cat copy
+
+echo % rename
+hg import -mrename - <<EOF
+diff --git a/copy b/rename
+similarity index 100%
+rename from copy
+rename to rename
+EOF
+
+hg locate
+
+echo % delete
+hg import -mdelete - <<EOF
+diff --git a/copyx b/copyx
+deleted file mode 100755
+index 7898192..0000000
+--- a/copyx
++++ /dev/null
+@@ -1 +0,0 @@
+-a
+EOF
+
+hg locate
+test -f copyx && echo failed || true
+
+echo % regular diff
+hg import -mregular - <<EOF
+diff --git a/rename b/rename
+index 7898192..72e1fe3 100644
+--- a/rename
++++ b/rename
+@@ -1 +1,5 @@
+ a
++a
++a
++a
++a
+EOF
+
+echo % copy and modify
+hg import -mcopymod - <<EOF
+diff --git a/rename b/copy2
+similarity index 80%
+copy from rename
+copy to copy2
+index 72e1fe3..b53c148 100644
+--- a/rename
++++ b/copy2
+@@ -1,5 +1,5 @@
+ a
+ a
+-a
++b
+ a
+ a
+EOF
+
+hg cat copy2
+
+echo % rename and modify
+hg import -mrenamemod - <<EOF
+diff --git a/copy2 b/rename2
+similarity index 80%
+rename from copy2
+rename to rename2
+index b53c148..8f81e29 100644
+--- a/copy2
++++ b/rename2
+@@ -1,5 +1,5 @@
+ a
+ a
+ b
+-a
++c
+ a
+EOF
+
+hg locate copy2
+hg cat rename2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-git-import.out	Sat Aug 12 12:47:18 2006 -0700
@@ -0,0 +1,39 @@
+% new file
+applying patch from stdin
+patching file new
+% chmod +x
+applying patch from stdin
+% copy
+applying patch from stdin
+a
+a
+% rename
+applying patch from stdin
+copyx
+new
+rename
+% delete
+applying patch from stdin
+patching file copyx
+new
+rename
+% regular diff
+applying patch from stdin
+patching file rename
+% copy and modify
+applying patch from stdin
+patching file copy2
+a
+a
+b
+a
+a
+% rename and modify
+applying patch from stdin
+patching file rename2
+copy2: No such file or directory
+a
+a
+b
+c
+a