changeset 4897:4574925db5c0

Add Chris Mason's mpatch library. The original repo is http://oss.oracle.com/mercurial/mason/mpatch
author Bryan O'Sullivan <bos@serpentine.com>
date Tue, 17 Jul 2007 09:39:30 -0700
parents ee04732fe61d
children bc905a6c0e76
files mercurial/diffhelpers.c mercurial/patch.py setup.py
diffstat 3 files changed, 864 insertions(+), 137 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/diffhelpers.c	Tue Jul 17 09:39:30 2007 -0700
@@ -0,0 +1,150 @@
+/*
+ * diffhelpers.c - helper routines for mpatch
+ *
+ * Copyright 2007 Chris Mason <chris.mason@oracle.com>
+ *
+ * This software may be used and distributed according to the terms
+ * of the GNU General Public License v2, incorporated herein by reference.
+ */
+
+#include <Python.h>
+#include <stdlib.h>
+#include <string.h>
+
+static char diffhelpers_doc[] = "Efficient diff parsing";
+static PyObject *diffhelpers_Error;
+
+
+/* fixup the last lines of a and b when the patch has no newline at eof */
+static void _fix_newline(PyObject *hunk, PyObject *a, PyObject *b)
+{
+	int hunksz = PyList_Size(hunk);
+	PyObject *s = PyList_GET_ITEM(hunk, hunksz-1);
+	char *l = PyString_AS_STRING(s);
+	int sz = PyString_GET_SIZE(s);
+	int alen = PyList_Size(a);
+	int blen = PyList_Size(b);
+	char c = l[0];
+
+	PyObject *hline = PyString_FromStringAndSize(l, sz-1);
+	if (c == ' ' || c == '+') {
+		PyObject *rline = PyString_FromStringAndSize(l+1, sz-2);
+		PyList_SetItem(b, blen-1, rline);
+	}
+	if (c == ' ' || c == '-') {
+		Py_INCREF(hline);
+		PyList_SetItem(a, alen-1, hline);
+	}
+	PyList_SetItem(hunk, hunksz-1, hline);
+}
+
+/* python callable form of _fix_newline */
+static PyObject *
+fix_newline(PyObject *self, PyObject *args)
+{
+	PyObject *hunk, *a, *b;
+	if (!PyArg_ParseTuple(args, "OOO", &hunk, &a, &b))
+		return NULL;
+	_fix_newline(hunk, a, b);
+	return Py_BuildValue("l", 0);
+}
+
+/*
+ * read lines from fp into the hunk.  The hunk is parsed into two arrays
+ * a and b.  a gets the old state of the text, b gets the new state
+ * The control char from the hunk is saved when inserting into a, but not b
+ * (for performance while deleting files)
+ */
+static PyObject *
+addlines(PyObject *self, PyObject *args)
+{
+
+	PyObject *fp, *hunk, *a, *b, *x;
+	int i;
+	int lena, lenb;
+	int num;
+	int todoa, todob;
+	char *s, c;
+	PyObject *l;
+	if (!PyArg_ParseTuple(args, "OOiiOO", &fp, &hunk, &lena, &lenb, &a, &b))
+		return NULL;
+
+	while(1) {
+		todoa = lena - PyList_Size(a);
+		todob = lenb - PyList_Size(b);
+		num = todoa > todob ? todoa : todob;
+		if (num == 0)
+		    break;
+		for (i = 0 ; i < num ; i++) {
+			x = PyFile_GetLine(fp, 0);
+			s = PyString_AS_STRING(x);
+			c = *s;
+			if (strcmp(s, "\\ No newline at end of file\n") == 0) {
+				_fix_newline(hunk, a, b);
+				continue;
+			}
+			PyList_Append(hunk, x);
+			if (c == '+') {
+				l = PyString_FromString(s + 1);
+				PyList_Append(b, l);
+				Py_DECREF(l);
+			} else if (c == '-') {
+				PyList_Append(a, x);
+			} else {
+				l = PyString_FromString(s + 1);
+				PyList_Append(b, l);
+				Py_DECREF(l);
+				PyList_Append(a, x);
+			}
+			Py_DECREF(x);
+		}
+	}
+	return Py_BuildValue("l", 0);
+}
+
+/*
+ * compare the lines in a with the lines in b.  a is assumed to have
+ * a control char at the start of each line, this char is ignored in the
+ * compare
+ */
+static PyObject *
+testhunk(PyObject *self, PyObject *args)
+{
+
+	PyObject *a, *b;
+	long bstart;
+	int alen, blen;
+	int i;
+	char *sa, *sb;
+
+	if (!PyArg_ParseTuple(args, "OOl", &a, &b, &bstart))
+		return NULL;
+	alen = PyList_Size(a);
+	blen = PyList_Size(b);
+	if (alen > blen - bstart) {
+		return Py_BuildValue("l", -1);
+	}
+	for (i = 0 ; i < alen ; i++) {
+		sa = PyString_AS_STRING(PyList_GET_ITEM(a, i));
+		sb = PyString_AS_STRING(PyList_GET_ITEM(b, i + bstart));
+		if (strcmp(sa+1, sb) != 0)
+			return Py_BuildValue("l", -1);
+	}
+	return Py_BuildValue("l", 0);
+}
+
+static PyMethodDef methods[] = {
+	{"addlines", addlines, METH_VARARGS, "add lines to a hunk\n"},
+	{"fix_newline", fix_newline, METH_VARARGS, "fixup newline counters\n"},
+	{"testhunk", testhunk, METH_VARARGS, "test lines in a hunk\n"},
+	{NULL, NULL}
+};
+
+PyMODINIT_FUNC
+initdiffhelpers(void)
+{
+	Py_InitModule3("diffhelpers", methods, diffhelpers_doc);
+	diffhelpers_Error = PyErr_NewException("diffhelpers.diffhelpersError",
+	                                        NULL, NULL);
+}
+
--- a/mercurial/patch.py	Tue Jul 17 09:28:01 2007 -0300
+++ b/mercurial/patch.py	Tue Jul 17 09:39:30 2007 -0700
@@ -1,16 +1,20 @@
 # patch.py - patch file parsing routines
 #
 # Copyright 2006 Brendan Cully <brendan@kublai.com>
