changeset 2873:4ec58b157265

refactor text diff/patch code. rename commands.dodiff to patch.diff. rename commands.doexport to patch.export. move some functions from commands to new mercurial.cmdutil module. turn list of diff options into mdiff.diffopts class. patch.diff and patch.export now has clean api for call from 3rd party python code.
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Sat, 12 Aug 2006 16:13:27 -0700
parents 5dd6631c8238
children 3d6efcbbd1c9
files hgext/mq.py hgext/notify.py mercurial/cmdutil.py mercurial/commands.py mercurial/hgweb/hgweb_mod.py mercurial/mdiff.py mercurial/patch.py mercurial/ui.py
diffstat 8 files changed, 279 insertions(+), 241 deletions(-) [+]
line wrap: on
line diff
--- a/hgext/mq.py
+++ b/hgext/mq.py
@@ -32,7 +32,7 @@ refresh contents of top applied patch   
 from mercurial.demandload import *
 from mercurial.i18n import gettext as _
 demandload(globals(), "os sys re struct traceback errno bz2")
-demandload(globals(), "mercurial:commands,hg,revlog,ui,util")
+demandload(globals(), "mercurial:commands,hg,patch,revlog,ui,util")
 
 commands.norepo += " qclone qversion"
 
@@ -65,6 +65,7 @@ class queue:
         self.guards_path = "guards"
         self.active_guards = None
         self.guards_dirty = False
+        self._diffopts = None
 
         if os.path.exists(self.join(self.series_path)):
             self.full_series = self.opener(self.series_path).read().splitlines()
@@ -74,6 +75,11 @@ class queue:
             lines = self.opener(self.status_path).read().splitlines()
             self.applied = [statusentry(l) for l in lines]
 
+    def diffopts(self):
+        if self._diffopts is None:
+            self._diffopts = self.ui.diffopts()
+        return self._diffopts
+
     def join(self, *p):
         return os.path.join(self.path, *p)
 
@@ -291,6 +297,11 @@ class queue:
             message.insert(0, subject)
         return (message, comments, user, date, diffstart > 1)
 
