patch.py
663 lines
| 21.7 KiB
| text/x-python
|
PythonLexer
/ mercurial / patch.py
|
r2861 | # patch.py - patch file parsing routines | ||
# | ||||
|
r2865 | # Copyright 2006 Brendan Cully <brendan@kublai.com> | ||
# | ||||
|
r2861 | # This software may be used and distributed according to the terms | ||
# of the GNU General Public License, incorporated herein by reference. | ||||
|
r3891 | from i18n import _ | ||
|
r2874 | from node import * | ||
|
r3967 | import base85, cmdutil, mdiff, util, context, revlog | ||
|
r3963 | import cStringIO, email.Parser, os, popen2, re, sha | ||
|
r3877 | import sys, tempfile, zlib | ||
|
r2866 | |||
|
r2933 | # helper functions | ||
def copyfile(src, dst, basedir=None): | ||||
if not basedir: | ||||
basedir = os.getcwd() | ||||
abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)] | ||||
if os.path.exists(absdst): | ||||
raise util.Abort(_("cannot create %s: destination already exists") % | ||||
dst) | ||||
targetdir = os.path.dirname(absdst) | ||||
if not os.path.isdir(targetdir): | ||||
os.makedirs(targetdir) | ||||
|
r3629 | |||
util.copyfile(abssrc, absdst) | ||||
|
r2933 | |||
# public functions | ||||
|
r2866 | def extract(ui, fileobj): | ||
'''extract patch from data read from fileobj. | ||||
|
r4263 | patch can be a normal patch or contained in an email message. | ||
|
r2866 | |||
|
r4263 | return tuple (filename, message, user, date, 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.''' | ||||
|
r2866 | |||
# attempt to detect the start of a patch | ||||
# (this heuristic is borrowed from quilt) | ||||
diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' + | ||||
'retrieving revision [0-9]+(\.[0-9]+)*$|' + | ||||
'(---|\*\*\*)[ \t])', re.MULTILINE) | ||||
fd, tmpname = tempfile.mkstemp(prefix='hg-patch-') | ||||
tmpfp = os.fdopen(fd, 'w') | ||||
try: | ||||
msg = email.Parser.Parser().parse(fileobj) | ||||
message = msg['Subject'] | ||||
user = msg['From'] | ||||
# should try to parse msg['Date'] | ||||
date = None | ||||
|
r4263 | nodeid = None | ||
parents = [] | ||||
|
r2866 | |||
if message: | ||||
|
r4201 | if message.startswith('[PATCH'): | ||
pend = message.find(']') | ||||
|
r4208 | if pend >= 0: | ||
message = message[pend+1:].lstrip() | ||||
|
r2866 | message = message.replace('\n\t', ' ') | ||
ui.debug('Subject: %s\n' % message) | ||||
if user: | ||||
ui.debug('From: %s\n' % user) | ||||
diffs_seen = 0 | ||||
ok_types = ('text/plain', 'text/x-diff', 'text/x-patch') | ||||
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: | ||||
|
r4220 | hgpatch = False | ||
ignoretext = False | ||||
|
r2866 | ui.debug(_('found patch at byte %d\n') % m.start(0)) | ||
diffs_seen += 1 | ||||
cfp = cStringIO.StringIO() | ||||
if message: | ||||
cfp.write(message) | ||||
cfp.write('\n') | ||||
for line in payload[:m.start(0)].splitlines(): | ||||
if line.startswith('# HG changeset patch'): | ||||
ui.debug(_('patch generated by hg export\n')) | ||||
hgpatch = True | ||||
# drop earlier commit message content | ||||
cfp.seek(0) | ||||
cfp.truncate() | ||||
elif hgpatch: | ||||
if line.startswith('# User '): | ||||
user = line[7:] | ||||
ui.debug('From: %s\n' % user) | ||||
elif line.startswith("# Date "): | ||||
date = line[7:] | ||||
|
r4263 | elif line.startswith("# Node ID "): | ||
nodeid = line[10:] | ||||
elif line.startswith("# Parent "): | ||||
parents.append(line[10:]) | ||||
|
r4220 | elif line == '---' and 'git-send-email' in msg['X-Mailer']: | ||
ignoretext = True | ||||
if not line.startswith('# ') and not ignoretext: | ||||
|
r2866 | 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 | ||||
tmpfp.close() | ||||
if not diffs_seen: | ||||
os.unlink(tmpname) | ||||
|
r4263 | return None, message, user, date, None, None, None | ||
p1 = parents and parents.pop(0) or None | ||||
p2 = parents and parents.pop(0) or None | ||||
return tmpname, message, user, date, nodeid, p1, p2 | ||||
|
r2861 | |||
|
r3716 | GP_PATCH = 1 << 0 # we have to run patch | ||
GP_FILTER = 1 << 1 # there's some copy/rename operation | ||||
GP_BINARY = 1 << 2 # there's a binary patch | ||||
|
r2861 | def readgitpatch(patchname): | ||
"""extract git-style metadata about patches from <patchname>""" | ||||
class gitpatch: | ||||
"op is one of ADD, DELETE, RENAME, MODIFY or COPY" | ||||
def __init__(self, path): | ||||
self.path = path | ||||
self.oldpath = None | ||||
self.mode = None | ||||
self.op = 'MODIFY' | ||||
self.copymod = False | ||||
self.lineno = 0 | ||||
|
r3367 | self.binary = False | ||
|
r3223 | |||
|
r2861 | # 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 | ||||
|
r3716 | dopatch = 0 | ||
|
r2861 | |||
lineno = 0 | ||||
for line in pf: | ||||
lineno += 1 | ||||
if line.startswith('diff --git'): | ||||
m = gitre.match(line) | ||||
if m: | ||||
if gp: | ||||
gitpatches.append(gp) | ||||
|
r3673 | src, dst = m.group(1, 2) | ||
|
r2861 | gp = gitpatch(dst) | ||
gp.lineno = lineno | ||||
elif gp: | ||||
if line.startswith('--- '): | ||||
if gp.op in ('COPY', 'RENAME'): | ||||
gp.copymod = True | ||||
|
r3716 | dopatch |= GP_FILTER | ||
|
r2861 | gitpatches.append(gp) | ||
gp = None | ||||
|
r3716 | dopatch |= GP_PATCH | ||
|
r2861 | continue | ||
if line.startswith('rename from '): | ||||
gp.op = 'RENAME' | ||||
gp.oldpath = line[12:].rstrip() | ||||
elif line.startswith('rename to '): | ||||
gp.path = line[10:].rstrip() | ||||
elif line.startswith('copy from '): | ||||
gp.op = 'COPY' | ||||
gp.oldpath = line[10:].rstrip() | ||||
elif line.startswith('copy to '): | ||||
gp.path = line[8:].rstrip() | ||||
elif line.startswith('deleted file'): | ||||
gp.op = 'DELETE' | ||||
elif line.startswith('new file mode '): | ||||
gp.op = 'ADD' | ||||
gp.mode = int(line.rstrip()[-3:], 8) | ||||
elif line.startswith('new mode '): | ||||
gp.mode = int(line.rstrip()[-3:], 8) | ||||
|
r3367 | elif line.startswith('GIT binary patch'): | ||
|
r3716 | dopatch |= GP_BINARY | ||
|
r3367 | gp.binary = True | ||
|
r2861 | if gp: | ||
gitpatches.append(gp) | ||||
if not gitpatches: | ||||
|
r3716 | dopatch = GP_PATCH | ||
|
r2861 | |||
return (dopatch, gitpatches) | ||||
|
r3055 | def dogitpatch(patchname, gitpatches, cwd=None): | ||
|
r2861 | """Preprocess git patch so that vanilla patch can handle it""" | ||
|
r3367 | def extractbin(fp): | ||
|
r3717 | i = [0] # yuck | ||
def readline(): | ||||
i[0] += 1 | ||||
return fp.readline().rstrip() | ||||
line = readline() | ||||
|
r3367 | while line and not line.startswith('literal '): | ||
|
r3717 | line = readline() | ||
|
r3367 | if not line: | ||
|
r3717 | return None, i[0] | ||
|
r3374 | size = int(line[8:]) | ||
|
r3367 | dec = [] | ||
|
r3717 | line = readline() | ||
|
r3367 | while line: | ||
|
r3374 | 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]) | ||||
|
r3717 | line = readline() | ||
|
r3367 | text = zlib.decompress(''.join(dec)) | ||
if len(text) != size: | ||||
raise util.Abort(_('binary patch is %d bytes, not %d') % | ||||
(len(text), size)) | ||||
|
r3717 | return text, i[0] | ||
|
r3367 | |||
|
r2861 | pf = file(patchname) | ||
pfline = 1 | ||||
fd, patchname = tempfile.mkstemp(prefix='hg-patch-') | ||||
tmpfp = os.fdopen(fd, 'w') | ||||
try: | ||||
|
r3473 | for i in xrange(len(gitpatches)): | ||
|
r2861 | p = gitpatches[i] | ||
|
r3367 | if not p.copymod and not p.binary: | ||
|
r2861 | continue | ||
# rewrite patch hunk | ||||
while pfline < p.lineno: | ||||
tmpfp.write(pf.readline()) | ||||
pfline += 1 | ||||
|
r3367 | |||
if p.binary: | ||||
|
r3717 | text, delta = extractbin(pf) | ||
|
r3367 | if not text: | ||
raise util.Abort(_('binary patch extraction failed')) | ||||
|
r3717 | pfline += delta | ||
|
r3367 | 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)) | ||||
|
r2861 | line = pf.readline() | ||
pfline += 1 | ||||
|
r3367 | while not line.startswith('--- a/'): | ||
tmpfp.write(line) | ||||
line = pf.readline() | ||||
pfline += 1 | ||||
tmpfp.write('--- a/%s\n' % p.path) | ||||
|
r2861 | |||
line = pf.readline() | ||||
while line: | ||||
tmpfp.write(line) | ||||
line = pf.readline() | ||||
except: | ||||
tmpfp.close() | ||||
os.unlink(patchname) | ||||
raise | ||||
tmpfp.close() | ||||
return patchname | ||||
|
r3465 | def patch(patchname, ui, strip=1, cwd=None, files={}): | ||
|
r2861 | """apply the patch <patchname> to the working directory. | ||
a list of patched files is returned""" | ||||
|
r3057 | # helper function | ||
def __patch(patchname): | ||||
"""patch and updates the files and fuzz variables""" | ||||
fuzz = False | ||||
|
r2861 | |||
args = [] | ||||
|
r4435 | patcher = ui.config('ui', 'patch') | ||
if not patcher: | ||||
patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''), | ||||
'patch') | ||||
if util.needbinarypatch(): | ||||
args.append('--binary') | ||||
|
r4434 | |||
|
r2861 | if cwd: | ||
args.append('-d %s' % util.shellquote(cwd)) | ||||
fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, | ||||
util.shellquote(patchname))) | ||||
for line in fp: | ||||
line = line.rstrip() | ||||
|
r2919 | ui.note(line + '\n') | ||
|
r2861 | if line.startswith('patching file '): | ||
pf = util.parse_patch_output(line) | ||||
|
r2919 | printed_file = False | ||
|
r2861 | files.setdefault(pf, (None, None)) | ||
|
r2919 | 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') | ||||
|
r2861 | code = fp.close() | ||
if code: | ||||
|
r2868 | raise util.Abort(_("patch command failed: %s") % | ||
util.explain_exit(code)[0]) | ||||
|
r3465 | return fuzz | ||
|
r3057 | |||
(dopatch, gitpatches) = readgitpatch(patchname) | ||||
|
r3465 | for gp in gitpatches: | ||
files[gp.path] = (gp.op, gp) | ||||
|
r3057 | |||
|
r3465 | fuzz = False | ||
|
r3057 | if dopatch: | ||
|
r3716 | filterpatch = dopatch & (GP_FILTER | GP_BINARY) | ||
if filterpatch: | ||||
|
r3057 | patchname = dogitpatch(patchname, gitpatches, cwd=cwd) | ||
try: | ||||
|
r3716 | if dopatch & GP_PATCH: | ||
|
r3465 | fuzz = __patch(patchname) | ||
|
r3057 | finally: | ||
|
r3716 | if filterpatch: | ||
|
r3057 | os.unlink(patchname) | ||
|
r2861 | |||
|
r3465 | return fuzz | ||
|
r2874 | |||
|
r3554 | def diffopts(ui, opts={}, untrusted=False): | ||
def get(key, name=None): | ||||
return (opts.get(key) or | ||||
ui.configbool('diff', name or key, None, untrusted=untrusted)) | ||||
|
r2888 | return mdiff.diffopts( | ||
text=opts.get('text'), | ||||
|
r3554 | 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')) | ||||
|
r2888 | |||
|
r2933 | def updatedir(ui, repo, patches, wlock=None): | ||
'''Update dirstate after patch application according to metadata''' | ||||
if not patches: | ||||
return | ||||
copies = [] | ||||
|
r3701 | removes = {} | ||
|
r2933 | cfiles = patches.keys() | ||
cwd = repo.getcwd() | ||||
if cwd: | ||||
|
r4229 | cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()] | ||
|
r2933 | for f in patches: | ||
ctype, gp = patches[f] | ||||
if ctype == 'RENAME': | ||||
copies.append((gp.oldpath, gp.path, gp.copymod)) | ||||
|
r3701 | removes[gp.oldpath] = 1 | ||
|
r2933 | elif ctype == 'COPY': | ||
copies.append((gp.oldpath, gp.path, gp.copymod)) | ||||
elif ctype == 'DELETE': | ||||
|
r3701 | removes[gp.path] = 1 | ||
|
r2933 | for src, dst, after in copies: | ||
if not after: | ||||
copyfile(src, dst, repo.root) | ||||
repo.copy(src, dst, wlock=wlock) | ||||
|
r3701 | removes = removes.keys() | ||
|
r2933 | if removes: | ||
|
r3701 | removes.sort() | ||
|
r2933 | repo.remove(removes, True, wlock=wlock) | ||
for f in patches: | ||||
ctype, gp = patches[f] | ||||
if gp and gp.mode: | ||||
x = gp.mode & 0100 != 0 | ||||
dst = os.path.join(repo.root, gp.path) | ||||
|
r3588 | # patch won't create empty files | ||
if ctype == 'ADD' and not os.path.exists(dst): | ||||
|
r4006 | repo.wwrite(gp.path, '', x and 'x' or '') | ||
else: | ||||
util.set_exec(dst, x) | ||||
|
r2933 | cmdutil.addremove(repo, cfiles, wlock=wlock) | ||
files = patches.keys() | ||||
files.extend([r for r in removes if r not in files]) | ||||
files.sort() | ||||
return files | ||||
|
r3367 | def b85diff(fp, to, tn): | ||
'''print base85-encoded binary diff''' | ||||
def gitindex(text): | ||||
if not text: | ||||
return '0' * 40 | ||||
l = len(text) | ||||
s = sha.new('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 | ||||
|
r4105 | tohash = gitindex(to) | ||
tnhash = gitindex(tn) | ||||
if tohash == tnhash: | ||||
|
r4106 | return "" | ||
|
r3367 | # TODO: deltas | ||
|
r4106 | 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) | ||||
|
r3367 | |||
|
r2874 | 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] | ||||
|
r2934 | |||
|
r3967 | ccache = {} | ||
def getctx(r): | ||||
if r not in ccache: | ||||
ccache[r] = context.changectx(repo, r) | ||||
return ccache[r] | ||||
flcache = {} | ||||
def getfilectx(f, ctx): | ||||
flctx = ctx.filectx(f, filelog=flcache.get(f)) | ||||
if f not in flcache: | ||||
flcache[f] = flctx._filelog | ||||
return flctx | ||||
|
r2934 | |||
|
r2874 | # reading the data for node1 early allows it to play nicely | ||
|
r2875 | # with repo.status and the revlog cache. | ||
|
r3967 | ctx1 = context.changectx(repo, node1) | ||
# force manifest reading | ||||
man1 = ctx1.manifest() | ||||
date1 = util.datestr(ctx1.date()) | ||||
|
r2874 | |||
if not changes: | ||||
|
r2875 | changes = repo.status(node1, node2, files, match=match)[:5] | ||
|
r2874 | modified, added, removed, deleted, unknown = changes | ||
if not modified and not added and not removed: | ||||
return | ||||
|
r3967 | if node2: | ||
ctx2 = context.changectx(repo, node2) | ||||
else: | ||||
ctx2 = context.workingctx(repo) | ||||
man2 = ctx2.manifest() | ||||
# returns False if there was no rename between ctx1 and ctx2 | ||||
# returns None if the file was created between ctx1 and ctx2 | ||||
# returns the (file, node) present in ctx1 that was renamed to f in ctx2 | ||||
def renamed(f): | ||||
startrev = ctx1.rev() | ||||
c = ctx2 | ||||
crev = c.rev() | ||||
if crev is None: | ||||
crev = repo.changelog.count() | ||||
|
r3694 | orig = f | ||
|
r3967 | while crev > startrev: | ||
if f in c.files(): | ||||
|
r3693 | try: | ||
|
r3967 | src = getfilectx(f, c).renamed() | ||
except revlog.LookupError: | ||||
|
r3693 | return None | ||
if src: | ||||
f = src[0] | ||||
|
r3967 | crev = c.parents()[0].rev() | ||
# try to reuse | ||||
c = getctx(crev) | ||||
if f not in man1: | ||||
|
r3694 | return None | ||
|
r3696 | if f == orig: | ||
return False | ||||
|
r3967 | return f | ||
|
r2874 | |||
if repo.ui.quiet: | ||||
r = None | ||||
else: | ||||
|
r3387 | hexfunc = repo.ui.debugflag and hex or short | ||
|
r2874 | r = [hexfunc(node) for node in [node1, node2] if node] | ||
|
r2907 | if opts.git: | ||
copied = {} | ||||
for f in added: | ||||
src = renamed(f) | ||||
if src: | ||||
copied[f] = src | ||||
|
r3967 | srcs = [x[1] for x in copied.items()] | ||
|
r2907 | |||
|
r2874 | all = modified + added + removed | ||
all.sort() | ||||
|
r3702 | gone = {} | ||
|
r3996 | |||
|
r2874 | for f in all: | ||
to = None | ||||
tn = None | ||||
|
r2907 | dodiff = True | ||
|
r3329 | header = [] | ||
|
r3967 | if f in man1: | ||
to = getfilectx(f, ctx1).data() | ||||
|
r2874 | if f not in removed: | ||
|
r3967 | tn = getfilectx(f, ctx2).data() | ||
|
r2907 | if opts.git: | ||
def gitmode(x): | ||||
return x and '100755' or '100644' | ||||
def addmodehdr(header, omode, nmode): | ||||
if omode != nmode: | ||||
header.append('old mode %s\n' % omode) | ||||
header.append('new mode %s\n' % nmode) | ||||
a, b = f, f | ||||
if f in added: | ||||
|
r3967 | mode = gitmode(man2.execf(f)) | ||
|
r2907 | if f in copied: | ||
|
r3967 | a = copied[f] | ||
omode = gitmode(man1.execf(a)) | ||||
|
r2907 | addmodehdr(header, omode, mode) | ||
|
r3702 | if a in removed and a not in gone: | ||
op = 'rename' | ||||
gone[a] = 1 | ||||
else: | ||||
op = 'copy' | ||||
|
r2907 | header.append('%s from %s\n' % (op, a)) | ||
header.append('%s to %s\n' % (op, f)) | ||||
|
r3967 | to = getfilectx(a, ctx1).data() | ||
|
r2907 | else: | ||
header.append('new file mode %s\n' % mode) | ||||
|
r4092 | if util.binary(tn): | ||
dodiff = 'binary' | ||||
|
r2907 | elif f in removed: | ||
if f in srcs: | ||||
dodiff = False | ||||
else: | ||||
|
r3967 | mode = gitmode(man1.execf(f)) | ||
|
r2907 | header.append('deleted file mode %s\n' % mode) | ||
else: | ||||
|
r3967 | omode = gitmode(man1.execf(f)) | ||
nmode = gitmode(man2.execf(f)) | ||||
|
r2907 | addmodehdr(header, omode, nmode) | ||
|
r3367 | if util.binary(to) or util.binary(tn): | ||
dodiff = 'binary' | ||||
|
r2907 | r = None | ||
|
r3329 | header.insert(0, 'diff --git a/%s b/%s\n' % (a, b)) | ||
|
r4106 | if dodiff: | ||
if dodiff == 'binary': | ||||
text = b85diff(fp, to, tn) | ||||
else: | ||||
|
r4108 | text = mdiff.unidiff(to, date1, | ||
# ctx2 date may be dynamic | ||||
tn, util.datestr(ctx2.date()), | ||||
f, r, opts=opts) | ||||
|
r3329 | if text or len(header) > 1: | ||
|
r2907 | fp.write(''.join(header)) | ||
|
r3329 | fp.write(text) | ||
|
r2874 | |||
def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False, | ||||
opts=None): | ||||
'''export changesets as hg patches.''' | ||||
total = len(revs) | ||||
|
r3900 | revwidth = max([len(str(rev)) for rev in revs]) | ||
|
r2874 | |||
|
r3970 | def single(rev, seqno, fp): | ||
ctx = repo.changectx(rev) | ||||
node = ctx.node() | ||||
parents = [p.node() for p in ctx.parents() if p] | ||||
|
r2874 | if switch_parent: | ||
parents.reverse() | ||||
prev = (parents and parents[0]) or nullid | ||||
if not fp: | ||||
fp = cmdutil.make_file(repo, template, node, total=total, | ||||
seqno=seqno, revwidth=revwidth) | ||||
|
r4125 | if fp != sys.stdout and hasattr(fp, 'name'): | ||
|
r2874 | repo.ui.note("%s\n" % fp.name) | ||
fp.write("# HG changeset patch\n") | ||||
|
r3970 | fp.write("# User %s\n" % ctx.user()) | ||
fp.write("# Date %d %d\n" % ctx.date()) | ||||
|
r2874 | 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])) | ||||
|
r3970 | fp.write(ctx.description().rstrip()) | ||
|
r2874 | fp.write("\n\n") | ||
diff(repo, prev, node, fp=fp, opts=opts) | ||||
if fp not in (sys.stdout, repo.ui): | ||||
fp.close() | ||||
|
r3900 | for seqno, rev in enumerate(revs): | ||
|
r3970 | single(rev, seqno+1, fp) | ||
|
r3096 | |||
def diffstat(patchlines): | ||||
|
r4316 | if not util.find_in_path('diffstat', os.environ.get('PATH', '')): | ||
return | ||||
|
r3096 | fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt") | ||
try: | ||||
p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name) | ||||
try: | ||||
for line in patchlines: print >> p.tochild, line | ||||
p.tochild.close() | ||||
if p.wait(): return | ||||
fp = os.fdopen(fd, 'r') | ||||
stat = [] | ||||
for line in fp: stat.append(line.lstrip()) | ||||
last = stat.pop() | ||||
stat.insert(0, last) | ||||
stat = ''.join(stat) | ||||
if stat.startswith('0 files'): raise ValueError | ||||
return stat | ||||
except: raise | ||||
finally: | ||||
try: os.unlink(name) | ||||
except: pass | ||||