+# Copyright 2007 Chris Mason <chris.mason@oracle.com>
 #
 # This software may be used and distributed according to the terms
 # of the GNU General Public License, incorporated herein by reference.
 
 from i18n import _
 from node import *
-import base85, cmdutil, mdiff, util, context, revlog
+import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
 import cStringIO, email.Parser, os, popen2, re, sha
 import sys, tempfile, zlib
 
+class PatchError(Exception):
+    pass
+
 # helper functions
 
 def copyfile(src, dst, basedir=None):
@@ -135,7 +139,7 @@
 GP_FILTER = 1 << 1  # there's some copy/rename operation
 GP_BINARY = 1 << 2  # there's a binary patch
 
-def readgitpatch(patchname):
+def readgitpatch(fp, firstline):
     """extract git-style metadata about patches from <patchname>"""
     class gitpatch:
         "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
@@ -148,16 +152,20 @@
             self.lineno = 0
             self.binary = False
 
+    def reader(fp, firstline):
+        yield firstline
+        for line in fp:
+            yield line
+
     # 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 = 0
 
     lineno = 0
-    for line in pf:
+    for line in reader(fp, firstline):
         lineno += 1
         if line.startswith('diff --git'):
             m = gitre.match(line)
@@ -204,157 +212,725 @@
 
     return (dopatch, gitpatches)
 
