Teach import to understand git diff extensions.
authorBrendan Cully <brendan@kublai.com>
Fri, 11 Aug 2006 15:50:07 -0700
changeset 2860 b3d1145ed06c
parent 2854 7706fa503677
child 2861 0f08f2c042ec
Teach import to understand git diff extensions. Vanilla patch chokes on git patches that include files that are copied or renamed, then modified. So this code detects that case and rewrites the patch if necessary.
mercurial/commands.py
mercurial/util.py
--- a/mercurial/commands.py	Fri Aug 11 14:50:41 2006 -0700
+++ b/mercurial/commands.py	Fri Aug 11 15:50:07 2006 -0700
@@ -1825,6 +1825,7 @@
     wlock = repo.wlock()
     lock = repo.lock()
 
+    wlock = repo.wlock()
     for patch in patches:
         pf = os.path.join(d, patch)
 
@@ -1908,12 +1909,44 @@
                 raise util.Abort(_('no diffs found'))
 
             files = util.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)
--- a/mercurial/util.py	Fri Aug 11 14:50:41 2006 -0700
+++ b/mercurial/util.py	Fri Aug 11 15:50:07 2006 -0700
@@ -93,26 +93,162 @@
             return p_name
     return default
 
+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 Abort(_("cannot create %s: destination already exists") %
+                            p.path)
+
+            (src, dst) = [os.path.join(os.getcwd(), n)
+                          for n in (p.oldpath, p.path)]
+
+            print "copying %s to %s" % (src, dst)
+            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 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"""
-    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)))
+
+    (dopatch, gitpatches) = readgitpatch(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()
+    if dopatch:
+        if dopatch == 'filter':
+            patchname = dogitpatch(patchname, gitpatches)
+        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)))
+
+        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 = parse_patch_output(line)
+                files.setdefault(pf, (None, None))
+        code = fp.close()
+        if code:
+            raise Abort(_("patch command failed: %s") % explain_exit(code)[0])
+
+    for gp in gitpatches:
+        files[gp.path] = (gp.op, gp)
+
+    return files
 
 def binary(s):
     """return true if a string is binary data using diff's heuristic"""