+    def printdiff(self, repo, node1, node2=None, files=None,
+                  fp=None, changes=None, opts=None):
+        patch.diff(repo, node1, node2, files,
+                   fp=fp, changes=changes, opts=self.diffopts())
+
     def mergeone(self, repo, mergeq, head, patch, rev, wlock):
         # first try just applying the patch
         (err, n) = self.apply(repo, [ patch ], update_status=False,
@@ -324,7 +335,7 @@ class queue:
         if comments:
             comments = "\n".join(comments) + '\n\n'
             patchf.write(comments)
-        commands.dodiff(patchf, self.ui, repo, head, n)
+        self.printdiff(repo, head, n, fp=patchf)
         patchf.close()
         return (0, n)
 
@@ -918,7 +929,7 @@ class queue:
             self.ui.write("No patches applied\n")
             return
         qp = self.qparents(repo, top)
-        commands.dodiff(sys.stdout, self.ui, repo, qp, None, files)
+        self.printdiff(repo, qp, files=files)
 
     def refresh(self, repo, msg='', short=False):
         if len(self.applied) == 0:
@@ -1001,8 +1012,8 @@ class queue:
             r = list(util.unique(dd))
             a = list(util.unique(aa))
             filelist = list(util.unique(c + r + a ))
-            commands.dodiff(patchf, self.ui, repo, patchparent, None,
-                            filelist, changes=(c, a, r, [], u))
+            self.printdiff(repo, patchparent, files=filelist,
+                           changes=(c, a, r, [], u), fp=patchf)
             patchf.close()
 
             changes = repo.changelog.read(tip)
@@ -1025,7 +1036,7 @@ class queue:
             self.applied[-1] = statusentry(revlog.hex(n), patch)
             self.applied_dirty = 1
         else:
-            commands.dodiff(patchf, self.ui, repo, patchparent, None)
+            self.printdiff(repo, patchparent, fp=patchf)
             patchf.close()
             self.pop(repo, force=True, wlock=wlock)
             self.push(repo, force=True, wlock=wlock)
--- a/hgext/notify.py
+++ b/hgext/notify.py
@@ -67,7 +67,7 @@
 from mercurial.demandload import *
 from mercurial.i18n import gettext as _
 from mercurial.node import *
-demandload(globals(), 'email.Parser mercurial:commands,templater,util')
+demandload(globals(), 'email.Parser mercurial:commands,patch,templater,util')
 demandload(globals(), 'fnmatch socket time')
 
 # template for single changeset can include email headers.
@@ -238,7 +238,7 @@ class notifier(object):
             return
         fp = templater.stringio()
         prev = self.repo.changelog.parents(node)[0]
-        commands.dodiff(fp, self.ui, self.repo, prev, ref)
+        patch.diff(self.repo, fp, prev, ref)
         difflines = fp.getvalue().splitlines(1)
         if maxdiff > 0 and len(difflines) > maxdiff:
             self.sio.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
new file mode 100644
--- /dev/null
+++ b/mercurial/cmdutil.py
@@ -0,0 +1,68 @@
+# commands.py - command processing for mercurial
+#
+# Copyright 2005, 2006 Matt Mackall <mpm@selenic.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
+from node import *
+from i18n import gettext as _
+demandload(globals(), 'os sys')
+
+def make_filename(repo, pat, node,
+                  total=None, seqno=None, revwidth=None, pathname=None):
+    node_expander = {
+        'H': lambda: hex(node),
+        'R': lambda: str(repo.changelog.rev(node)),
+        'h': lambda: short(node),
+        }
+    expander = {
+        '%': lambda: '%',
+        'b': lambda: os.path.basename(repo.root),
+        }
+
+    try:
+        if node:
+            expander.update(node_expander)
+        if node and revwidth is not None:
+            expander['r'] = (lambda:
+                    str(repo.changelog.rev(node)).zfill(revwidth))
+        if total is not None:
+            expander['N'] = lambda: str(total)
+        if seqno is not None:
+            expander['n'] = lambda: str(seqno)
+        if total is not None and seqno is not None:
+            expander['n'] = lambda:str(seqno).zfill(len(str(total)))
+        if pathname is not None:
+            expander['s'] = lambda: os.path.basename(pathname)
+            expander['d'] = lambda: os.path.dirname(pathname) or '.'
+            expander['p'] = lambda: pathname
+
+        newname = []
+        patlen = len(pat)
+        i = 0
+        while i < patlen:
+            c = pat[i]
+            if c == '%':
+                i += 1
+                c = pat[i]
+                c = expander[c]()
+            newname.append(c)
+            i += 1
+        return ''.join(newname)
+    except KeyError, inst:
+        raise util.Abort(_("invalid format spec '%%%s' in output file name"),
+                    inst.args[0])
+
+def make_file(repo, pat, node=None,
+              total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
+    if not pat or pat == '-':
+        return 'w' in mode and sys.stdout or sys.stdin
+    if hasattr(pat, 'write') and 'w' in mode:
+        return pat
+    if hasattr(pat, 'read') and 'r' in mode:
+        return pat
+    return open(make_filename(repo, pat, node, total, seqno, revwidth,
+                              pathname),
+                mode)
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -10,10 +10,10 @@ from node import *
 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 difflib patch random signal tempfile time")
+demandload(globals(), "fnmatch difflib patch random signal tempfile time")
 demandload(globals(), "traceback errno socket version struct atexit sets bz2")
 demandload(globals(), "archival cStringIO changegroup")
-demandload(globals(), "hgweb.server sshserver")
+demandload(globals(), "cmdutil hgweb.server sshserver")
 
 class UnknownCommand(Exception):
     """Exception raised if command is not in the command table."""
@@ -25,15 +25,6 @@ def bail_if_changed(repo):
     if modified or added or removed or deleted:
         raise util.Abort(_("outstanding uncommitted changes"))
 
-def filterfiles(filters, files):
-    l = [x for x in files if x in filters]
-
-    for t in filters:
-        if t and t[-1] != "/":
-            t += "/"
-        l += [x for x in files if x.startswith(t)]
-    return l
-
 def relpath(repo, args):
     cwd = repo.getcwd()
     if cwd:
@@ -344,63 +335,6 @@ def revrange(ui, repo, revs):
             seen[rev] = 1
             yield str(rev)
 
-def make_filename(repo, pat, node,
-                  total=None, seqno=None, revwidth=None, pathname=None):
-    node_expander = {
-        'H': lambda: hex(node),
-        'R': lambda: str(repo.changelog.rev(node)),
-        'h': lambda: short(node),
-        }
-    expander = {
-        '%': lambda: '%',
-        'b': lambda: os.path.basename(repo.root),
-        }
-
-    try:
-        if node:
-            expander.update(node_expander)
-        if node and revwidth is not None:
-            expander['r'] = (lambda:
-                    str(repo.changelog.rev(node)).zfill(revwidth))
-        if total is not None:
-            expander['N'] = lambda: str(total)
-        if seqno is not None:
-            expander['n'] = lambda: str(seqno)
-        if total is not None and seqno is not None:
-            expander['n'] = lambda:str(seqno).zfill(len(str(total)))
-        if pathname is not None:
-            expander['s'] = lambda: os.path.basename(pathname)
-            expander['d'] = lambda: os.path.dirname(pathname) or '.'
-            expander['p'] = lambda: pathname
-
-        newname = []
-        patlen = len(pat)
-        i = 0
-        while i < patlen:
-            c = pat[i]
-            if c == '%':
-                i += 1
-                c = pat[i]
-                c = expander[c]()
-            newname.append(c)
-            i += 1
-        return ''.join(newname)
-    except KeyError, inst:
-        raise util.Abort(_("invalid format spec '%%%s' in output file name"),
-                    inst.args[0])
-
-def make_file(repo, pat, node=None,
-              total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
-    if not pat or pat == '-':
-        return 'w' in mode and sys.stdout or sys.stdin
-    if hasattr(pat, 'write') and 'w' in mode:
-        return pat
-    if hasattr(pat, 'read') and 'r' in mode:
-        return pat
-    return open(make_filename(repo, pat, node, total, seqno, revwidth,
-                              pathname),
-                mode)
-
 def write_bundle(cg, filename=None, compress=True):
     """Write a bundle file and return its filename.
 
@@ -453,74 +387,6 @@ def write_bundle(cg, filename=None, comp
         if cleanup is not None:
             os.unlink(cleanup)
 
-def dodiff(fp, ui, repo, node1, node2, files=None, match=util.always,
-           changes=None, text=False, opts={}):
-    if not node1:
-        node1 = repo.dirstate.parents()[0]
-    # reading the data for node1 early allows it to play nicely
-    # with repo.changes and the revlog cache.
-    change = repo.changelog.read(node1)
-    mmap = repo.manifest.read(change[0])
-    date1 = util.datestr(change[2])
-
-    if not changes:
-        changes = repo.changes(node1, node2, files, match=match)
-    modified, added, removed, deleted, unknown = changes
-    if files:
-        modified, added, removed = map(lambda x: filterfiles(files, x),
-                                       (modified, added, removed))
-
-    if not modified and not added and not removed:
-        return
-
-    if node2:
-        change = repo.changelog.read(node2)
-        mmap2 = repo.manifest.read(change[0])
-        _date2 = util.datestr(change[2])
-        def date2(f):
-            return _date2
-        def read(f):
-            return repo.file(f).read(mmap2[f])
-    else:
-        tz = util.makedate()[1]
-        _date2 = util.datestr()
-        def date2(f):
-            try:
-                return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
-            except OSError, err:
-                if err.errno != errno.ENOENT: raise
-                return _date2
-        def read(f):
-            return repo.wread(f)
-
-    if ui.quiet:
-        r = None
-    else:
-        hexfunc = ui.verbose and hex or short
-        r = [hexfunc(node) for node in [node1, node2] if node]
-
-    diffopts = ui.diffopts()
-    showfunc = opts.get('show_function') or diffopts['showfunc']
-    ignorews = opts.get('ignore_all_space') or diffopts['ignorews']
-    ignorewsamount = opts.get('ignore_space_change') or \
-                     diffopts['ignorewsamount']
-    ignoreblanklines = opts.get('ignore_blank_lines') or \
-                     diffopts['ignoreblanklines']
-
-    all = modified + added + removed
-    all.sort()
-    for f in all:
-        to = None
-        tn = None
-        if f in mmap:
-            to = repo.file(f).read(mmap[f])
-        if f not in removed:
-            tn = read(f)
-        fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, text=text,
-                               showfunc=showfunc, ignorews=ignorews,
-                               ignorewsamount=ignorewsamount,
-                               ignoreblanklines=ignoreblanklines))
-
 def trimuser(ui, name, rev, revcache):
     """trim the name of the user who committed a change"""
     user = revcache.get(rev)
@@ -922,7 +788,7 @@ def archive(ui, repo, dest, **opts):
             raise util.Abort(_('uncommitted merge - please provide a '
                                'specific revision'))
 
-    dest = make_filename(repo, dest, node)
+    dest = cmdutil.make_filename(repo, dest, node)
     if os.path.realpath(dest) == repo.root:
         raise util.Abort(_('repository root cannot be destination'))
     dummy, matchfn, dummy = matchpats(repo, [], opts)
@@ -933,7 +799,7 @@ def archive(ui, repo, dest, **opts):
             raise util.Abort(_('cannot archive plain files to stdout'))
         dest = sys.stdout
         if not prefix: prefix = os.path.basename(repo.root) + '-%h'
-    prefix = make_filename(repo, prefix, node)
+    prefix = cmdutil.make_filename(repo, prefix, node)
     archival.archive(repo, dest, node, kind, not opts['no_decode'],
                      matchfn, prefix)
 
@@ -1038,7 +904,7 @@ def cat(ui, repo, file1, *pats, **opts):
     """
     ctx = repo.changectx(opts['rev'] or "-1")
     for src, abs, rel, exact in walk(repo, (file1,) + pats, opts, ctx.node()):
-        fp = make_file(repo, opts['output'], ctx.node(), pathname=abs)
+        fp = cmdutil.make_file(repo, opts['output'], ctx.node(), pathname=abs)
         fp.write(ctx.filectx(abs).data())
 
 def clone(ui, source, dest=None, **opts):
@@ -1507,35 +1373,8 @@ def diff(ui, repo, *pats, **opts):
 
     fns, matchfn, anypats = matchpats(repo, pats, opts)
 
-    dodiff(sys.stdout, ui, repo, node1, node2, fns, match=matchfn,
-           text=opts['text'], opts=opts)
-
-def doexport(ui, repo, changeset, seqno, total, revwidth, opts):
-    node = repo.lookup(changeset)
-    parents = [p for p in repo.changelog.parents(node) if p != nullid]
-    if opts['switch_parent']:
-        parents.reverse()
-    prev = (parents and parents[0]) or nullid
-    change = repo.changelog.read(node)
-
-    fp = make_file(repo, opts['output'], node, total=total, seqno=seqno,
-                   revwidth=revwidth)
-    if fp != sys.stdout:
-        ui.note("%s\n" % fp.name)
-
-    fp.write("# HG changeset patch\n")
-    fp.write("# User %s\n" % change[1])
-    fp.write("# Date %d %d\n" % change[2])
-    fp.write("# Node ID %s\n" % hex(node))
-    fp.write("# Parent  %s\n" % hex(prev))
-    if len(parents) > 1:
-        fp.write("# Parent  %s\n" % hex(parents[1]))
-    fp.write(change[4].rstrip())
-    fp.write("\n\n")
-
-    dodiff(fp, ui, repo, prev, node, text=opts['text'])
-    if fp != sys.stdout:
-        fp.close()
+    patch.diff(repo, node1, node2, fns, match=matchfn,
+               opts=ui.diffopts(opts))
 
 def export(ui, repo, *changesets, **opts):
     """dump the header and diffs for one or more changesets
@@ -1566,15 +1405,13 @@ def export(ui, repo, *changesets, **opts
     """
     if not changesets:
         raise util.Abort(_("export requires at least one changeset"))
-    seqno = 0
     revs = list(revrange(ui, repo, changesets))
-    total = len(revs)
-    revwidth = max(map(len, revs))
-    msg = len(revs) > 1 and _("Exporting patches:\n") or _("Exporting patch:\n")
-    ui.note(msg)
-    for cset in revs:
-        seqno += 1
-        doexport(ui, repo, cset, seqno, total, revwidth, opts)
+    if len(revs) > 1:
+        ui.note(_('exporting patches:\n'))
+    else:
+        ui.note(_('exporting patch:\n'))
+    patch.export(repo, map(repo.lookup, revs), template=opts['output'],
+                 switch_parent=opts['switch_parent'], opts=ui.diffopts(opts))
 
 def forget(ui, repo, *pats, **opts):
     """don't add the specified files on the next commit (DEPRECATED)
@@ -1963,7 +1800,7 @@ def incoming(ui, repo, source="default",
             displayer.show(changenode=n)
             if opts['patch']:
                 prev = (parents and parents[0]) or nullid
-                dodiff(ui, ui, other, prev, n)
+                patch.diff(repo, other, prev, n)
                 ui.write("\n")
     finally:
         if hasattr(other, 'close'):
@@ -2114,7 +1951,7 @@ def log(ui, repo, *pats, **opts):
             displayer.show(rev, brinfo=br)
             if opts['patch']:
                 prev = (parents and parents[0]) or nullid
-                dodiff(du, du, repo, prev, changenode, match=matchfn)
+                patch.diff(repo, prev, changenode, match=matchfn, fp=du)
                 du.write("\n\n")
         elif st == 'iter':
             if count == limit: break
@@ -2195,7 +2032,7 @@ def outgoing(ui, repo, dest=None, **opts
         displayer.show(changenode=n)
         if opts['patch']:
             prev = (parents and parents[0]) or nullid
-            dodiff(ui, ui, repo, prev, n)
+            patch.diff(repo, prev, n)
             ui.write("\n")
 
 def parents(ui, repo, file_=None, rev=None, branches=None, **opts):
@@ -2843,7 +2680,7 @@ def tip(ui, repo, **opts):
         br = repo.branchlookup([n])
     show_changeset(ui, repo, opts).show(changenode=n, brinfo=br)
     if opts['patch']:
-        dodiff(ui, ui, repo, repo.changelog.parents(n)[0], n)
+        patch.diff(repo, repo.changelog.parents(n)[0], n)
 
 def unbundle(ui, repo, fname, **opts):
     """apply a changegroup file
--- a/mercurial/hgweb/hgweb_mod.py
+++ b/mercurial/hgweb/hgweb_mod.py
@@ -134,32 +134,22 @@ class hgweb(object):
             modified, added, removed = map(lambda x: filterfiles(files, x),
                                            (modified, added, removed))
 
-        diffopts = self.repo.ui.diffopts()
-        showfunc = diffopts['showfunc']
-        ignorews = diffopts['ignorews']
-        ignorewsamount = diffopts['ignorewsamount']
-        ignoreblanklines = diffopts['ignoreblanklines']
+        diffopts = ui.diffopts()
         for f in modified:
             to = r.file(f).read(mmap1[f])
             tn = r.file(f).read(mmap2[f])
             yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
-                            showfunc=showfunc, ignorews=ignorews,
-                            ignorewsamount=ignorewsamount,
-                            ignoreblanklines=ignoreblanklines), f, tn)
+                                          opts=diffopts), f, tn)
         for f in added:
             to = None
             tn = r.file(f).read(mmap2[f])
             yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