-def dogitpatch(patchname, gitpatches, cwd=None):
-    """Preprocess git patch so that vanilla patch can handle it"""
-    def extractbin(fp):
-        i = [0] # yuck
-        def readline():
-            i[0] += 1
-            return fp.readline().rstrip()
-        line = readline()
+def patch(patchname, ui, strip=1, cwd=None, files={}):
+    """apply the patch <patchname> to the working directory.
+    a list of patched files is returned"""
+    fp = file(patchname)
+    fuzz = False
+    if cwd:
+        curdir = os.getcwd()
+        os.chdir(cwd)
+    try:
+        ret = applydiff(ui, fp, files, strip=strip)
+    except PatchError:
+        raise util.Abort(_("patch failed to apply"))
+    if cwd:
+        os.chdir(curdir)
+    if ret < 0:
+        raise util.Abort(_("patch failed to apply"))
+    if ret > 0:
+        fuzz = True
+    return fuzz
+
+# @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
+unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
+contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
+
+class patchfile:
+    def __init__(self, ui, fname):
+        self.fname = fname
+        self.ui = ui
+        try:
+            fp = file(fname, 'r')
+            self.lines = fp.readlines()
+            self.exists = True
+        except IOError:
+            dirname = os.path.dirname(fname)
+            if dirname and not os.path.isdir(dirname):
+                dirs = dirname.split(os.path.sep)
+                d = ""
+                for x in dirs:
+                    d = os.path.join(d, x)
+                    if not os.path.isdir(d):
+                        os.mkdir(d)
+            self.lines = []
+            self.exists = False
+            
+        self.hash = {}
+        self.dirty = 0
+        self.offset = 0
+        self.rej = []
+        self.fileprinted = False
+        self.printfile(False)
+        self.hunks = 0
+
+    def printfile(self, warn):
+        if self.fileprinted:
+            return
+        if warn or self.ui.verbose:
+            self.fileprinted = True
+        s = _("patching file %s\n" % self.fname)
+        if warn:
+            self.ui.warn(s)
+        else:
+            self.ui.note(s)
+
+
+    def findlines(self, l, linenum):
+        # looks through the hash and finds candidate lines.  The
+        # result is a list of line numbers sorted based on distance
+        # from linenum
+        def sorter(a, b):
+            vala = abs(a - linenum)
+            valb = abs(b - linenum)
+            return cmp(vala, valb)
+            
+        try:
+            cand = self.hash[l]
+        except:
+            return []
+
+        if len(cand) > 1:
+            # resort our list of potentials forward then back.
+            cand.sort(cmp=sorter)
+        return cand
+
+    def hashlines(self):
+        self.hash = {}
+        for x in xrange(len(self.lines)):
+            s = self.lines[x]
+            self.hash.setdefault(s, []).append(x)
+
+    def write_rej(self):
+        # our rejects are a little different from patch(1).  This always
+        # creates rejects in the same form as the original patch.  A file
+        # header is inserted so that you can run the reject through patch again
+        # without having to type the filename.
+
+        if not self.rej:
+            return
+        if self.hunks != 1:
+            hunkstr = "s"
+        else:
+            hunkstr = ""
+
+        fname = self.fname + ".rej"
+        self.ui.warn(
+                 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n" %
+                 (len(self.rej), self.hunks, hunkstr, fname)))
+        try: os.unlink(fname)
+        except:
+            pass
+        fp = file(fname, 'w')
+        base = os.path.basename(self.fname)
+        fp.write("--- %s\n+++ %s\n" % (base, base))
+        for x in self.rej:
+            for l in x.hunk:
+                fp.write(l)
+                if l[-1] != '\n':
+                    fp.write("\n\ No newline at end of file\n")
+
+    def write(self, dest=None):
+        if self.dirty:
+            if not dest:
+                dest = self.fname
+            st = None
+            try:
+                st = os.lstat(dest)
+                if st.st_nlink > 1:
+                    os.unlink(dest)
+            except: pass
+            fp = file(dest, 'w')
+            if st:
+                os.chmod(dest, st.st_mode)
+            fp.writelines(self.lines)
+            fp.close()
+
+    def close(self):
+        self.write()
+        self.write_rej()
+
+    def apply(self, h, reverse):
+        if not h.complete():
+            raise PatchError("bad hunk #%d %s (%d %d %d %d)" %
+                            (h.number, h.desc, len(h.a), h.lena, len(h.b),
+                            h.lenb))
+
+        self.hunks += 1
+        if reverse:
+            h.reverse()
+
+        if self.exists and h.createfile():
+            self.ui.warn(_("file %s already exists\n" % self.fname))
+            self.rej.append(h)
+            return -1
+
+        if isinstance(h, binhunk):
+            if h.rmfile():
+                os.unlink(self.fname)
+            else:
+                self.lines[:] = h.new()
+                self.offset += len(h.new())
+                self.dirty = 1
+            return 0
+
+        # fast case first, no offsets, no fuzz
+        old = h.old()
+        # patch starts counting at 1 unless we are adding the file
+        if h.starta == 0:
+            start = 0
+        else:
+            start = h.starta + self.offset - 1
+        orig_start = start
+        if diffhelpers.testhunk(old, self.lines, start) == 0:
+            if h.rmfile():
+                os.unlink(self.fname)
+            else:
+                self.lines[start : start + h.lena] = h.new()
+                self.offset += h.lenb - h.lena
+                self.dirty = 1
+            return 0
+
+        # ok, we couldn't match the hunk.  Lets look for offsets and fuzz it
+        self.hashlines()
+        if h.hunk[-1][0] != ' ':
+            # if the hunk tried to put something at the bottom of the file
+            # override the start line and use eof here
+            search_start = len(self.lines)
+        else:
+            search_start = orig_start
+
+        for fuzzlen in xrange(3):
+            for toponly in [ True, False ]:
+                old = h.old(fuzzlen, toponly)
+
+                cand = self.findlines(old[0][1:], search_start)
+                for l in cand:
+                    if diffhelpers.testhunk(old, self.lines, l) == 0:
+                        newlines = h.new(fuzzlen, toponly)
+                        self.lines[l : l + len(old)] = newlines
+                        self.offset += len(newlines) - len(old)
+                        self.dirty = 1
+                        if fuzzlen:
+                            fuzzstr = "with fuzz %d " % fuzzlen
+                            f = self.ui.warn
+                            self.printfile(True)
+                        else:
+                            fuzzstr = ""
+                            f = self.ui.note
+                        offset = l - orig_start - fuzzlen
+                        if offset == 1:
+                            linestr = "line"
+                        else:
+                            linestr = "lines"
+                        f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n" %
+                           (h.number, l+1, fuzzstr, offset, linestr)))
+                        return fuzzlen
+        self.printfile(True)
+        self.ui.warn(_("Hunk #%d FAILED at %d\n" % (h.number, orig_start)))
+        self.rej.append(h)
+        return -1
+
+class hunk:
+    def __init__(self, desc, num, lr, context):
+        self.number = num
+        self.desc = desc
+        self.hunk = [ desc ]
+        self.a = []
+        self.b = []
+        if context:
+            self.read_context_hunk(lr)
+        else:
+            self.read_unified_hunk(lr)
+
+    def read_unified_hunk(self, lr):
+        m = unidesc.match(self.desc)
+        if not m:
+            raise PatchError("bad hunk #%d" % self.number)
+        self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
+        if self.lena == None:
+            self.lena = 1
+        else:
+            self.lena = int(self.lena)
+        if self.lenb == None:
+            self.lenb = 1
+        else:
+            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)
+        # 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:
+                del self.hunk[-1]
+                del self.a[-1]
+                del self.b[-1]
+                self.lena -= 1
+                self.lenb -= 1
+
+    def read_context_hunk(self, lr):
+        self.desc = lr.readline()
+        m = contextdesc.match(self.desc)
+        if not m:
+            raise PatchError("bad hunk #%d" % self.number)
+        foo, self.starta, foo2, aend, foo3 = m.groups()
+        self.starta = int(self.starta)
+        if aend == None:
+            aend = self.starta
+        self.lena = int(aend) - self.starta
+        if self.starta:
+            self.lena += 1
+        for x in xrange(self.lena):
+            l = lr.readline()
+            if l.startswith('---'):
+                lr.push(l)
+                break
+            s = l[2:]
+            if l.startswith('- ') or l.startswith('! '):
+                u = '-' + s
+            elif l.startswith('  '):
+                u = ' ' + s
+            else:
+                raise PatchError("bad hunk #%d old text line %d" % (self.number, x))
+            self.a.append(u)
+            self.hunk.append(u)
+
+        l = lr.readline()
+        if l.startswith('\ '):
+            s = self.a[-1][:-1]
+            self.a[-1] = s
+            self.hunk[-1] = s
+            l = lr.readline()
+        m = contextdesc.match(l)
+        if not m:
+            raise PatchError("bad hunk #%d" % self.number)
+        foo, self.startb, foo2, bend, foo3 = m.groups()
+        self.startb = int(self.startb)
+        if bend == None:
+            bend = self.startb
+        self.lenb = int(bend) - self.startb
+        if self.startb:
+            self.lenb += 1
+        hunki = 1
+        for x in xrange(self.lenb):
+            l = lr.readline()
+            if l.startswith('\ '):
+                s = self.b[-1][:-1]
+                self.b[-1] = s
+                self.hunk[hunki-1] = s
+                continue
+            if not l:
+                lr.push(l)
+                break
+            s = l[2:]
+            if l.startswith('+ ') or l.startswith('! '):
+                u = '+' + s
+            elif l.startswith('  '):
+                u = ' ' + s
+            elif len(self.b) == 0:
+                # this can happen when the hunk does not add any lines
+                lr.push(l)
+                break
+            else:
+                raise PatchError("bad hunk #%d old text line %d" % (self.number, x))
+            self.b.append(s)
+            while True:
+                if hunki >= len(self.hunk):
+                    h = ""
+                else:
+                    h = self.hunk[hunki]
+                hunki += 1
+                if h == u:
+                    break
+                elif h.startswith('-'):
+                    continue
+                else:
+                    self.hunk.insert(hunki-1, u)
+                    break
+
+        if not self.a:
+            # this happens when lines were only added to the hunk
+            for x in self.hunk:
+                if x.startswith('-') or x.startswith(' '):
+                    self.a.append(x)
+        if not self.b:
+            # this happens when lines were only deleted from the hunk
+            for x in self.hunk:
+                if x.startswith('+') or x.startswith(' '):
+                    self.b.append(x[1:])
+        # @@ -start,len +start,len @@
+        self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
+                                             self.startb, self.lenb)
+        self.hunk[0] = self.desc
+
+    def reverse(self):
+        origlena = self.lena
+        origstarta = self.starta
+        self.lena = self.lenb
+        self.starta = self.startb
+        self.lenb = origlena
+        self.startb = origstarta
+        self.a = []
+        self.b = []
+        # self.hunk[0] is the @@ description
+        for x in xrange(1, len(self.hunk)):
+            o = self.hunk[x]
+            if o.startswith('-'):
+                n = '+' + o[1:]
+                self.b.append(o[1:])
+            elif o.startswith('+'):
+                n = '-' + o[1:]
+                self.a.append(n)
+            else:
+                n = o
+                self.b.append(o[1:])
+                self.a.append(o)
+            self.hunk[x] = o
+
+    def fix_newline(self):
+        diffhelpers.fix_newline(self.hunk, self.a, self.b)
+
+    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
+
+    def rmfile(self):
+        return self.startb == 0 and self.lenb == 0
+
+    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
+        # returns a new shortened list of lines.
+        fuzz = min(fuzz, len(l)-1)
+        if fuzz:
+            top = 0
+            bot = 0
+            hlen = len(self.hunk)
+            for x in xrange(hlen-1):
+                # the hunk starts with the @@ line, so use x+1
+                if self.hunk[x+1][0] == ' ':
+                    top += 1
+                else:
+                    break
+            if not toponly:
+                for x in xrange(hlen-1):
+                    if self.hunk[hlen-bot-1][0] == ' ':
+                        bot += 1
+                    else:
+                        break
+
+            # top and bot now count context in the hunk
+            # adjust them if either one is short
+            context = max(top, bot, 3)
+            if bot < context:
+                bot = max(0, fuzz - (context - bot))
+            else:
+                bot = min(fuzz, bot)
+            if top < context:
+                top = max(0, fuzz - (context - top))
+            else:
+                top = min(fuzz, top)
+
+            return l[top:len(l)-bot]
+        return l
+
+    def old(self, fuzz=0, toponly=False):
+        return self.fuzzit(self.a, fuzz, toponly)
+        
+    def newctrl(self):
+        res = []
+        for x in self.hunk:
+            c = x[0]
+            if c == ' ' or c == '+':
+                res.append(x)
+        return res
+
+    def new(self, fuzz=0, toponly=False):
+        return self.fuzzit(self.b, fuzz, toponly)
+
+class binhunk:
+    'A binary patch file. Only understands literals so far.'
+    def __init__(self, gitpatch):
+        self.gitpatch = gitpatch
+        self.text = None
+        self.hunk = ['GIT binary patch\n']
+
+    def createfile(self):
+        return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
+
+    def rmfile(self):
+        return self.gitpatch.op == 'DELETE'
+
+    def complete(self):
+        return self.text is not None
+
+    def new(self):
+        return [self.text]
+
+    def extract(self, fp):
+        line = fp.readline()
+        self.hunk.append(line)
         while line and not line.startswith('literal '):
