6
6
# This software may be used and distributed according to the terms of the
7
7
# GNU General Public License version 2 or any later version.
9
import cStringIO, email.Parser, os, re
10
13
from node import hex, nullid, short
11
14
import base85, cmdutil, mdiff, util, diffhelpers, copies
12
import cStringIO, email.Parser, os, re
13
import sys, tempfile, zlib
15
16
gitre = re.compile('diff --git a/(.*) b/(.*)')
343
347
gp.path = line[8:]
344
348
elif line.startswith('deleted file'):
346
# is the deleted file a symlink?
347
gp.setmode(int(line[-6:], 8))
348
350
elif line.startswith('new file mode '):
350
352
gp.setmode(int(line[-6:], 8))
526
528
self.writelines(fname, rejlines())
528
def write(self, dest=None):
533
self.writelines(dest, self.lines)
539
530
def apply(self, h):
540
531
if not h.complete():
541
532
raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
899
def pathstrip(path, strip):
903
return '', path.rstrip()
906
i = path.find('/', i)
908
raise PatchError(_("unable to strip away %d of %d dirs from %s") %
909
(count, strip, path))
911
# consume '//' in the path
912
while i < pathlen - 1 and path[i] == '/':
915
return path[:i].lstrip(), path[i:].rstrip()
908
917
def selectfile(afile_orig, bfile_orig, hunk, strip):
909
def pathstrip(path, count=1):
913
return '', path.rstrip()
915
i = path.find('/', i)
917
raise PatchError(_("unable to strip away %d dirs from %s") %
920
# consume '//' in the path
921
while i < pathlen - 1 and path[i] == '/':
924
return path[:i].lstrip(), path[i:].rstrip()
926
918
nulla = afile_orig == "/dev/null"
927
919
nullb = bfile_orig == "/dev/null"
928
920
abase, afile = pathstrip(afile_orig, strip)
1125
1117
if (empty is None and not gitworkdone) or empty:
1128
1121
def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1130
Reads a patch from fp and tries to apply it.
1122
"""Reads a patch from fp and tries to apply it.
1132
1124
The dict 'changed' is filled in with all of the filenames changed
1133
1125
by the patch. Returns 0 for a clean patch, -1 if any rejects were
1136
1128
If 'eolmode' is 'strict', the patch content and patched file are
1137
1129
read in binary mode. Otherwise, line endings are ignored when
1138
1130
patching then normalized according to 'eolmode'.
1132
Callers probably want to call 'updatedir' after this to apply
1133
certain categories of changes not done by this function.
1136
ui, fp, patchfile, copyfile,
1137
changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
1140
def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
1141
sourcefile=None, eolmode='strict'):
1142
1144
current_file = None
1144
opener = util.opener(os.getcwd())
1146
opener = util.opener(cwd)
1146
1148
def closefile():
1147
1149
if not current_file:
1149
current_file.close()
1151
if current_file.dirty:
1152
current_file.writelines(current_file.fname, current_file.lines)
1153
current_file.write_rej()
1150
1154
return len(current_file.rej)
1152
1156
for state, values in iterhunks(ui, fp, sourcefile):
1153
1157
if state == 'hunk':
1154
1158
if not current_file:
1156
current_hunk = values
1157
ret = current_file.apply(current_hunk)
1160
ret = current_file.apply(values)
1159
1162
changed.setdefault(current_file.fname, None)
1164
1167
afile, bfile, first_hunk = values
1167
current_file = patchfile(ui, sourcefile, opener,
1170
current_file = patcher(ui, sourcefile, opener,
1170
1173
current_file, missing = selectfile(afile, bfile,
1171
1174
first_hunk, strip)
1172
current_file = patchfile(ui, current_file, opener,
1175
current_file = patcher(ui, current_file, opener,
1176
missing=missing, eolmode=eolmode)
1174
1177
except PatchError, err:
1175
1178
ui.warn(str(err) + '\n')
1176
current_file, current_hunk = None, None
1179
1182
elif state == 'git':
1182
for gp in gitpatches:
1184
gp.path = pathstrip(gp.path, strip - 1)[1]
1186
gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1183
1187
if gp.op in ('COPY', 'RENAME'):
1184
copyfile(gp.oldpath, gp.path, cwd)
1188
copyfn(gp.oldpath, gp.path, cwd)
1185
1189
changed[gp.path] = gp
1187
1191
raise util.Abort(_('unsupported parser state: %s') % state)
1195
def diffopts(ui, opts=None, untrusted=False):
1196
def get(key, name=None, getter=ui.configbool):
1197
return ((opts and opts.get(key)) or
1198
getter('diff', name or key, None, untrusted=untrusted))
1199
return mdiff.diffopts(
1200
text=opts and opts.get('text'),
1202
nodates=get('nodates'),
1203
showfunc=get('show_function', 'showfunc'),
1204
ignorews=get('ignore_all_space', 'ignorews'),
1205
ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1206
ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1207
context=get('unified', getter=ui.config))
1209
1199
def updatedir(ui, repo, patches, similarity=0):
1210
1200
'''Update dirstate after patch application according to metadata'''
1211
1201
if not patches:
1227
1217
copies.append((gp.oldpath, gp.path))
1228
1218
elif gp.op == 'DELETE':
1229
1219
removes.add(gp.path)
1230
1222
for src, dst in copies:
1232
1224
if (not similarity) and removes:
1233
repo.remove(sorted(removes), True)
1225
wctx.remove(sorted(removes), True)
1234
1227
for f in patches:
1235
1228
gp = patches[f]
1236
1229
if gp and gp.mode:
1240
1233
if gp.op == 'ADD' and not os.path.exists(dst):
1241
1234
flags = (isexec and 'x' or '') + (islink and 'l' or '')
1242
1235
repo.wwrite(gp.path, '', flags)
1243
elif gp.op != 'DELETE':
1244
util.set_flags(dst, islink, isexec)
1236
util.set_flags(dst, islink, isexec)
1245
1237
cmdutil.addremove(repo, cfiles, similarity=similarity)
1246
1238
files = patches.keys()
1247
1239
files.extend([r for r in removes if r not in files])
1338
1330
return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1339
1331
except NoHunks:
1332
ui.warn(_('internal patcher failed\n'
1333
'please report details to '
1334
'http://mercurial.selenic.com/bts/\n'
1335
'or mercurial@selenic.com\n'))
1340
1336
patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1342
1338
ui.debug('no valid hunks found; trying with %r instead\n' %
1393
1389
class GitDiffRequired(Exception):
1392
def diffopts(ui, opts=None, untrusted=False):
1393
def get(key, name=None, getter=ui.configbool):
1394
return ((opts and opts.get(key)) or
1395
getter('diff', name or key, None, untrusted=untrusted))
1396
return mdiff.diffopts(
1397
text=opts and opts.get('text'),
1399
nodates=get('nodates'),
1400
showfunc=get('show_function', 'showfunc'),
1401
ignorews=get('ignore_all_space', 'ignorews'),
1402
ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1403
ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1404
context=get('unified', getter=ui.config))
1396
1406
def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1397
1407
losedatafn=None):
1398
1408
'''yields diff of changes to files between two nodes, or node and
1466
1476
return difffn(opts, None)
1478
def difflabel(func, *args, **kw):
1479
'''yields 2-tuples of (output, label) based on the output of func()'''
1480
prefixes = [('diff', 'diff.diffline'),
1481
('copy', 'diff.extended'),
1482
('rename', 'diff.extended'),
1483
('old', 'diff.extended'),
1484
('new', 'diff.extended'),
1485
('deleted', 'diff.extended'),
1486
('---', 'diff.file_a'),
1487
('+++', 'diff.file_b'),
1488
('@@', 'diff.hunk'),
1489
('-', 'diff.deleted'),
1490
('+', 'diff.inserted')]
1492
for chunk in func(*args, **kw):
1493
lines = chunk.split('\n')
1494
for i, line in enumerate(lines):
1498
if line and line[0] in '+-':
1499
# highlight trailing whitespace, but only in changed lines
1500
stripline = line.rstrip()
1501
for prefix, label in prefixes:
1502
if stripline.startswith(prefix):
1503
yield (stripline, label)
1507
if line != stripline:
1508
yield (line[len(stripline):], 'diff.trailingwhitespace')
1510
def diffui(*args, **kw):
1511
'''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1512
return difflabel(diff, *args, **kw)
1468
1515
def _addmodehdr(header, omode, nmode):
1469
1516
if omode != nmode:
1470
1517
header.append('old mode %s\n' % omode)
1571
def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1573
'''export changesets as hg patches.'''
1576
revwidth = max([len(str(rev)) for rev in revs])
1578
def single(rev, seqno, fp):
1581
parents = [p.node() for p in ctx.parents() if p]
1582
branch = ctx.branch()
1585
prev = (parents and parents[0]) or nullid
1588
fp = cmdutil.make_file(repo, template, node, total=total,
1589
seqno=seqno, revwidth=revwidth,
1591
if fp != sys.stdout and hasattr(fp, 'name'):
1592
repo.ui.note("%s\n" % fp.name)
1594
fp.write("# HG changeset patch\n")
1595
fp.write("# User %s\n" % ctx.user())
1596
fp.write("# Date %d %d\n" % ctx.date())
1597
if branch and (branch != 'default'):
1598
fp.write("# Branch %s\n" % branch)
1599
fp.write("# Node ID %s\n" % hex(node))
1600
fp.write("# Parent %s\n" % hex(prev))
1601
if len(parents) > 1:
1602
fp.write("# Parent %s\n" % hex(parents[1]))
1603
fp.write(ctx.description().rstrip())
1606
for chunk in diff(repo, prev, node, opts=opts):
1609
for seqno, rev in enumerate(revs):
1610
single(rev, seqno + 1, fp)
1612
1618
def diffstatdata(lines):
1613
1619
filename, adds, removes = None, 0, 0
1614
1620
for line in lines:
1676
1682
% (len(stats), totaladds, totalremoves))
1678
1684
return ''.join(output)
1686
def diffstatui(*args, **kw):
1687
'''like diffstat(), but yields 2-tuples of (output, label) for
1691
for line in diffstat(*args, **kw).splitlines():
1692
if line and line[-1] in '+-':
1693
name, graph = line.rsplit(' ', 1)
1694
yield (name + ' ', '')
1695
m = re.search(r'\++', graph)
1697
yield (m.group(0), 'diffstat.inserted')
1698
m = re.search(r'-+', graph)
1700
yield (m.group(0), 'diffstat.deleted')