-                            showfunc=showfunc, ignorews=ignorews,
-                            ignorewsamount=ignorewsamount,
-                            ignoreblanklines=ignoreblanklines), f, tn)
+                                          opts=diffopts), f, tn)
         for f in removed:
             to = r.file(f).read(mmap1[f])
             tn = None
             yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
-                            showfunc=showfunc, ignorews=ignorews,
-                            ignorewsamount=ignorewsamount,
-                            ignoreblanklines=ignoreblanklines), f, tn)
+                                          opts=diffopts), f, tn)
 
     def changelog(self, pos, shortlog=False):
         def changenav(**map):
--- a/mercurial/mdiff.py
+++ b/mercurial/mdiff.py
@@ -19,14 +19,39 @@ def splitnewlines(text):
             lines[-1] = lines[-1][:-1]
     return lines
 
-def unidiff(a, ad, b, bd, fn, r=None, text=False,
-            showfunc=False, ignorews=False, ignorewsamount=False,
-            ignoreblanklines=False):
+class diffopts(object):
+    '''context is the number of context lines
+    text treats all files as text
+    showfunc enables diff -p output
+    ignorews ignores all whitespace changes in the diff
+    ignorewsamount ignores changes in the amount of whitespace
+    ignoreblanklines ignores changes whose lines are all blank'''
 