-            line = readline()
+            line = fp.readline()
+            self.hunk.append(line)
         if not line:
-            return None, i[0]
-        size = int(line[8:])
+            raise PatchError('could not extract binary patch')
+        size = int(line[8:].rstrip())
         dec = []
-        line = readline()
-        while line:
+        line = fp.readline()
+        self.hunk.append(line)
+        while len(line) > 1:
             l = line[0]
             if l <= 'Z' and l >= 'A':
                 l = ord(l) - ord('A') + 1
             else:
                 l = ord(l) - ord('a') + 27
-            dec.append(base85.b85decode(line[1:])[:l])
-            line = readline()
+            dec.append(base85.b85decode(line[1:-1])[:l])
+            line = fp.readline()
+            self.hunk.append(line)
         text = zlib.decompress(''.join(dec))
         if len(text) != size:
-            raise util.Abort(_('binary patch is %d bytes, not %d') %
-                             (len(text), size))
-        return text, i[0]
+            raise PatchError('binary patch is %d bytes, not %d' %
+                             len(text), size)
+        self.text = text
 
-    pf = file(patchname)
-    pfline = 1
-
-    fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
-    tmpfp = os.fdopen(fd, 'w')
+def parsefilename(str):
+    # --- filename \t|space stuff
+    s = str[4:]
+    i = s.find('\t')
+    if i < 0:
+        i = s.find(' ')
+        if i < 0:
+            return s
+    return s[:i]
 
