summaryrefslogtreecommitdiff
path: root/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/patch.py
diff options
context:
space:
mode:
authorNishanth Amuluru2011-01-08 11:20:57 +0530
committerNishanth Amuluru2011-01-08 11:20:57 +0530
commit65411d01d448ff0cd4abd14eee14cf60b5f8fc20 (patch)
treeb4c404363c4c63a61d6e2f8bd26c5b057c1fb09d /eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/patch.py
parent2e35094d43b4cc6974172e1febf76abb50f086ec (diff)
downloadpytask-65411d01d448ff0cd4abd14eee14cf60b5f8fc20.tar.gz
pytask-65411d01d448ff0cd4abd14eee14cf60b5f8fc20.tar.bz2
pytask-65411d01d448ff0cd4abd14eee14cf60b5f8fc20.zip
Added buildout stuff and made changes accordingly
--HG-- rename : profile/management/__init__.py => eggs/djangorecipe-0.20-py2.6.egg/EGG-INFO/dependency_links.txt rename : profile/management/__init__.py => eggs/djangorecipe-0.20-py2.6.egg/EGG-INFO/not-zip-safe rename : profile/management/__init__.py => eggs/infrae.subversion-1.4.5-py2.6.egg/EGG-INFO/dependency_links.txt rename : profile/management/__init__.py => eggs/infrae.subversion-1.4.5-py2.6.egg/EGG-INFO/not-zip-safe rename : profile/management/__init__.py => eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/EGG-INFO/dependency_links.txt rename : profile/management/__init__.py => eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/EGG-INFO/not-zip-safe rename : profile/management/__init__.py => eggs/py-1.4.0-py2.6.egg/EGG-INFO/dependency_links.txt rename : profile/management/__init__.py => eggs/py-1.4.0-py2.6.egg/EGG-INFO/not-zip-safe rename : profile/management/__init__.py => eggs/zc.buildout-1.5.2-py2.6.egg/EGG-INFO/dependency_links.txt rename : profile/management/__init__.py => eggs/zc.buildout-1.5.2-py2.6.egg/EGG-INFO/not-zip-safe rename : profile/management/__init__.py => eggs/zc.recipe.egg-1.3.2-py2.6.egg/EGG-INFO/dependency_links.txt rename : profile/management/__init__.py => eggs/zc.recipe.egg-1.3.2-py2.6.egg/EGG-INFO/not-zip-safe rename : profile/management/__init__.py => parts/django/Django.egg-info/dependency_links.txt rename : taskapp/models.py => parts/django/django/conf/app_template/models.py rename : taskapp/tests.py => parts/django/django/conf/app_template/tests.py rename : taskapp/views.py => parts/django/django/conf/app_template/views.py rename : taskapp/views.py => parts/django/django/contrib/gis/tests/geo3d/views.py rename : profile/management/__init__.py => parts/django/tests/modeltests/delete/__init__.py rename : profile/management/__init__.py => parts/django/tests/modeltests/files/__init__.py rename : profile/management/__init__.py => parts/django/tests/modeltests/invalid_models/__init__.py rename : profile/management/__init__.py => parts/django/tests/modeltests/m2m_signals/__init__.py rename : profile/management/__init__.py => parts/django/tests/modeltests/model_package/__init__.py rename : profile/management/__init__.py => parts/django/tests/regressiontests/bash_completion/__init__.py rename : profile/management/__init__.py => parts/django/tests/regressiontests/bash_completion/management/__init__.py rename : profile/management/__init__.py => parts/django/tests/regressiontests/bash_completion/management/commands/__init__.py rename : profile/management/__init__.py => parts/django/tests/regressiontests/bash_completion/models.py rename : profile/management/__init__.py => parts/django/tests/regressiontests/delete_regress/__init__.py rename : profile/management/__init__.py => parts/django/tests/regressiontests/file_storage/__init__.py rename : profile/management/__init__.py => parts/django/tests/regressiontests/max_lengths/__init__.py rename : profile/forms.py => pytask/profile/forms.py rename : profile/management/__init__.py => pytask/profile/management/__init__.py rename : profile/management/commands/seed_db.py => pytask/profile/management/commands/seed_db.py rename : profile/models.py => pytask/profile/models.py rename : profile/templatetags/user_tags.py => pytask/profile/templatetags/user_tags.py rename : taskapp/tests.py => pytask/profile/tests.py rename : profile/urls.py => pytask/profile/urls.py rename : profile/utils.py => pytask/profile/utils.py rename : profile/views.py => pytask/profile/views.py rename : static/css/base.css => pytask/static/css/base.css rename : taskapp/tests.py => pytask/taskapp/tests.py rename : taskapp/views.py => pytask/taskapp/views.py rename : templates/base.html => pytask/templates/base.html rename : templates/profile/browse_notifications.html => pytask/templates/profile/browse_notifications.html rename : templates/profile/edit.html => pytask/templates/profile/edit.html rename : templates/profile/view.html => pytask/templates/profile/view.html rename : templates/profile/view_notification.html => pytask/templates/profile/view_notification.html rename : templates/registration/activate.html => pytask/templates/registration/activate.html rename : templates/registration/activation_email.txt => pytask/templates/registration/activation_email.txt rename : templates/registration/activation_email_subject.txt => pytask/templates/registration/activation_email_subject.txt rename : templates/registration/logged_out.html => pytask/templates/registration/logged_out.html rename : templates/registration/login.html => pytask/templates/registration/login.html rename : templates/registration/logout.html => pytask/templates/registration/logout.html rename : templates/registration/password_change_done.html => pytask/templates/registration/password_change_done.html rename : templates/registration/password_change_form.html => pytask/templates/registration/password_change_form.html rename : templates/registration/password_reset_complete.html => pytask/templates/registration/password_reset_complete.html rename : templates/registration/password_reset_confirm.html => pytask/templates/registration/password_reset_confirm.html rename : templates/registration/password_reset_done.html => pytask/templates/registration/password_reset_done.html rename : templates/registration/password_reset_email.html => pytask/templates/registration/password_reset_email.html rename : templates/registration/password_reset_form.html => pytask/templates/registration/password_reset_form.html rename : templates/registration/registration_complete.html => pytask/templates/registration/registration_complete.html rename : templates/registration/registration_form.html => pytask/templates/registration/registration_form.html rename : utils.py => pytask/utils.py
Diffstat (limited to 'eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/patch.py')
-rw-r--r--eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/patch.py1630
1 files changed, 1630 insertions, 0 deletions
diff --git a/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/patch.py b/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/patch.py
new file mode 100644
index 0000000..8e9b13e
--- /dev/null
+++ b/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/patch.py
@@ -0,0 +1,1630 @@
+# 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 version 2 or any later version.
+
+import cStringIO, email.Parser, os, re
+import tempfile, zlib
+
+from i18n import _
+from node import hex, nullid, short
+import base85, mdiff, util, diffhelpers, copies, encoding
+
+gitre = re.compile('diff --git a/(.*) b/(.*)')
+
+class PatchError(Exception):
+ pass
+
+# helper functions
+
+def copyfile(src, dst, basedir):
+ abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
+ if os.path.lexists(absdst):
+ raise util.Abort(_("cannot create %s: destination already exists") %
+ dst)
+
+ dstdir = os.path.dirname(absdst)
+ if dstdir and not os.path.isdir(dstdir):
+ try:
+ os.makedirs(dstdir)
+ except IOError:
+ raise util.Abort(
+ _("cannot create %s: unable to create destination directory")
+ % dst)
+
+ util.copyfile(abssrc, absdst)
+
+# public functions
+
+def split(stream):
+ '''return an iterator of individual patches from a stream'''
+ def isheader(line, inheader):
+ if inheader and line[0] in (' ', '\t'):
+ # continuation
+ return True
+ if line[0] in (' ', '-', '+'):
+ # diff line - don't check for header pattern in there
+ return False
+ l = line.split(': ', 1)
+ return len(l) == 2 and ' ' not in l[0]
+
+ def chunk(lines):
+ return cStringIO.StringIO(''.join(lines))
+
+ def hgsplit(stream, cur):
+ inheader = True
+
+ for line in stream:
+ if not line.strip():
+ inheader = False
+ if not inheader and line.startswith('# HG changeset patch'):
+ yield chunk(cur)
+ cur = []
+ inheader = True
+
+ cur.append(line)
+
+ if cur:
+ yield chunk(cur)
+
+ def mboxsplit(stream, cur):
+ for line in stream:
+ if line.startswith('From '):
+ for c in split(chunk(cur[1:])):
+ yield c
+ cur = []
+
+ cur.append(line)
+
+ if cur:
+ for c in split(chunk(cur[1:])):
+ yield c
+
+ def mimesplit(stream, cur):
+ def msgfp(m):
+ fp = cStringIO.StringIO()
+ g = email.Generator.Generator(fp, mangle_from_=False)
+ g.flatten(m)
+ fp.seek(0)
+ return fp
+
+ for line in stream:
+ cur.append(line)
+ c = chunk(cur)
+
+ m = email.Parser.Parser().parse(c)
+ if not m.is_multipart():
+ yield msgfp(m)
+ else:
+ ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
+ for part in m.walk():
+ ct = part.get_content_type()
+ if ct not in ok_types:
+ continue
+ yield msgfp(part)
+
+ def headersplit(stream, cur):
+ inheader = False
+
+ for line in stream:
+ if not inheader and isheader(line, inheader):
+ yield chunk(cur)
+ cur = []
+ inheader = True
+ if inheader and not isheader(line, inheader):
+ inheader = False
+
+ cur.append(line)
+
+ if cur:
+ yield chunk(cur)
+
+ def remainder(cur):
+ yield chunk(cur)
+
+ class fiter(object):
+ def __init__(self, fp):
+ self.fp = fp
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ l = self.fp.readline()
+ if not l:
+ raise StopIteration
+ return l
+
+ inheader = False
+ cur = []
+
+ mimeheaders = ['content-type']
+
+ if not hasattr(stream, 'next'):
+ # http responses, for example, have readline but not next
+ stream = fiter(stream)
+
+ for line in stream:
+ cur.append(line)
+ if line.startswith('# HG changeset patch'):
+ return hgsplit(stream, cur)
+ elif line.startswith('From '):
+ return mboxsplit(stream, cur)
+ elif isheader(line, inheader):
+ inheader = True
+ if line.split(':', 1)[0].lower() in mimeheaders:
+ # let email parser handle this
+ return mimesplit(stream, cur)
+ elif line.startswith('--- ') and inheader:
+ # No evil headers seen by diff start, split by hand
+ return headersplit(stream, cur)
+ # Not enough info, keep reading
+
+ # if we are here, we have a very plain patch
+ return remainder(cur)
+
+def extract(ui, fileobj):
+ '''extract patch from data read from fileobj.
+
+ patch can be a normal patch or contained in an email message.
+
+ return tuple (filename, message, user, date, branch, node, p1, p2).
+ Any item in the returned tuple can be None. If filename is None,
+ fileobj did not contain a patch. Caller must unlink filename when done.'''
+
+ # attempt to detect the start of a patch
+ # (this heuristic is borrowed from quilt)
+ diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
+ r'retrieving revision [0-9]+(\.[0-9]+)*$|'
+ r'---[ \t].*?^\+\+\+[ \t]|'
+ r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
+
+ fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
+ tmpfp = os.fdopen(fd, 'w')
+ try:
+ msg = email.Parser.Parser().parse(fileobj)
+
+ subject = msg['Subject']
+ user = msg['From']
+ if not subject and not user:
+ # Not an email, restore parsed headers if any
+ subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
+
+ gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
+ # should try to parse msg['Date']
+ date = None
+ nodeid = None
+ branch = None
+ parents = []
+
+ if subject:
+ if subject.startswith('[PATCH'):
+ pend = subject.find(']')
+ if pend >= 0:
+ subject = subject[pend + 1:].lstrip()
+ subject = subject.replace('\n\t', ' ')
+ ui.debug('Subject: %s\n' % subject)
+ if user:
+ ui.debug('From: %s\n' % user)
+ diffs_seen = 0
+ ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
+ message = ''
+ for part in msg.walk():
+ content_type = part.get_content_type()
+ ui.debug('Content-Type: %s\n' % content_type)
+ if content_type not in ok_types:
+ continue
+ payload = part.get_payload(decode=True)
+ m = diffre.search(payload)
+ if m:
+ hgpatch = False
+ hgpatchheader = False
+ ignoretext = False
+
+ ui.debug('found patch at byte %d\n' % m.start(0))
+ diffs_seen += 1
+ cfp = cStringIO.StringIO()
+ for line in payload[:m.start(0)].splitlines():
+ if line.startswith('# HG changeset patch') and not hgpatch:
+ ui.debug('patch generated by hg export\n')
+ hgpatch = True
+ hgpatchheader = True
+ # drop earlier commit message content
+ cfp.seek(0)
+ cfp.truncate()
+ subject = None
+ elif hgpatchheader:
+ if line.startswith('# User '):
+ user = line[7:]
+ ui.debug('From: %s\n' % user)
+ elif line.startswith("# Date "):
+ date = line[7:]
+ elif line.startswith("# Branch "):
+ branch = line[9:]
+ elif line.startswith("# Node ID "):
+ nodeid = line[10:]
+ elif line.startswith("# Parent "):
+ parents.append(line[10:])
+ elif not line.startswith("# "):
+ hgpatchheader = False
+ elif line == '---' and gitsendmail:
+ ignoretext = True
+ if not hgpatchheader and not ignoretext:
+ cfp.write(line)
+ cfp.write('\n')
+ message = cfp.getvalue()
+ if tmpfp:
+ tmpfp.write(payload)
+ if not payload.endswith('\n'):
+ tmpfp.write('\n')
+ elif not diffs_seen and message and content_type == 'text/plain':
+ message += '\n' + payload
+ except:
+ tmpfp.close()
+ os.unlink(tmpname)
+ raise
+
+ if subject and not message.startswith(subject):
+ message = '%s\n%s' % (subject, message)
+ tmpfp.close()
+ if not diffs_seen:
+ os.unlink(tmpname)
+ return None, message, user, date, branch, None, None, None
+ p1 = parents and parents.pop(0) or None
+ p2 = parents and parents.pop(0) or None
+ return tmpname, message, user, date, branch, nodeid, p1, p2
+
+class patchmeta(object):
+ """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.binary = False
+
+ def setmode(self, mode):
+ islink = mode & 020000
+ isexec = mode & 0100
+ self.mode = (islink, isexec)
+
+ def __repr__(self):
+ return "<patchmeta %s %r>" % (self.op, self.path)
+
+def readgitpatch(lr):
+ """extract git-style metadata about patches from <patchname>"""
+
+ # Filter patch for git information
+ gp = None
+ gitpatches = []
+ for line in lr:
+ line = line.rstrip(' \r\n')
+ if line.startswith('diff --git'):
+ m = gitre.match(line)
+ if m:
+ if gp:
+ gitpatches.append(gp)
+ dst = m.group(2)
+ gp = patchmeta(dst)
+ elif gp:
+ if line.startswith('--- '):
+ gitpatches.append(gp)
+ gp = None
+ continue
+ if line.startswith('rename from '):
+ gp.op = 'RENAME'
+ gp.oldpath = line[12:]
+ elif line.startswith('rename to '):
+ gp.path = line[10:]
+ elif line.startswith('copy from '):
+ gp.op = 'COPY'
+ gp.oldpath = line[10:]
+ elif line.startswith('copy to '):
+ gp.path = line[8:]
+ elif line.startswith('deleted file'):
+ gp.op = 'DELETE'
+ elif line.startswith('new file mode '):
+ gp.op = 'ADD'
+ gp.setmode(int(line[-6:], 8))
+ elif line.startswith('new mode '):
+ gp.setmode(int(line[-6:], 8))
+ elif line.startswith('GIT binary patch'):
+ gp.binary = True
+ if gp:
+ gitpatches.append(gp)
+
+ return gitpatches
+
+class linereader(object):
+ # simple class to allow pushing lines back into the input stream
+ def __init__(self, fp, textmode=False):
+ self.fp = fp
+ self.buf = []
+ self.textmode = textmode
+ self.eol = None
+
+ def push(self, line):
+ if line is not None:
+ self.buf.append(line)
+
+ def readline(self):
+ if self.buf:
+ l = self.buf[0]
+ del self.buf[0]
+ return l
+ l = self.fp.readline()
+ if not self.eol:
+ if l.endswith('\r\n'):
+ self.eol = '\r\n'
+ elif l.endswith('\n'):
+ self.eol = '\n'
+ if self.textmode and l.endswith('\r\n'):
+ l = l[:-2] + '\n'
+ return l
+
+ def __iter__(self):
+ while 1:
+ l = self.readline()
+ if not l:
+ break
+ yield l
+
+# @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
+unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
+contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
+eolmodes = ['strict', 'crlf', 'lf', 'auto']
+
+class patchfile(object):
+ def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
+ self.fname = fname
+ self.eolmode = eolmode
+ self.eol = None
+ self.opener = opener
+ self.ui = ui
+ self.lines = []
+ self.exists = False
+ self.missing = missing
+ if not missing:
+ try:
+ self.lines = self.readlines(fname)
+ self.exists = True
+ except IOError:
+ pass
+ else:
+ self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
+
+ self.hash = {}
+ self.dirty = 0
+ self.offset = 0
+ self.skew = 0
+ self.rej = []
+ self.fileprinted = False
+ self.printfile(False)
+ self.hunks = 0
+
+ def readlines(self, fname):
+ if os.path.islink(fname):
+ return [os.readlink(fname)]
+ fp = self.opener(fname, 'r')
+ try:
+ lr = linereader(fp, self.eolmode != 'strict')
+ lines = list(lr)
+ self.eol = lr.eol
+ return lines
+ finally:
+ fp.close()
+
+ def writelines(self, fname, lines):
+ # Ensure supplied data ends in fname, being a regular file or
+ # a symlink. cmdutil.updatedir will -too magically- take care
+ # of setting it to the proper type afterwards.
+ islink = os.path.islink(fname)
+ if islink:
+ fp = cStringIO.StringIO()
+ else:
+ fp = self.opener(fname, 'w')
+ try:
+ if self.eolmode == 'auto':
+ eol = self.eol
+ elif self.eolmode == 'crlf':
+ eol = '\r\n'
+ else:
+ eol = '\n'
+
+ if self.eolmode != 'strict' and eol and eol != '\n':
+ for l in lines:
+ if l and l[-1] == '\n':
+ l = l[:-1] + eol
+ fp.write(l)
+ else:
+ fp.writelines(lines)
+ if islink:
+ self.opener.symlink(fp.getvalue(), fname)
+ finally:
+ fp.close()
+
+ def unlink(self, fname):
+ os.unlink(fname)
+
+ 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
+
+ cand = self.hash.get(l, [])
+ if len(cand) > 1:
+ # resort our list of potentials forward then back.
+ cand.sort(key=lambda x: abs(x - linenum))
+ return cand
+
+ def hashlines(self):
+ self.hash = {}
+ for x, s in enumerate(self.lines):
+ self.hash.setdefault(s, []).append(x)
+
+ def makerejlines(self, fname):
+ base = os.path.basename(fname)
+ yield "--- %s\n+++ %s\n" % (base, base)
+ for x in self.rej:
+ for l in x.hunk:
+ yield l
+ if l[-1] != '\n':
+ yield "\n\ No newline at end of file\n"
+
+ 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
+
+ fname = self.fname + ".rej"
+ self.ui.warn(
+ _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
+ (len(self.rej), self.hunks, fname))
+
+ fp = self.opener(fname, 'w')
+ fp.writelines(self.makerejlines(self.fname))
+ fp.close()
+
+ def apply(self, h):
+ 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 self.missing:
+ self.rej.append(h)
+ return -1
+
+ 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():
+ self.unlink(self.fname)
+ else:
+ self.lines[:] = h.new()
+ self.offset += len(h.new())
+ self.dirty = 1
+ return 0
+
+ horig = h
+ if (self.eolmode in ('crlf', 'lf')
+ or self.eolmode == 'auto' and self.eol):
+ # If new eols are going to be normalized, then normalize
+ # hunk data before patching. Otherwise, preserve input
+ # line-endings.
+ h = h.getnormalized()
+
+ # 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 there's skew we want to emit the "(offset %d lines)" even
+ # when the hunk cleanly applies at start + skew, so skip the
+ # fast case code
+ if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
+ if h.rmfile():
+ self.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 + self.skew
+
+ 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.skew = l - orig_start
+ self.dirty = 1
+ offset = l - orig_start - fuzzlen
+ if fuzzlen:
+ msg = _("Hunk #%d succeeded at %d "
+ "with fuzz %d "
+ "(offset %d lines).\n")
+ self.printfile(True)
+ self.ui.warn(msg %
+ (h.number, l + 1, fuzzlen, offset))
+ else:
+ msg = _("Hunk #%d succeeded at %d "
+ "(offset %d lines).\n")
+ self.ui.note(msg % (h.number, l + 1, offset))
+ return fuzzlen
+ self.printfile(True)
+ self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
+ self.rej.append(horig)
+ return -1
+
+class hunk(object):
+ def __init__(self, desc, num, lr, context, create=False, remove=False):
+ self.number = num
+ self.desc = desc
+ self.hunk = [desc]
+ self.a = []
+ self.b = []
+ self.starta = self.lena = None
+ self.startb = self.lenb = None
+ if lr is not None:
+ if context:
+ self.read_context_hunk(lr)
+ else:
+ self.read_unified_hunk(lr)
+ self.create = create
+ self.remove = remove and not create
+
+ def getnormalized(self):
+ """Return a copy with line endings normalized to LF."""
+
+ def normalize(lines):
+ nlines = []
+ for line in lines:
+ if line.endswith('\r\n'):
+ line = line[:-2] + '\n'
+ nlines.append(line)
+ return nlines
+
+ # Dummy object, it is rebuilt manually
+ nh = hunk(self.desc, self.number, None, None, False, False)
+ nh.number = self.number
+ nh.desc = self.desc
+ nh.hunk = self.hunk
+ nh.a = normalize(self.a)
+ nh.b = normalize(self.b)
+ nh.starta = self.starta
+ nh.startb = self.startb
+ nh.lena = self.lena
+ nh.lenb = self.lenb
+ nh.create = self.create
+ nh.remove = self.remove
+ return nh
+
+ 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 is None:
+ self.lena = 1
+ else:
+ self.lena = int(self.lena)
+ if self.lenb is None:
+ self.lenb = 1
+ else:
+ self.lenb = int(self.lenb)
+ self.starta = int(self.starta)
+ self.startb = int(self.startb)
+ 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:
+ 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 is 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('---'):
+ # lines addition, old block is empty
+ 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 is 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('\ '):
+ # XXX: the only way to hit this is with an invalid line range.
+ # The no-eol marker is not counted in the line range, but I
+ # guess there are diff(1) out there which behave differently.
+ s = self.b[-1][:-1]
+ self.b[-1] = s
+ self.hunk[hunki - 1] = s
+ continue
+ if not l:
+ # line deletions, new block is empty and we hit EOF
+ 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:
+ # line deletions, new block is empty
+ 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 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 and self.create
+
+ def rmfile(self):
+ return self.startb == 0 and self.lenb == 0 and self.remove
+
+ 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 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, lr):
+ line = lr.readline()
+ self.hunk.append(line)
+ while line and not line.startswith('literal '):
+ line = lr.readline()
+ self.hunk.append(line)
+ if not line:
+ raise PatchError(_('could not extract binary patch'))
+ size = int(line[8:].rstrip())
+ dec = []
+ line = lr.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:-1])[:l])
+ line = lr.readline()
+ self.hunk.append(line)
+ text = zlib.decompress(''.join(dec))
+ if len(text) != size:
+ raise PatchError(_('binary patch is %d bytes, not %d') %
+ len(text), size)
+ self.text = text
+
+def parsefilename(str):
+ # --- filename \t|space stuff
+ s = str[4:].rstrip('\r\n')
+ i = s.find('\t')
+ if i < 0:
+ i = s.find(' ')
+ if i < 0:
+ return s
+ return s[:i]
+
+def pathstrip(path, strip):
+ pathlen = len(path)
+ i = 0
+ if strip == 0:
+ return '', path.rstrip()
+ count = strip
+ while count > 0:
+ i = path.find('/', i)
+ if i == -1:
+ raise PatchError(_("unable to strip away %d of %d dirs from %s") %
+ (count, strip, path))
+ i += 1
+ # consume '//' in the path
+ while i < pathlen - 1 and path[i] == '/':
+ i += 1
+ count -= 1
+ return path[:i].lstrip(), path[i:].rstrip()
+
+def selectfile(afile_orig, bfile_orig, hunk, strip):
+ nulla = afile_orig == "/dev/null"
+ nullb = bfile_orig == "/dev/null"
+ abase, afile = pathstrip(afile_orig, strip)
+ gooda = not nulla and os.path.lexists(afile)
+ bbase, bfile = pathstrip(bfile_orig, strip)
+ if afile == bfile:
+ goodb = gooda
+ else:
+ goodb = not nullb and os.path.lexists(bfile)
+ createfunc = hunk.createfile
+ missing = not goodb and not gooda and not createfunc()
+
+ # some diff programs apparently produce patches where the afile is
+ # not /dev/null, but afile starts with bfile
+ abasedir = afile[:afile.rfind('/') + 1]
+ bbasedir = bfile[:bfile.rfind('/') + 1]
+ if missing and abasedir == bbasedir and afile.startswith(bfile):
+ # this isn't very pretty
+ hunk.create = True
+ if createfunc():
+ missing = False
+ else:
+ hunk.create = False
+
+ # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
+ # diff is between a file and its backup. In this case, the original
+ # file should be patched (see original mpatch code).
+ isbackup = (abase == bbase and bfile.startswith(afile))
+ fname = None
+ if not missing:
+ if gooda and goodb:
+ fname = isbackup and afile or bfile
+ elif gooda:
+ fname = afile
+
+ if not fname:
+ if not nullb:
+ fname = isbackup and afile or bfile
+ elif not nulla:
+ fname = afile
+ else:
+ raise PatchError(_("undefined source and destination files"))
+
+ return fname, missing
+
+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, lr.textmode)
+ gitlr.push(firstline)
+ gitpatches = readgitpatch(gitlr)
+ fp.seek(pos)
+ return gitpatches
+
+def iterhunks(ui, fp, sourcefile=None):
+ """Read a patch and yield the following events:
+ - ("file", afile, bfile, firsthunk): select a new target file.
+ - ("hunk", hunk): a new hunk is ready to be applied, follows a
+ "file" event.
+ - ("git", gitchanges): current diff is in git format, gitchanges
+ maps filenames to gitpatch records. Unique event.
+ """
+ changed = {}
+ current_hunk = None
+ afile = ""
+ bfile = ""
+ state = None
+ hunknum = 0
+ emitfile = False
+ git = False
+
+ # our states
+ BFILE = 1
+ context = None
+ lr = linereader(fp)
+ # gitworkdone is True if a git operation (copy, rename, ...) was
+ # performed already for the current file. Useful when the file
+ # section may have no hunk.
+ gitworkdone = False
+
+ while True:
+ newfile = newgitfile = False
+ x = lr.readline()
+ if not x:
+ break
+ if current_hunk:
+ if x.startswith('\ '):
+ current_hunk.fix_newline()
+ yield 'hunk', current_hunk
+ current_hunk = None
+ if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
+ ((context is not False) and x.startswith('***************')))):
+ if context is None and x.startswith('***************'):
+ context = True
+ gpatch = changed.get(bfile)
+ 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)
+ hunknum += 1
+ if emitfile:
+ emitfile = False
+ yield 'file', (afile, bfile, current_hunk)
+ elif state == BFILE and x.startswith('GIT binary patch'):
+ current_hunk = binhunk(changed[bfile])
+ hunknum += 1
+ if emitfile:
+ emitfile = False
+ yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
+ current_hunk.extract(lr)
+ elif x.startswith('diff --git'):
+ # check for git diff, scanning the whole patch file if needed
+ m = gitre.match(x)
+ gitworkdone = False
+ if m:
+ afile, bfile = m.group(1, 2)
+ if not git:
+ git = True
+ gitpatches = scangitpatch(lr, x)
+ yield 'git', gitpatches
+ for gp in gitpatches:
+ changed[gp.path] = gp
+ # else error?
+ # copy/rename + modify should modify target, not source
+ gp = changed.get(bfile)
+ if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
+ or gp.mode):
+ afile = bfile
+ gitworkdone = True
+ newgitfile = 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)
+
+ if newfile:
+ gitworkdone = False
+
+ if newgitfile or newfile:
+ emitfile = True
+ state = BFILE
+ hunknum = 0
+ if current_hunk:
+ if current_hunk.complete():
+ yield 'hunk', current_hunk
+ else:
+ raise PatchError(_("malformed patch %s %s") % (afile,
+ current_hunk.desc))
+
+def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
+ """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.
+
+ If 'eolmode' is 'strict', the patch content and patched file are
+ read in binary mode. Otherwise, line endings are ignored when
+ patching then normalized according to 'eolmode'.
+
+ Callers probably want to call 'cmdutil.updatedir' after this to
+ apply certain categories of changes not done by this function.
+ """
+ return _applydiff(
+ ui, fp, patchfile, copyfile,
+ changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
+
+
+def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
+ sourcefile=None, eolmode='strict'):
+ rejects = 0
+ err = 0
+ current_file = None
+ cwd = os.getcwd()
+ opener = util.opener(cwd)
+
+ def closefile():
+ if not current_file:
+ return 0
+ if current_file.dirty:
+ current_file.writelines(current_file.fname, current_file.lines)
+ current_file.write_rej()
+ return len(current_file.rej)
+
+ for state, values in iterhunks(ui, fp, sourcefile):
+ if state == 'hunk':
+ if not current_file:
+ continue
+ ret = current_file.apply(values)
+ if ret >= 0:
+ changed.setdefault(current_file.fname, None)
+ if ret > 0:
+ err = 1
+ elif state == 'file':
+ rejects += closefile()
+ afile, bfile, first_hunk = values
+ try:
+ if sourcefile:
+ current_file = patcher(ui, sourcefile, opener,
+ eolmode=eolmode)
+ else:
+ current_file, missing = selectfile(afile, bfile,
+ first_hunk, strip)
+ current_file = patcher(ui, current_file, opener,
+ missing=missing, eolmode=eolmode)
+ except PatchError, err:
+ ui.warn(str(err) + '\n')
+ current_file = None
+ rejects += 1
+ continue
+ elif state == 'git':
+ for gp in values:
+ gp.path = pathstrip(gp.path, strip - 1)[1]
+ if gp.oldpath:
+ gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
+ # Binary patches really overwrite target files, copying them
+ # will just make it fails with "target file exists"
+ if gp.op in ('COPY', 'RENAME') and not gp.binary:
+ copyfn(gp.oldpath, gp.path, cwd)
+ changed[gp.path] = gp
+ else:
+ raise util.Abort(_('unsupported parser state: %s') % state)
+
+ rejects += closefile()
+
+ if rejects:
+ return -1
+ return err
+
+def externalpatch(patcher, patchname, ui, strip, cwd, files):
+ """use <patcher> to apply <patchname> to the working directory.
+ returns whether patch was applied with fuzz factor."""
+
+ fuzz = False
+ args = []
+ 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)
+ 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=None, eolmode='strict'):
+ """use builtin patch to apply <patchobj> to the working directory.
+ returns whether patch was applied with fuzz factor."""
+
+ if files is None:
+ files = {}
+ if eolmode is None:
+ eolmode = ui.config('patch', 'eol', 'strict')
+ if eolmode.lower() not in eolmodes:
+ raise util.Abort(_('unsupported line endings type: %s') % eolmode)
+ eolmode = eolmode.lower()
+
+ try:
+ fp = open(patchobj, 'rb')
+ except TypeError:
+ fp = patchobj
+ if cwd:
+ curdir = os.getcwd()
+ os.chdir(cwd)
+ try:
+ ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
+ finally:
+ if cwd:
+ os.chdir(curdir)
+ if fp != patchobj:
+ fp.close()
+ if ret < 0:
+ raise PatchError(_('patch failed to apply'))
+ return ret > 0
+
+def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
+ """Apply <patchname> to the working directory.
+
+ 'eolmode' specifies how end of lines should be handled. It can be:
+ - 'strict': inputs are read in binary mode, EOLs are preserved
+ - 'crlf': EOLs are ignored when patching and reset to CRLF
+ - 'lf': EOLs are ignored when patching and reset to LF
+ - None: get it from user settings, default to 'strict'
+ 'eolmode' is ignored when using an external patcher program.
+
+ Returns whether patch was applied with fuzz factor.
+ """
+ patcher = ui.config('ui', 'patch')
+ if files is None:
+ files = {}
+ try:
+ if patcher:
+ return externalpatch(patcher, patchname, ui, strip, cwd, files)
+ return internalpatch(patchname, ui, strip, cwd, files, eolmode)
+ except PatchError, err:
+ raise util.Abort(str(err))
+
+def b85diff(to, tn):
+ '''print base85-encoded binary diff'''
+ def gitindex(text):
+ if not text:
+ return hex(nullid)
+ l = len(text)
+ s = util.sha1('blob %d\0' % l)
+ s.update(text)
+ return s.hexdigest()
+
+ def fmtline(line):
+ l = len(line)
+ if l <= 26:
+ l = chr(ord('A') + l - 1)
+ else:
+ l = chr(l - 26 + ord('a') - 1)
+ return '%c%s\n' % (l, base85.b85encode(line, True))
+
+ def chunk(text, csize=52):
+ l = len(text)
+ i = 0
+ while i < l:
+ yield text[i:i + csize]
+ i += csize
+
+ tohash = gitindex(to)
+ tnhash = gitindex(tn)
+ if tohash == tnhash:
+ return ""
+
+ # TODO: deltas
+ ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
+ (tohash, tnhash, len(tn))]
+ for l in chunk(zlib.compress(tn)):
+ ret.append(fmtline(l))
+ ret.append('\n')
+ return ''.join(ret)
+
+class GitDiffRequired(Exception):
+ pass
+
+def diffopts(ui, opts=None, untrusted=False):
+ def get(key, name=None, getter=ui.configbool):
+ return ((opts and opts.get(key)) or
+ getter('diff', name or key, None, untrusted=untrusted))
+ return mdiff.diffopts(
+ text=opts and opts.get('text'),
+ git=get('git'),
+ nodates=get('nodates'),
+ showfunc=get('show_function', 'showfunc'),
+ ignorews=get('ignore_all_space', 'ignorews'),
+ ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
+ ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
+ context=get('unified', getter=ui.config))
+
+def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
+ losedatafn=None, prefix=''):
+ '''yields 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.
+
+ losedatafn(**kwarg) is a callable run when opts.upgrade=True and
+ every time some change cannot be represented with the current
+ patch format. Return False to upgrade to git patch format, True to
+ accept the loss or raise an exception to abort the diff. It is
+ called with the name of current file being diffed as 'fn'. If set
+ to None, patches will always be upgraded to git format when
+ necessary.
+
+ prefix is a filename prefix that is prepended to all filenames on
+ display (used for subrepos).
+ '''
+
+ if opts is None:
+ opts = mdiff.defaultopts
+
+ if not node1 and not node2:
+ node1 = repo.dirstate.parents()[0]
+
+ def lrugetfilectx():
+ cache = {}
+ order = []
+ def getfilectx(f, ctx):
+ fctx = ctx.filectx(f, filelog=cache.get(f))
+ if f not in cache:
+ if len(cache) > 20:
+ del cache[order.pop(0)]
+ cache[f] = fctx.filelog()
+ else:
+ order.remove(f)
+ order.append(f)
+ return fctx
+ return getfilectx
+ getfilectx = lrugetfilectx()
+
+ ctx1 = repo[node1]
+ ctx2 = repo[node2]
+
+ if not changes:
+ changes = repo.status(ctx1, ctx2, match=match)
+ modified, added, removed = changes[:3]
+
+ if not modified and not added and not removed:
+ return []
+
+ revs = None
+ if not repo.ui.quiet:
+ hexfunc = repo.ui.debugflag and hex or short
+ revs = [hexfunc(node) for node in [node1, node2] if node]
+
+ copy = {}
+ if opts.git or opts.upgrade:
+ copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
+
+ difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
+ modified, added, removed, copy, getfilectx, opts, losedata, prefix)
+ if opts.upgrade and not opts.git:
+ try:
+ def losedata(fn):
+ if not losedatafn or not losedatafn(fn=fn):
+ raise GitDiffRequired()
+ # Buffer the whole output until we are sure it can be generated
+ return list(difffn(opts.copy(git=False), losedata))
+ except GitDiffRequired:
+ return difffn(opts.copy(git=True), None)
+ else:
+ return difffn(opts, None)
+
+def difflabel(func, *args, **kw):
+ '''yields 2-tuples of (output, label) based on the output of func()'''
+ prefixes = [('diff', 'diff.diffline'),
+ ('copy', 'diff.extended'),
+ ('rename', 'diff.extended'),
+ ('old', 'diff.extended'),
+ ('new', 'diff.extended'),
+ ('deleted', 'diff.extended'),
+ ('---', 'diff.file_a'),
+ ('+++', 'diff.file_b'),
+ ('@@', 'diff.hunk'),
+ ('-', 'diff.deleted'),
+ ('+', 'diff.inserted')]
+
+ for chunk in func(*args, **kw):
+ lines = chunk.split('\n')
+ for i, line in enumerate(lines):
+ if i != 0:
+ yield ('\n', '')
+ stripline = line
+ if line and line[0] in '+-':
+ # highlight trailing whitespace, but only in changed lines
+ stripline = line.rstrip()
+ for prefix, label in prefixes:
+ if stripline.startswith(prefix):
+ yield (stripline, label)
+ break
+ else:
+ yield (line, '')
+ if line != stripline:
+ yield (line[len(stripline):], 'diff.trailingwhitespace')
+
+def diffui(*args, **kw):
+ '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
+ return difflabel(diff, *args, **kw)
+
+
+def _addmodehdr(header, omode, nmode):
+ if omode != nmode:
+ header.append('old mode %s\n' % omode)
+ header.append('new mode %s\n' % nmode)
+
+def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
+ copy, getfilectx, opts, losedatafn, prefix):
+
+ def join(f):
+ return os.path.join(prefix, f)
+
+ date1 = util.datestr(ctx1.date())
+ man1 = ctx1.manifest()
+
+ gone = set()
+ gitmode = {'l': '120000', 'x': '100755', '': '100644'}
+
+ copyto = dict([(v, k) for k, v in copy.items()])
+
+ if opts.git:
+ revs = None
+
+ for f in sorted(modified + added + removed):
+ to = None
+ tn = None
+ dodiff = True
+ header = []
+ if f in man1:
+ to = getfilectx(f, ctx1).data()
+ if f not in removed:
+ tn = getfilectx(f, ctx2).data()
+ a, b = f, f
+ if opts.git or losedatafn:
+ if f in added:
+ mode = gitmode[ctx2.flags(f)]
+ if f in copy or f in copyto:
+ if opts.git:
+ if f in copy:
+ a = copy[f]
+ else:
+ a = copyto[f]
+ omode = gitmode[man1.flags(a)]
+ _addmodehdr(header, omode, mode)
+ if a in removed and a not in gone:
+ op = 'rename'
+ gone.add(a)
+ else:
+ op = 'copy'
+ header.append('%s from %s\n' % (op, join(a)))
+ header.append('%s to %s\n' % (op, join(f)))
+ to = getfilectx(a, ctx1).data()
+ else:
+ losedatafn(f)
+ else:
+ if opts.git:
+ header.append('new file mode %s\n' % mode)
+ elif ctx2.flags(f):
+ losedatafn(f)
+ # In theory, if tn was copied or renamed we should check
+ # if the source is binary too but the copy record already
+ # forces git mode.
+ if util.binary(tn):
+ if opts.git:
+ dodiff = 'binary'
+ else:
+ losedatafn(f)
+ if not opts.git and not tn:
+ # regular diffs cannot represent new empty file
+ losedatafn(f)
+ elif f in removed:
+ if opts.git:
+ # have we already reported a copy above?
+ if ((f in copy and copy[f] in added
+ and copyto[copy[f]] == f) or
+ (f in copyto and copyto[f] in added
+ and copy[copyto[f]] == f)):
+ dodiff = False
+ else:
+ header.append('deleted file mode %s\n' %
+ gitmode[man1.flags(f)])
+ elif not to or util.binary(to):
+ # regular diffs cannot represent empty file deletion
+ losedatafn(f)
+ else:
+ oflag = man1.flags(f)
+ nflag = ctx2.flags(f)
+ binary = util.binary(to) or util.binary(tn)
+ if opts.git:
+ _addmodehdr(header, gitmode[oflag], gitmode[nflag])
+ if binary:
+ dodiff = 'binary'
+ elif binary or nflag != oflag:
+ losedatafn(f)
+ if opts.git:
+ header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
+
+ if dodiff:
+ if dodiff == 'binary':
+ text = b85diff(to, tn)
+ else:
+ text = mdiff.unidiff(to, date1,
+ # ctx2 date may be dynamic
+ tn, util.datestr(ctx2.date()),
+ join(a), join(b), revs, opts=opts)
+ if header and (text or len(header) > 1):
+ yield ''.join(header)
+ if text:
+ yield text
+
+def diffstatdata(lines):
+ filename, adds, removes = None, 0, 0
+ for line in lines:
+ if line.startswith('diff'):
+ if filename:
+ isbinary = adds == 0 and removes == 0
+ yield (filename, adds, removes, isbinary)
+ # set numbers to 0 anyway when starting new file
+ adds, removes = 0, 0
+ if line.startswith('diff --git'):
+ filename = gitre.search(line).group(1)
+ else:
+ # format: "diff -r ... -r ... filename"
+ filename = line.split(None, 5)[-1]
+ elif line.startswith('+') and not line.startswith('+++'):
+ adds += 1
+ elif line.startswith('-') and not line.startswith('---'):
+ removes += 1
+ if filename:
+ isbinary = adds == 0 and removes == 0
+ yield (filename, adds, removes, isbinary)
+
+def diffstat(lines, width=80, git=False):
+ output = []
+ stats = list(diffstatdata(lines))
+
+ maxtotal, maxname = 0, 0
+ totaladds, totalremoves = 0, 0
+ hasbinary = False
+
+ sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
+ for filename, adds, removes, isbinary in stats]
+
+ for filename, adds, removes, isbinary, namewidth in sized:
+ totaladds += adds
+ totalremoves += removes
+ maxname = max(maxname, namewidth)
+ maxtotal = max(maxtotal, adds + removes)
+ if isbinary:
+ hasbinary = True
+
+ countwidth = len(str(maxtotal))
+ if hasbinary and countwidth < 3:
+ countwidth = 3
+ graphwidth = width - countwidth - maxname - 6
+ if graphwidth < 10:
+ graphwidth = 10
+
+ def scale(i):
+ if maxtotal <= graphwidth:
+ return i
+ # If diffstat runs out of room it doesn't print anything,
+ # which isn't very useful, so always print at least one + or -
+ # if there were at least some changes.
+ return max(i * graphwidth // maxtotal, int(bool(i)))
+
+ for filename, adds, removes, isbinary, namewidth in sized:
+ if git and isbinary:
+ count = 'Bin'
+ else:
+ count = adds + removes
+ pluses = '+' * scale(adds)
+ minuses = '-' * scale(removes)
+ output.append(' %s%s | %*s %s%s\n' %
+ (filename, ' ' * (maxname - namewidth),
+ countwidth, count,
+ pluses, minuses))
+
+ if stats:
+ output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
+ % (len(stats), totaladds, totalremoves))
+
+ return ''.join(output)
+
+def diffstatui(*args, **kw):
+ '''like diffstat(), but yields 2-tuples of (output, label) for
+ ui.write()
+ '''
+
+ for line in diffstat(*args, **kw).splitlines():
+ if line and line[-1] in '+-':
+ name, graph = line.rsplit(' ', 1)
+ yield (name + ' ', '')
+ m = re.search(r'\++', graph)
+ if m:
+ yield (m.group(0), 'diffstat.inserted')
+ m = re.search(r'-+', graph)
+ if m:
+ yield (m.group(0), 'diffstat.deleted')
+ else:
+ yield (line, '')
+ yield ('\n', '')