+    defaults = {
+        'context': 3,
+        'text': False,
+        'showfunc': True,
+        'ignorews': False,
+        'ignorewsamount': False,
+        'ignoreblanklines': False,
+        }
+
+    __slots__ = defaults.keys()
+
+    def __init__(self, **opts):
+        for k in self.__slots__:
+            v = opts.get(k)
+            if v is None:
+                v = self.defaults[k]
+            setattr(self, k, v)
+
+defaultopts = diffopts()
+
+def unidiff(a, ad, b, bd, fn, r=None, opts=defaultopts):
     if not a and not b: return ""
     epoch = util.datestr((0, 0))
 
-    if not text and (util.binary(a) or util.binary(b)):
+    if not opts.text and (util.binary(a) or util.binary(b)):
         l = ['Binary file %s has changed\n' % fn]
     elif not a:
         b = splitnewlines(b)
@@ -49,10 +74,7 @@ def unidiff(a, ad, b, bd, fn, r=None, te
     else:
         al = splitnewlines(a)
         bl = splitnewlines(b)
-        l = list(bunidiff(a, b, al, bl, "a/" + fn, "b/" + fn,
-                          showfunc=showfunc, ignorews=ignorews,
-                          ignorewsamount=ignorewsamount,
-                          ignoreblanklines=ignoreblanklines))
+        l = list(bunidiff(a, b, al, bl, "a/" + fn, "b/" + fn, opts=opts))
         if not l: return ""
         # difflib uses a space, rather than a tab
         l[0] = "%s\t%s\n" % (l[0][:-2], ad)
@@ -72,21 +94,15 @@ def unidiff(a, ad, b, bd, fn, r=None, te
 # t1 and t2 are the text to be diffed
 # l1 and l2 are the text broken up into lines
 # header1 and header2 are the filenames for the diff output
-# context is the number of context lines
-# showfunc enables diff -p output
-# ignorews ignores all whitespace changes in the diff
-# ignorewsamount ignores changes in the amount of whitespace
-# ignoreblanklines ignores changes whose lines are all blank
-def bunidiff(t1, t2, l1, l2, header1, header2, context=3, showfunc=False,
-             ignorews=False, ignorewsamount=False, ignoreblanklines=False):
+def bunidiff(t1, t2, l1, l2, header1, header2, opts=defaultopts):
     def contextend(l, len):
-        ret = l + context
+        ret = l + opts.context
         if ret > len:
             ret = len
         return ret
 
     def contextstart(l):
-        ret = l - context
+        ret = l - opts.context
         if ret < 0:
             return 0
         return ret
@@ -101,7 +117,7 @@ def bunidiff(t1, t2, l1, l2, header1, he
         blen = b2 - bstart + aend - a2
 
         func = ""
-        if showfunc:
+        if opts.showfunc:
             # walk backwards from the start of the context
             # to find a line starting with an alphanumeric char.
             for x in xrange(astart, -1, -1):
@@ -119,14 +135,14 @@ def bunidiff(t1, t2, l1, l2, header1, he
 
     header = [ "--- %s\t\n" % header1, "+++ %s\t\n" % header2 ]
 
-    if showfunc:
+    if opts.showfunc:
         funcre = re.compile('\w')
-    if ignorewsamount:
+    if opts.ignorewsamount:
         wsamountre = re.compile('[ \t]+')
         wsappendedre = re.compile(' \n')
-    if ignoreblanklines:
+    if opts.ignoreblanklines:
         wsblanklinesre = re.compile('\n')
-    if ignorews:
+    if opts.ignorews:
         wsre = re.compile('[ \t]')
 
     # bdiff.blocks gives us the matching sequences in the files.  The loop
@@ -159,13 +175,13 @@ def bunidiff(t1, t2, l1, l2, header1, he
         if not old and not new:
             continue
 
-        if ignoreblanklines:
+        if opts.ignoreblanklines:
             wsold = wsblanklinesre.sub('', "".join(old))
             wsnew = wsblanklinesre.sub('', "".join(new))
             if wsold == wsnew:
                 continue
 
-        if ignorewsamount:
+        if opts.ignorewsamount:
             wsold = wsamountre.sub(' ', "".join(old))
             wsold = wsappendedre.sub('\n', wsold)
             wsnew = wsamountre.sub(' ', "".join(new))
@@ -173,7 +189,7 @@ def bunidiff(t1, t2, l1, l2, header1, he
             if wsold == wsnew:
                 continue
 
-        if ignorews:
+        if opts.ignorews:
             wsold = wsre.sub('', "".join(old))
             wsnew = wsre.sub('', "".join(new))
             if wsold == wsnew:
@@ -184,7 +200,7 @@ def bunidiff(t1, t2, l1, l2, header1, he
         prev = None
         if hunk:
             # join with the previous hunk if it falls inside the context
-            if astart < hunk[1] + context + 1:
+            if astart < hunk[1] + opts.context + 1:
                 prev = hunk
                 astart = hunk[1]
                 bstart = hunk[3]
--- a/mercurial/patch.py
+++ b/mercurial/patch.py
@@ -7,8 +7,9 @@
 
 from demandload import demandload
 from i18n import gettext as _
-demandload(globals(), "util")
-demandload(globals(), "cStringIO email.Parser os re shutil tempfile")
+from node import *
+demandload(globals(), "cmdutil mdiff util")
+demandload(globals(), "cStringIO email.Parser os re shutil sys tempfile")
 
 def extract(ui, fileobj):
     '''extract patch from data read from fileobj.
@@ -249,3 +250,117 @@ def patch(strip, patchname, ui, cwd=None
         files[gp.path] = (gp.op, gp)
 
     return files
+
+def diff(repo, node1=None, node2=None, files=None, match=util.always,
+         fp=None, changes=None, opts=None):
+    '''print diff of changes to files between two nodes, or node and
+    working directory.
+
+    if node1 is None, use first dirstate parent instead.
+    if node2 is None, compare node1 with working directory.'''
+
+    if opts is None:
+        opts = mdiff.defaultopts
+    if fp is None:
+        fp = repo.ui
+
+    if not node1:
+        node1 = repo.dirstate.parents()[0]
+    # reading the data for node1 early allows it to play nicely
+    # with repo.changes and the revlog cache.
+    change = repo.changelog.read(node1)
+    mmap = repo.manifest.read(change[0])
+    date1 = util.datestr(change[2])
+
+    if not changes:
+        changes = repo.changes(node1, node2, files, match=match)
+    modified, added, removed, deleted, unknown = changes
+    if files:
+        def filterfiles(filters):
+            l = [x for x in files if x in filters]
+
+            for t in filters:
+                if t and t[-1] != "/":
+                    t += "/"
+                l += [x for x in files if x.startswith(t)]
+            return l
+
+        modified, added, removed = map(lambda x: filterfiles(x),
+                                       (modified, added, removed))
+
+    if not modified and not added and not removed:
+        return
+
+    if node2:
+        change = repo.changelog.read(node2)
+        mmap2 = repo.manifest.read(change[0])
+        _date2 = util.datestr(change[2])
+        def date2(f):
+            return _date2
+        def read(f):
+            return repo.file(f).read(mmap2[f])
+    else:
+        tz = util.makedate()[1]
+        _date2 = util.datestr()
+        def date2(f):
+            try:
+                return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
+            except OSError, err:
+                if err.errno != errno.ENOENT: raise
+                return _date2
+        def read(f):
+            return repo.wread(f)
+
+    if repo.ui.quiet:
+        r = None
+    else:
+        hexfunc = repo.ui.verbose and hex or short
+        r = [hexfunc(node) for node in [node1, node2] if node]
+
+    all = modified + added + removed
+    all.sort()
+    for f in all:
+        to = None
+        tn = None
+        if f in mmap:
+            to = repo.file(f).read(mmap[f])
+        if f not in removed:
+            tn = read(f)
+        fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts))
+
+def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
+           opts=None):
+    '''export changesets as hg patches.'''
+
+    total = len(revs)
+    revwidth = max(map(len, revs))
+
+    def single(node, seqno, fp):
+        parents = [p for p in repo.changelog.parents(node) if p != nullid]
+        if switch_parent:
+            parents.reverse()
+        prev = (parents and parents[0]) or nullid
+        change = repo.changelog.read(node)
+
+        if not fp:
+            fp = cmdutil.make_file(repo, template, node, total=total,
+                                   seqno=seqno, revwidth=revwidth)
+        if fp not in (sys.stdout, repo.ui):
+            repo.ui.note("%s\n" % fp.name)
+
+        fp.write("# HG changeset patch\n")
+        fp.write("# User %s\n" % change[1])
+        fp.write("# Date %d %d\n" % change[2])
+        fp.write("# Node ID %s\n" % hex(node))
+        fp.write("# Parent  %s\n" % hex(prev))
+        if len(parents) > 1:
+            fp.write("# Parent  %s\n" % hex(parents[1]))
+        fp.write(change[4].rstrip())
+        fp.write("\n\n")
+
+        diff(repo, prev, node, fp=fp, opts=opts)
+        if fp not in (sys.stdout, repo.ui):
+            fp.close()
+
+    for seqno, cset in enumerate(revs):
+        single(cset, seqno, fp)
--- a/mercurial/ui.py
+++ b/mercurial/ui.py
@@ -8,7 +8,7 @@
 from i18n import gettext as _
 from demandload import *
 demandload(globals(), "errno getpass os re smtplib socket sys tempfile")
-demandload(globals(), "ConfigParser templater traceback util")
+demandload(globals(), "ConfigParser mdiff templater traceback util")
 
 class ui(object):
     def __init__(self, verbose=False, debug=False, quiet=False,
@@ -169,16 +169,17 @@ class ui(object):
             result[key.lower()] = value
         return result
 
-    def diffopts(self):
-        if self.diffcache:
-            return self.diffcache
-        result = {'showfunc': True, 'ignorews': False,
-                  'ignorewsamount': False, 'ignoreblanklines': False}
-        for key, value in self.configitems("diff"):
-            if value:
-                result[key.lower()] = (value.lower() == 'true')
-        self.diffcache = result
-        return result
+    def diffopts(self, opts={}):
+        return mdiff.diffopts(
+            text=opts.get('text'),
+            showfunc=(opts.get('show_function') or
+                      self.configbool('diff', 'showfunc', None)),
+            ignorews=(opts.get('ignore_all_space') or
+                      self.configbool('diff', 'ignorews', None)),
+            ignorewsamount=(opts.get('ignore_space_change') or
+                            self.configbool('diff', 'ignorewsamount', None)),
+            ignoreblanklines=(opts.get('ignore_blank_lines') or
+                              self.configbool('diff', 'ignoreblanklines', None)))
 
     def username(self):
         """Return default username to be used in commits.