-    try:
-        for i in xrange(len(gitpatches)):
-            p = gitpatches[i]
-            if not p.copymod and not p.binary:
-                continue
-
-            # rewrite patch hunk
-            while pfline < p.lineno:
-                tmpfp.write(pf.readline())
-                pfline += 1
+def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
+    def pathstrip(path, count=1):
+        pathlen = len(path)
+        i = 0
+        if count == 0:
+            return path.rstrip()
+        while count > 0:
+            i = path.find(os.sep, i)
+            if i == -1:
+                raise PatchError("Unable to strip away %d dirs from %s" %
+                                 (count, path))
+            i += 1
+            # consume '//' in the path
+            while i < pathlen - 1 and path[i] == os.sep:
+                i += 1
+            count -= 1
+        return path[i:].rstrip()
 
-            if p.binary:
-                text, delta = extractbin(pf)
-                if not text:
-                    raise util.Abort(_('binary patch extraction failed'))
-                pfline += delta
-                if not cwd:
-                    cwd = os.getcwd()
-                absdst = os.path.join(cwd, p.path)
-                basedir = os.path.dirname(absdst)
-                if not os.path.isdir(basedir):
-                    os.makedirs(basedir)
-                out = file(absdst, 'wb')
-                out.write(text)
-                out.close()
-            elif p.copymod:
-                copyfile(p.oldpath, p.path, basedir=cwd)
-                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)
+    nulla = afile_orig == "/dev/null"
+    nullb = bfile_orig == "/dev/null"
+    afile = pathstrip(afile_orig, strip)
+    gooda = os.path.exists(afile) and not nulla
+    bfile = pathstrip(bfile_orig, strip)
+    if afile == bfile:
+        goodb = gooda
+    else:
+        goodb = os.path.exists(bfile) and not nullb
+    createfunc = hunk.createfile
+    if reverse:
+        createfunc = hunk.rmfile
+    if not goodb and not gooda and not createfunc():
+        raise PatchError(_("Unable to find %s or %s for patching\n" %
+                        (afile, bfile)))
+    if gooda and goodb:
+        fname = bfile
+        if afile in bfile:
+            fname = afile
+    elif gooda:
+        fname = afile
+    elif not nullb:
+        fname = bfile
+        if afile in bfile:
+            fname = afile
+    elif not nulla:
+        fname = afile
+    return fname
+
+class linereader:
+    # simple class to allow pushing lines back into the input stream
+    def __init__(self, fp):
+        self.fp = fp
+        self.buf = []
+
+    def push(self, line):
+        self.buf.append(line)
 
-        line = pf.readline()
-        while line:
-            tmpfp.write(line)
-            line = pf.readline()
-    except:
-        tmpfp.close()
-        os.unlink(patchname)
-        raise
+    def readline(self):
+        if self.buf:
+            l = self.buf[0]
+            del self.buf[0]
+            return l
+        return self.fp.readline()
+
+def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
+              rejmerge=None, updatedir=None):
+    """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
+       any fuzz.""" 
+
+    def scangitpatch(fp, firstline, cwd=None):
+        '''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.'''
 
-    tmpfp.close()
-    return patchname
+        pos = 0
+        try:
+            pos = fp.tell()
+        except IOError:
+            fp = cStringIO.StringIO(fp.read())
+
+        (dopatch, gitpatches) = readgitpatch(fp, firstline)
+        for gp in gitpatches:
+            if gp.copymod:
+                copyfile(gp.oldpath, gp.path, basedir=cwd)
+
+        fp.seek(pos)
 
-def patch(patchname, ui, strip=1, cwd=None, files={}):
-    """apply the patch <patchname> to the working directory.
-    a list of patched files is returned"""
+        return fp, dopatch, gitpatches
+
+    current_hunk = None
+    current_file = None
+    afile = ""
+    bfile = ""
+    state = None
+    hunknum = 0
+    rejects = 0
+
+    git = False
+    gitre = re.compile('diff --git (a/.*) (b/.*)')
 
-    # helper function
-    def __patch(patchname):
-        """patch and updates the files and fuzz variables"""
-        fuzz = False
-
-        args = []
-        patcher = ui.config('ui', 'patch')
-        if not patcher:
-            patcher = util.find_exe('gpatch') or util.find_exe('patch')
-            # Try to be smart only if patch call was not supplied
-            if util.needbinarypatch():
-                args.append('--binary')
-
-        if not patcher:
-            raise util.Abort(_('no patch command found in hgrc or PATH'))
-
-        if cwd:
-            args.append('-d %s' % util.shellquote(cwd))
-        fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
-                                           util.shellquote(patchname)))
+    # our states
+    BFILE = 1
+    err = 0
+    context = None
+    lr = linereader(fp)
+    dopatch = True
+    gitworkdone = False
 
-        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 util.Abort(_("patch command failed: %s") %
-                             util.explain_exit(code)[0])
-        return fuzz
+    while True:
+        newfile = False
+        x = lr.readline()
+        if not x:
+            break
+        if current_hunk:
+            if x.startswith('\ '):
+                current_hunk.fix_newline()
+            ret = current_file.apply(current_hunk, reverse)
+            if ret > 0:
+                err = 1
+            current_hunk = None
+            gitworkdone = False
+        if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
+            ((context or context == None) and x.startswith('***************')))):
+            try:
+                if context == None and x.startswith('***************'):
+                    context = True
+                current_hunk = hunk(x, hunknum + 1, lr, context)
+            except PatchError:
+                current_hunk = None
+                continue
+            hunknum += 1
+            if not current_file:
+                if sourcefile:
+                    current_file = patchfile(ui, sourcefile)
+                else:
+                    current_file = selectfile(afile, bfile, current_hunk,
+                                              strip, reverse)
+                    current_file = patchfile(ui, current_file)
+                changed.setdefault(current_file.fname, (None, None))
+        elif state == BFILE and x.startswith('GIT binary patch'):
+            current_hunk = binhunk(changed[bfile[2:]][1])
+            if not current_file:
+                if sourcefile:
+                    current_file = patchfile(ui, sourcefile)
+                else:
+                    current_file = selectfile(afile, bfile, current_hunk,
+                                              strip, reverse)
+                    current_file = patchfile(ui, current_file)
+            hunknum += 1
+            current_hunk.extract(fp)
+        elif x.startswith('diff --git'):
+            # check for git diff, scanning the whole patch file if needed
+            m = gitre.match(x)
+            if m:
+                afile, bfile = m.group(1, 2)
+                if not git:
+                    git = True
+                    fp, dopatch, gitpatches = scangitpatch(fp, x)
+                    for gp in gitpatches:
+                        changed[gp.path] = (gp.op, gp)
+                # else error?
+                # copy/rename + modify should modify target, not source
+                if changed.get(bfile[2:], (None, None))[0] in ('COPY',
+                                                               'RENAME'):
+                    afile = bfile
+                    gitworkdone = True
+            newfile = True
+        elif x.startswith('---'):
+            # check for a unified diff
+            l2 = lr.readline()
+            if not l2.startswith('+++'):
+                lr.push(l2)
+                continue
+            newfile = True
+            context = False
+            afile = parsefilename(x)
+            bfile = parsefilename(l2)
+        elif x.startswith('***'):
+            # check for a context diff
+            l2 = lr.readline()
+            if not l2.startswith('---'):
+                lr.push(l2)
+                continue
+            l3 = lr.readline()
+            lr.push(l3)
+            if not l3.startswith("***************"):
+                lr.push(l2)
+                continue
+            newfile = True
+            context = True
+            afile = parsefilename(x)
+            bfile = parsefilename(l2)
 
-    (dopatch, gitpatches) = readgitpatch(patchname)
-    for gp in gitpatches:
-        files[gp.path] = (gp.op, gp)
-
-    fuzz = False
-    if dopatch:
-        filterpatch = dopatch & (GP_FILTER | GP_BINARY)
-        if filterpatch:
-            patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
-        try:
-            if dopatch & GP_PATCH:
-                fuzz = __patch(patchname)
-        finally:
-            if filterpatch:
-                os.unlink(patchname)
-
-    return fuzz
+        if newfile:
+            if current_file:
+                current_file.close()
+                if rejmerge:
+                    rejmerge(current_file)
+                rejects += len(current_file.rej)
+            state = BFILE
+            current_file = None
+            hunknum = 0
+    if current_hunk:
+        if current_hunk.complete():
+            ret = current_file.apply(current_hunk, reverse)
+            if ret > 0:
+                err = 1
+        else:
+            fname = current_file and current_file.fname or None
+            raise PatchError("malformed patch %s %s" % (fname,
+                             current_hunk.desc))
+    if current_file:
+        current_file.close()
+        if rejmerge:
+            rejmerge(current_file)
+        rejects += len(current_file.rej)
+    if updatedir and git:
+        updatedir(gitpatches)
+    if rejects:
+        return -1
+    if hunknum == 0 and dopatch and not gitworkdone:
+        raise PatchError("No valid hunks found")
+    return err
 
 def diffopts(ui, opts={}, untrusted=False):
     def get(key, name=None):
--- a/setup.py	Tue Jul 17 09:28:01 2007 -0300
+++ b/setup.py	Tue Jul 17 09:39:30 2007 -0700
@@ -64,7 +64,8 @@
       packages=['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert'],
       ext_modules=[Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
                    Extension('mercurial.bdiff', ['mercurial/bdiff.c']),
-                   Extension('mercurial.base85', ['mercurial/base85.c'])],
+                   Extension('mercurial.base85', ['mercurial/base85.c']),
+                   Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c'])],
       data_files=[(os.path.join('mercurial', root),
                    [os.path.join(root, file_) for file_ in files])
                   for root, dirs, files in os.walk('templates')],