##// END OF EJS Templates
patch: always raise PatchError with a message, simplify handling
Patrick Mezard -
r12674:aa2fe1f5 default
parent child Browse files
Show More
@@ -1,573 +1,569
1 1 # record.py
2 2 #
3 3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''commands to interactively select changes for commit/qrefresh'''
9 9
10 10 from mercurial.i18n import gettext, _
11 11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
12 12 from mercurial import util
13 13 import copy, cStringIO, errno, os, re, tempfile
14 14
15 15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16 16
17 17 def scanpatch(fp):
18 18 """like patch.iterhunks, but yield different events
19 19
20 20 - ('file', [header_lines + fromfile + tofile])
21 21 - ('context', [context_lines])
22 22 - ('hunk', [hunk_lines])
23 23 - ('range', (-start,len, +start,len, diffp))
24 24 """
25 25 lr = patch.linereader(fp)
26 26
27 27 def scanwhile(first, p):
28 28 """scan lr while predicate holds"""
29 29 lines = [first]
30 30 while True:
31 31 line = lr.readline()
32 32 if not line:
33 33 break
34 34 if p(line):
35 35 lines.append(line)
36 36 else:
37 37 lr.push(line)
38 38 break
39 39 return lines
40 40
41 41 while True:
42 42 line = lr.readline()
43 43 if not line:
44 44 break
45 45 if line.startswith('diff --git a/'):
46 46 def notheader(line):
47 47 s = line.split(None, 1)
48 48 return not s or s[0] not in ('---', 'diff')
49 49 header = scanwhile(line, notheader)
50 50 fromfile = lr.readline()
51 51 if fromfile.startswith('---'):
52 52 tofile = lr.readline()
53 53 header += [fromfile, tofile]
54 54 else:
55 55 lr.push(fromfile)
56 56 yield 'file', header
57 57 elif line[0] == ' ':
58 58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
59 59 elif line[0] in '-+':
60 60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
61 61 else:
62 62 m = lines_re.match(line)
63 63 if m:
64 64 yield 'range', m.groups()
65 65 else:
66 66 raise patch.PatchError('unknown patch content: %r' % line)
67 67
68 68 class header(object):
69 69 """patch header
70 70
71 71 XXX shoudn't we move this to mercurial/patch.py ?
72 72 """
73 73 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
74 74 allhunks_re = re.compile('(?:index|new file|deleted file) ')
75 75 pretty_re = re.compile('(?:new file|deleted file) ')
76 76 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
77 77
78 78 def __init__(self, header):
79 79 self.header = header
80 80 self.hunks = []
81 81
82 82 def binary(self):
83 83 for h in self.header:
84 84 if h.startswith('index '):
85 85 return True
86 86
87 87 def pretty(self, fp):
88 88 for h in self.header:
89 89 if h.startswith('index '):
90 90 fp.write(_('this modifies a binary file (all or nothing)\n'))
91 91 break
92 92 if self.pretty_re.match(h):
93 93 fp.write(h)
94 94 if self.binary():
95 95 fp.write(_('this is a binary file\n'))
96 96 break
97 97 if h.startswith('---'):
98 98 fp.write(_('%d hunks, %d lines changed\n') %
99 99 (len(self.hunks),
100 100 sum([max(h.added, h.removed) for h in self.hunks])))
101 101 break
102 102 fp.write(h)
103 103
104 104 def write(self, fp):
105 105 fp.write(''.join(self.header))
106 106
107 107 def allhunks(self):
108 108 for h in self.header:
109 109 if self.allhunks_re.match(h):
110 110 return True
111 111
112 112 def files(self):
113 113 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
114 114 if fromfile == tofile:
115 115 return [fromfile]
116 116 return [fromfile, tofile]
117 117
118 118 def filename(self):
119 119 return self.files()[-1]
120 120
121 121 def __repr__(self):
122 122 return '<header %s>' % (' '.join(map(repr, self.files())))
123 123
124 124 def special(self):
125 125 for h in self.header:
126 126 if self.special_re.match(h):
127 127 return True
128 128
129 129 def countchanges(hunk):
130 130 """hunk -> (n+,n-)"""
131 131 add = len([h for h in hunk if h[0] == '+'])
132 132 rem = len([h for h in hunk if h[0] == '-'])
133 133 return add, rem
134 134
135 135 class hunk(object):
136 136 """patch hunk
137 137
138 138 XXX shouldn't we merge this with patch.hunk ?
139 139 """
140 140 maxcontext = 3
141 141
142 142 def __init__(self, header, fromline, toline, proc, before, hunk, after):
143 143 def trimcontext(number, lines):
144 144 delta = len(lines) - self.maxcontext
145 145 if False and delta > 0:
146 146 return number + delta, lines[:self.maxcontext]
147 147 return number, lines
148 148
149 149 self.header = header
150 150 self.fromline, self.before = trimcontext(fromline, before)
151 151 self.toline, self.after = trimcontext(toline, after)
152 152 self.proc = proc
153 153 self.hunk = hunk
154 154 self.added, self.removed = countchanges(self.hunk)
155 155
156 156 def write(self, fp):
157 157 delta = len(self.before) + len(self.after)
158 158 if self.after and self.after[-1] == '\\ No newline at end of file\n':
159 159 delta -= 1
160 160 fromlen = delta + self.removed
161 161 tolen = delta + self.added
162 162 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
163 163 (self.fromline, fromlen, self.toline, tolen,
164 164 self.proc and (' ' + self.proc)))
165 165 fp.write(''.join(self.before + self.hunk + self.after))
166 166
167 167 pretty = write
168 168
169 169 def filename(self):
170 170 return self.header.filename()
171 171
172 172 def __repr__(self):
173 173 return '<hunk %r@%d>' % (self.filename(), self.fromline)
174 174
175 175 def parsepatch(fp):
176 176 """patch -> [] of hunks """
177 177 class parser(object):
178 178 """patch parsing state machine"""
179 179 def __init__(self):
180 180 self.fromline = 0
181 181 self.toline = 0
182 182 self.proc = ''
183 183 self.header = None
184 184 self.context = []
185 185 self.before = []
186 186 self.hunk = []
187 187 self.stream = []
188 188
189 189 def addrange(self, limits):
190 190 fromstart, fromend, tostart, toend, proc = limits
191 191 self.fromline = int(fromstart)
192 192 self.toline = int(tostart)
193 193 self.proc = proc
194 194
195 195 def addcontext(self, context):
196 196 if self.hunk:
197 197 h = hunk(self.header, self.fromline, self.toline, self.proc,
198 198 self.before, self.hunk, context)
199 199 self.header.hunks.append(h)
200 200 self.stream.append(h)
201 201 self.fromline += len(self.before) + h.removed
202 202 self.toline += len(self.before) + h.added
203 203 self.before = []
204 204 self.hunk = []
205 205 self.proc = ''
206 206 self.context = context
207 207
208 208 def addhunk(self, hunk):
209 209 if self.context:
210 210 self.before = self.context
211 211 self.context = []
212 212 self.hunk = hunk
213 213
214 214 def newfile(self, hdr):
215 215 self.addcontext([])
216 216 h = header(hdr)
217 217 self.stream.append(h)
218 218 self.header = h
219 219
220 220 def finished(self):
221 221 self.addcontext([])
222 222 return self.stream
223 223
224 224 transitions = {
225 225 'file': {'context': addcontext,
226 226 'file': newfile,
227 227 'hunk': addhunk,
228 228 'range': addrange},
229 229 'context': {'file': newfile,
230 230 'hunk': addhunk,
231 231 'range': addrange},
232 232 'hunk': {'context': addcontext,
233 233 'file': newfile,
234 234 'range': addrange},
235 235 'range': {'context': addcontext,
236 236 'hunk': addhunk},
237 237 }
238 238
239 239 p = parser()
240 240
241 241 state = 'context'
242 242 for newstate, data in scanpatch(fp):
243 243 try:
244 244 p.transitions[state][newstate](p, data)
245 245 except KeyError:
246 246 raise patch.PatchError('unhandled transition: %s -> %s' %
247 247 (state, newstate))
248 248 state = newstate
249 249 return p.finished()
250 250
251 251 def filterpatch(ui, chunks):
252 252 """Interactively filter patch chunks into applied-only chunks"""
253 253 chunks = list(chunks)
254 254 chunks.reverse()
255 255 seen = set()
256 256 def consumefile():
257 257 """fetch next portion from chunks until a 'header' is seen
258 258 NB: header == new-file mark
259 259 """
260 260 consumed = []
261 261 while chunks:
262 262 if isinstance(chunks[-1], header):
263 263 break
264 264 else:
265 265 consumed.append(chunks.pop())
266 266 return consumed
267 267
268 268 resp_all = [None] # this two are changed from inside prompt,
269 269 resp_file = [None] # so can't be usual variables
270 270 applied = {} # 'filename' -> [] of chunks
271 271 def prompt(query):
272 272 """prompt query, and process base inputs
273 273
274 274 - y/n for the rest of file
275 275 - y/n for the rest
276 276 - ? (help)
277 277 - q (quit)
278 278
279 279 Returns True/False and sets reps_all and resp_file as
280 280 appropriate.
281 281 """
282 282 if resp_all[0] is not None:
283 283 return resp_all[0]
284 284 if resp_file[0] is not None:
285 285 return resp_file[0]
286 286 while True:
287 287 resps = _('[Ynsfdaq?]')
288 288 choices = (_('&Yes, record this change'),
289 289 _('&No, skip this change'),
290 290 _('&Skip remaining changes to this file'),
291 291 _('Record remaining changes to this &file'),
292 292 _('&Done, skip remaining changes and files'),
293 293 _('Record &all changes to all remaining files'),
294 294 _('&Quit, recording no changes'),
295 295 _('&?'))
296 296 r = ui.promptchoice("%s %s" % (query, resps), choices)
297 297 ui.write("\n")
298 298 if r == 7: # ?
299 299 doc = gettext(record.__doc__)
300 300 c = doc.find('::') + 2
301 301 for l in doc[c:].splitlines():
302 302 if l.startswith(' '):
303 303 ui.write(l.strip(), '\n')
304 304 continue
305 305 elif r == 0: # yes
306 306 ret = True
307 307 elif r == 1: # no
308 308 ret = False
309 309 elif r == 2: # Skip
310 310 ret = resp_file[0] = False
311 311 elif r == 3: # file (Record remaining)
312 312 ret = resp_file[0] = True
313 313 elif r == 4: # done, skip remaining
314 314 ret = resp_all[0] = False
315 315 elif r == 5: # all
316 316 ret = resp_all[0] = True
317 317 elif r == 6: # quit
318 318 raise util.Abort(_('user quit'))
319 319 return ret
320 320 pos, total = 0, len(chunks) - 1
321 321 while chunks:
322 322 pos = total - len(chunks) + 1
323 323 chunk = chunks.pop()
324 324 if isinstance(chunk, header):
325 325 # new-file mark
326 326 resp_file = [None]
327 327 fixoffset = 0
328 328 hdr = ''.join(chunk.header)
329 329 if hdr in seen:
330 330 consumefile()
331 331 continue
332 332 seen.add(hdr)
333 333 if resp_all[0] is None:
334 334 chunk.pretty(ui)
335 335 r = prompt(_('examine changes to %s?') %
336 336 _(' and ').join(map(repr, chunk.files())))
337 337 if r:
338 338 applied[chunk.filename()] = [chunk]
339 339 if chunk.allhunks():
340 340 applied[chunk.filename()] += consumefile()
341 341 else:
342 342 consumefile()
343 343 else:
344 344 # new hunk
345 345 if resp_file[0] is None and resp_all[0] is None:
346 346 chunk.pretty(ui)
347 347 r = total == 1 and prompt(_('record this change to %r?') %
348 348 chunk.filename()) \
349 349 or prompt(_('record change %d/%d to %r?') %
350 350 (pos, total, chunk.filename()))
351 351 if r:
352 352 if fixoffset:
353 353 chunk = copy.copy(chunk)
354 354 chunk.toline += fixoffset
355 355 applied[chunk.filename()].append(chunk)
356 356 else:
357 357 fixoffset += chunk.removed - chunk.added
358 358 return sum([h for h in applied.itervalues()
359 359 if h[0].special() or len(h) > 1], [])
360 360
361 361 def record(ui, repo, *pats, **opts):
362 362 '''interactively select changes to commit
363 363
364 364 If a list of files is omitted, all changes reported by :hg:`status`
365 365 will be candidates for recording.
366 366
367 367 See :hg:`help dates` for a list of formats valid for -d/--date.
368 368
369 369 You will be prompted for whether to record changes to each
370 370 modified file, and for files with multiple changes, for each
371 371 change to use. For each query, the following responses are
372 372 possible::
373 373
374 374 y - record this change
375 375 n - skip this change
376 376
377 377 s - skip remaining changes to this file
378 378 f - record remaining changes to this file
379 379
380 380 d - done, skip remaining changes and files
381 381 a - record all changes to all remaining files
382 382 q - quit, recording no changes
383 383
384 384 ? - display help
385 385
386 386 This command is not available when committing a merge.'''
387 387
388 388 dorecord(ui, repo, commands.commit, *pats, **opts)
389 389
390 390
391 391 def qrecord(ui, repo, patch, *pats, **opts):
392 392 '''interactively record a new patch
393 393
394 394 See :hg:`help qnew` & :hg:`help record` for more information and
395 395 usage.
396 396 '''
397 397
398 398 try:
399 399 mq = extensions.find('mq')
400 400 except KeyError:
401 401 raise util.Abort(_("'mq' extension not loaded"))
402 402
403 403 def committomq(ui, repo, *pats, **opts):
404 404 mq.new(ui, repo, patch, *pats, **opts)
405 405
406 406 opts = opts.copy()
407 407 opts['force'] = True # always 'qnew -f'
408 408 dorecord(ui, repo, committomq, *pats, **opts)
409 409
410 410
411 411 def dorecord(ui, repo, commitfunc, *pats, **opts):
412 412 if not ui.interactive():
413 413 raise util.Abort(_('running non-interactively, use commit instead'))
414 414
415 415 def recordfunc(ui, repo, message, match, opts):
416 416 """This is generic record driver.
417 417
418 418 Its job is to interactively filter local changes, and accordingly
419 419 prepare working dir into a state, where the job can be delegated to
420 420 non-interactive commit command such as 'commit' or 'qrefresh'.
421 421
422 422 After the actual job is done by non-interactive command, working dir
423 423 state is restored to original.
424 424
425 425 In the end we'll record interesting changes, and everything else will be
426 426 left in place, so the user can continue his work.
427 427 """
428 428
429 429 merge = len(repo[None].parents()) > 1
430 430 if merge:
431 431 raise util.Abort(_('cannot partially commit a merge '
432 432 '(use hg commit instead)'))
433 433
434 434 changes = repo.status(match=match)[:3]
435 435 diffopts = mdiff.diffopts(git=True, nodates=True)
436 436 chunks = patch.diff(repo, changes=changes, opts=diffopts)
437 437 fp = cStringIO.StringIO()
438 438 fp.write(''.join(chunks))
439 439 fp.seek(0)
440 440
441 441 # 1. filter patch, so we have intending-to apply subset of it
442 442 chunks = filterpatch(ui, parsepatch(fp))
443 443 del fp
444 444
445 445 contenders = set()
446 446 for h in chunks:
447 447 try:
448 448 contenders.update(set(h.files()))
449 449 except AttributeError:
450 450 pass
451 451
452 452 changed = changes[0] + changes[1] + changes[2]
453 453 newfiles = [f for f in changed if f in contenders]
454 454 if not newfiles:
455 455 ui.status(_('no changes to record\n'))
456 456 return 0
457 457
458 458 modified = set(changes[0])
459 459
460 460 # 2. backup changed files, so we can restore them in the end
461 461 backups = {}
462 462 backupdir = repo.join('record-backups')
463 463 try:
464 464 os.mkdir(backupdir)
465 465 except OSError, err:
466 466 if err.errno != errno.EEXIST:
467 467 raise
468 468 try:
469 469 # backup continues
470 470 for f in newfiles:
471 471 if f not in modified:
472 472 continue
473 473 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
474 474 dir=backupdir)
475 475 os.close(fd)
476 476 ui.debug('backup %r as %r\n' % (f, tmpname))
477 477 util.copyfile(repo.wjoin(f), tmpname)
478 478 backups[f] = tmpname
479 479
480 480 fp = cStringIO.StringIO()
481 481 for c in chunks:
482 482 if c.filename() in backups:
483 483 c.write(fp)
484 484 dopatch = fp.tell()
485 485 fp.seek(0)
486 486
487 487 # 3a. apply filtered patch to clean repo (clean)
488 488 if backups:
489 489 hg.revert(repo, repo.dirstate.parents()[0],
490 490 lambda key: key in backups)
491 491
492 492 # 3b. (apply)
493 493 if dopatch:
494 494 try:
495 495 ui.debug('applying patch\n')
496 496 ui.debug(fp.getvalue())
497 497 pfiles = {}
498 498 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
499 499 eolmode=None)
500 500 cmdutil.updatedir(ui, repo, pfiles)
501 501 except patch.PatchError, err:
502 s = str(err)
503 if s:
504 raise util.Abort(s)
505 else:
506 raise util.Abort(_('patch failed to apply'))
502 raise util.Abort(str(err))
507 503 del fp
508 504
509 505 # 4. We prepared working directory according to filtered patch.
510 506 # Now is the time to delegate the job to commit/qrefresh or the like!
511 507
512 508 # it is important to first chdir to repo root -- we'll call a
513 509 # highlevel command with list of pathnames relative to repo root
514 510 cwd = os.getcwd()
515 511 os.chdir(repo.root)
516 512 try:
517 513 commitfunc(ui, repo, *newfiles, **opts)
518 514 finally:
519 515 os.chdir(cwd)
520 516
521 517 return 0
522 518 finally:
523 519 # 5. finally restore backed-up files
524 520 try:
525 521 for realname, tmpname in backups.iteritems():
526 522 ui.debug('restoring %r to %r\n' % (tmpname, realname))
527 523 util.copyfile(tmpname, repo.wjoin(realname))
528 524 os.unlink(tmpname)
529 525 os.rmdir(backupdir)
530 526 except OSError:
531 527 pass
532 528
533 529 # wrap ui.write so diff output can be labeled/colorized
534 530 def wrapwrite(orig, *args, **kw):
535 531 label = kw.pop('label', '')
536 532 for chunk, l in patch.difflabel(lambda: args):
537 533 orig(chunk, label=label + l)
538 534 oldwrite = ui.write
539 535 extensions.wrapfunction(ui, 'write', wrapwrite)
540 536 try:
541 537 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
542 538 finally:
543 539 ui.write = oldwrite
544 540
545 541 cmdtable = {
546 542 "record":
547 543 (record,
548 544
549 545 # add commit options
550 546 commands.table['^commit|ci'][1],
551 547
552 548 _('hg record [OPTION]... [FILE]...')),
553 549 }
554 550
555 551
556 552 def uisetup(ui):
557 553 try:
558 554 mq = extensions.find('mq')
559 555 except KeyError:
560 556 return
561 557
562 558 qcmdtable = {
563 559 "qrecord":
564 560 (qrecord,
565 561
566 562 # add qnew options, except '--force'
567 563 [opt for opt in mq.cmdtable['^qnew'][1] if opt[1] != 'force'],
568 564
569 565 _('hg qrecord [OPTION]... PATCH [FILE]...')),
570 566 }
571 567
572 568 cmdtable.update(qcmdtable)
573 569
@@ -1,1632 +1,1628
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
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.
8 8
9 9 import cStringIO, email.Parser, os, re
10 10 import tempfile, zlib
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, util, diffhelpers, copies, encoding
15 15
16 16 gitre = re.compile('diff --git a/(.*) b/(.*)')
17 17
18 18 class PatchError(Exception):
19 19 pass
20 20
21 21 # helper functions
22 22
23 23 def copyfile(src, dst, basedir):
24 24 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
25 25 if os.path.lexists(absdst):
26 26 raise util.Abort(_("cannot create %s: destination already exists") %
27 27 dst)
28 28
29 29 dstdir = os.path.dirname(absdst)
30 30 if dstdir and not os.path.isdir(dstdir):
31 31 try:
32 32 os.makedirs(dstdir)
33 33 except IOError:
34 34 raise util.Abort(
35 35 _("cannot create %s: unable to create destination directory")
36 36 % dst)
37 37
38 38 util.copyfile(abssrc, absdst)
39 39
40 40 # public functions
41 41
42 42 def split(stream):
43 43 '''return an iterator of individual patches from a stream'''
44 44 def isheader(line, inheader):
45 45 if inheader and line[0] in (' ', '\t'):
46 46 # continuation
47 47 return True
48 48 if line[0] in (' ', '-', '+'):
49 49 # diff line - don't check for header pattern in there
50 50 return False
51 51 l = line.split(': ', 1)
52 52 return len(l) == 2 and ' ' not in l[0]
53 53
54 54 def chunk(lines):
55 55 return cStringIO.StringIO(''.join(lines))
56 56
57 57 def hgsplit(stream, cur):
58 58 inheader = True
59 59
60 60 for line in stream:
61 61 if not line.strip():
62 62 inheader = False
63 63 if not inheader and line.startswith('# HG changeset patch'):
64 64 yield chunk(cur)
65 65 cur = []
66 66 inheader = True
67 67
68 68 cur.append(line)
69 69
70 70 if cur:
71 71 yield chunk(cur)
72 72
73 73 def mboxsplit(stream, cur):
74 74 for line in stream:
75 75 if line.startswith('From '):
76 76 for c in split(chunk(cur[1:])):
77 77 yield c
78 78 cur = []
79 79
80 80 cur.append(line)
81 81
82 82 if cur:
83 83 for c in split(chunk(cur[1:])):
84 84 yield c
85 85
86 86 def mimesplit(stream, cur):
87 87 def msgfp(m):
88 88 fp = cStringIO.StringIO()
89 89 g = email.Generator.Generator(fp, mangle_from_=False)
90 90 g.flatten(m)
91 91 fp.seek(0)
92 92 return fp
93 93
94 94 for line in stream:
95 95 cur.append(line)
96 96 c = chunk(cur)
97 97
98 98 m = email.Parser.Parser().parse(c)
99 99 if not m.is_multipart():
100 100 yield msgfp(m)
101 101 else:
102 102 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
103 103 for part in m.walk():
104 104 ct = part.get_content_type()
105 105 if ct not in ok_types:
106 106 continue
107 107 yield msgfp(part)
108 108
109 109 def headersplit(stream, cur):
110 110 inheader = False
111 111
112 112 for line in stream:
113 113 if not inheader and isheader(line, inheader):
114 114 yield chunk(cur)
115 115 cur = []
116 116 inheader = True
117 117 if inheader and not isheader(line, inheader):
118 118 inheader = False
119 119
120 120 cur.append(line)
121 121
122 122 if cur:
123 123 yield chunk(cur)
124 124
125 125 def remainder(cur):
126 126 yield chunk(cur)
127 127
128 128 class fiter(object):
129 129 def __init__(self, fp):
130 130 self.fp = fp
131 131
132 132 def __iter__(self):
133 133 return self
134 134
135 135 def next(self):
136 136 l = self.fp.readline()
137 137 if not l:
138 138 raise StopIteration
139 139 return l
140 140
141 141 inheader = False
142 142 cur = []
143 143
144 144 mimeheaders = ['content-type']
145 145
146 146 if not hasattr(stream, 'next'):
147 147 # http responses, for example, have readline but not next
148 148 stream = fiter(stream)
149 149
150 150 for line in stream:
151 151 cur.append(line)
152 152 if line.startswith('# HG changeset patch'):
153 153 return hgsplit(stream, cur)
154 154 elif line.startswith('From '):
155 155 return mboxsplit(stream, cur)
156 156 elif isheader(line, inheader):
157 157 inheader = True
158 158 if line.split(':', 1)[0].lower() in mimeheaders:
159 159 # let email parser handle this
160 160 return mimesplit(stream, cur)
161 161 elif line.startswith('--- ') and inheader:
162 162 # No evil headers seen by diff start, split by hand
163 163 return headersplit(stream, cur)
164 164 # Not enough info, keep reading
165 165
166 166 # if we are here, we have a very plain patch
167 167 return remainder(cur)
168 168
169 169 def extract(ui, fileobj):
170 170 '''extract patch from data read from fileobj.
171 171
172 172 patch can be a normal patch or contained in an email message.
173 173
174 174 return tuple (filename, message, user, date, branch, node, p1, p2).
175 175 Any item in the returned tuple can be None. If filename is None,
176 176 fileobj did not contain a patch. Caller must unlink filename when done.'''
177 177
178 178 # attempt to detect the start of a patch
179 179 # (this heuristic is borrowed from quilt)
180 180 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
181 181 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
182 182 r'---[ \t].*?^\+\+\+[ \t]|'
183 183 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
184 184
185 185 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
186 186 tmpfp = os.fdopen(fd, 'w')
187 187 try:
188 188 msg = email.Parser.Parser().parse(fileobj)
189 189
190 190 subject = msg['Subject']
191 191 user = msg['From']
192 192 if not subject and not user:
193 193 # Not an email, restore parsed headers if any
194 194 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
195 195
196 196 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
197 197 # should try to parse msg['Date']
198 198 date = None
199 199 nodeid = None
200 200 branch = None
201 201 parents = []
202 202
203 203 if subject:
204 204 if subject.startswith('[PATCH'):
205 205 pend = subject.find(']')
206 206 if pend >= 0:
207 207 subject = subject[pend + 1:].lstrip()
208 208 subject = subject.replace('\n\t', ' ')
209 209 ui.debug('Subject: %s\n' % subject)
210 210 if user:
211 211 ui.debug('From: %s\n' % user)
212 212 diffs_seen = 0
213 213 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
214 214 message = ''
215 215 for part in msg.walk():
216 216 content_type = part.get_content_type()
217 217 ui.debug('Content-Type: %s\n' % content_type)
218 218 if content_type not in ok_types:
219 219 continue
220 220 payload = part.get_payload(decode=True)
221 221 m = diffre.search(payload)
222 222 if m:
223 223 hgpatch = False
224 224 hgpatchheader = False
225 225 ignoretext = False
226 226
227 227 ui.debug('found patch at byte %d\n' % m.start(0))
228 228 diffs_seen += 1
229 229 cfp = cStringIO.StringIO()
230 230 for line in payload[:m.start(0)].splitlines():
231 231 if line.startswith('# HG changeset patch'):
232 232 ui.debug('patch generated by hg export\n')
233 233 hgpatchheader = True
234 234 # drop earlier commit message content
235 235 cfp.seek(0)
236 236 cfp.truncate()
237 237 subject = None
238 238 elif hgpatchheader:
239 239 if line.startswith('# User '):
240 240 user = line[7:]
241 241 ui.debug('From: %s\n' % user)
242 242 elif line.startswith("# Date "):
243 243 date = line[7:]
244 244 elif line.startswith("# Branch "):
245 245 branch = line[9:]
246 246 elif line.startswith("# Node ID "):
247 247 nodeid = line[10:]
248 248 elif line.startswith("# Parent "):
249 249 parents.append(line[10:])
250 250 elif not line.startswith("# "):
251 251 hgpatchheader = False
252 252 hgpatch = True
253 253 elif line == '---' and gitsendmail:
254 254 ignoretext = True
255 255 if not hgpatchheader and not ignoretext:
256 256 cfp.write(line)
257 257 cfp.write('\n')
258 258 message = cfp.getvalue()
259 259 if tmpfp:
260 260 tmpfp.write(payload)
261 261 if not payload.endswith('\n'):
262 262 tmpfp.write('\n')
263 263 elif not diffs_seen and message and content_type == 'text/plain':
264 264 message += '\n' + payload
265 265 except:
266 266 tmpfp.close()
267 267 os.unlink(tmpname)
268 268 raise
269 269
270 270 if subject and not message.startswith(subject):
271 271 message = '%s\n%s' % (subject, message)
272 272 tmpfp.close()
273 273 if not diffs_seen:
274 274 os.unlink(tmpname)
275 275 return None, message, user, date, branch, None, None, None
276 276 p1 = parents and parents.pop(0) or None
277 277 p2 = parents and parents.pop(0) or None
278 278 return tmpname, message, user, date, branch, nodeid, p1, p2
279 279
280 280 class patchmeta(object):
281 281 """Patched file metadata
282 282
283 283 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
284 284 or COPY. 'path' is patched file path. 'oldpath' is set to the
285 285 origin file when 'op' is either COPY or RENAME, None otherwise. If
286 286 file mode is changed, 'mode' is a tuple (islink, isexec) where
287 287 'islink' is True if the file is a symlink and 'isexec' is True if
288 288 the file is executable. Otherwise, 'mode' is None.
289 289 """
290 290 def __init__(self, path):
291 291 self.path = path
292 292 self.oldpath = None
293 293 self.mode = None
294 294 self.op = 'MODIFY'
295 295 self.binary = False
296 296
297 297 def setmode(self, mode):
298 298 islink = mode & 020000
299 299 isexec = mode & 0100
300 300 self.mode = (islink, isexec)
301 301
302 302 def __repr__(self):
303 303 return "<patchmeta %s %r>" % (self.op, self.path)
304 304
305 305 def readgitpatch(lr):
306 306 """extract git-style metadata about patches from <patchname>"""
307 307
308 308 # Filter patch for git information
309 309 gp = None
310 310 gitpatches = []
311 311 for line in lr:
312 312 line = line.rstrip(' \r\n')
313 313 if line.startswith('diff --git'):
314 314 m = gitre.match(line)
315 315 if m:
316 316 if gp:
317 317 gitpatches.append(gp)
318 318 dst = m.group(2)
319 319 gp = patchmeta(dst)
320 320 elif gp:
321 321 if line.startswith('--- '):
322 322 gitpatches.append(gp)
323 323 gp = None
324 324 continue
325 325 if line.startswith('rename from '):
326 326 gp.op = 'RENAME'
327 327 gp.oldpath = line[12:]
328 328 elif line.startswith('rename to '):
329 329 gp.path = line[10:]
330 330 elif line.startswith('copy from '):
331 331 gp.op = 'COPY'
332 332 gp.oldpath = line[10:]
333 333 elif line.startswith('copy to '):
334 334 gp.path = line[8:]
335 335 elif line.startswith('deleted file'):
336 336 gp.op = 'DELETE'
337 337 elif line.startswith('new file mode '):
338 338 gp.op = 'ADD'
339 339 gp.setmode(int(line[-6:], 8))
340 340 elif line.startswith('new mode '):
341 341 gp.setmode(int(line[-6:], 8))
342 342 elif line.startswith('GIT binary patch'):
343 343 gp.binary = True
344 344 if gp:
345 345 gitpatches.append(gp)
346 346
347 347 return gitpatches
348 348
349 349 class linereader(object):
350 350 # simple class to allow pushing lines back into the input stream
351 351 def __init__(self, fp, textmode=False):
352 352 self.fp = fp
353 353 self.buf = []
354 354 self.textmode = textmode
355 355 self.eol = None
356 356
357 357 def push(self, line):
358 358 if line is not None:
359 359 self.buf.append(line)
360 360
361 361 def readline(self):
362 362 if self.buf:
363 363 l = self.buf[0]
364 364 del self.buf[0]
365 365 return l
366 366 l = self.fp.readline()
367 367 if not self.eol:
368 368 if l.endswith('\r\n'):
369 369 self.eol = '\r\n'
370 370 elif l.endswith('\n'):
371 371 self.eol = '\n'
372 372 if self.textmode and l.endswith('\r\n'):
373 373 l = l[:-2] + '\n'
374 374 return l
375 375
376 376 def __iter__(self):
377 377 while 1:
378 378 l = self.readline()
379 379 if not l:
380 380 break
381 381 yield l
382 382
383 383 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
384 384 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
385 385 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
386 386 eolmodes = ['strict', 'crlf', 'lf', 'auto']
387 387
388 388 class patchfile(object):
389 389 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
390 390 self.fname = fname
391 391 self.eolmode = eolmode
392 392 self.eol = None
393 393 self.opener = opener
394 394 self.ui = ui
395 395 self.lines = []
396 396 self.exists = False
397 397 self.missing = missing
398 398 if not missing:
399 399 try:
400 400 self.lines = self.readlines(fname)
401 401 self.exists = True
402 402 except IOError:
403 403 pass
404 404 else:
405 405 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
406 406
407 407 self.hash = {}
408 408 self.dirty = 0
409 409 self.offset = 0
410 410 self.skew = 0
411 411 self.rej = []
412 412 self.fileprinted = False
413 413 self.printfile(False)
414 414 self.hunks = 0
415 415
416 416 def readlines(self, fname):
417 417 if os.path.islink(fname):
418 418 return [os.readlink(fname)]
419 419 fp = self.opener(fname, 'r')
420 420 try:
421 421 lr = linereader(fp, self.eolmode != 'strict')
422 422 lines = list(lr)
423 423 self.eol = lr.eol
424 424 return lines
425 425 finally:
426 426 fp.close()
427 427
428 428 def writelines(self, fname, lines):
429 429 # Ensure supplied data ends in fname, being a regular file or
430 430 # a symlink. cmdutil.updatedir will -too magically- take care
431 431 # of setting it to the proper type afterwards.
432 432 islink = os.path.islink(fname)
433 433 if islink:
434 434 fp = cStringIO.StringIO()
435 435 else:
436 436 fp = self.opener(fname, 'w')
437 437 try:
438 438 if self.eolmode == 'auto':
439 439 eol = self.eol
440 440 elif self.eolmode == 'crlf':
441 441 eol = '\r\n'
442 442 else:
443 443 eol = '\n'
444 444
445 445 if self.eolmode != 'strict' and eol and eol != '\n':
446 446 for l in lines:
447 447 if l and l[-1] == '\n':
448 448 l = l[:-1] + eol
449 449 fp.write(l)
450 450 else:
451 451 fp.writelines(lines)
452 452 if islink:
453 453 self.opener.symlink(fp.getvalue(), fname)
454 454 finally:
455 455 fp.close()
456 456
457 457 def unlink(self, fname):
458 458 os.unlink(fname)
459 459
460 460 def printfile(self, warn):
461 461 if self.fileprinted:
462 462 return
463 463 if warn or self.ui.verbose:
464 464 self.fileprinted = True
465 465 s = _("patching file %s\n") % self.fname
466 466 if warn:
467 467 self.ui.warn(s)
468 468 else:
469 469 self.ui.note(s)
470 470
471 471
472 472 def findlines(self, l, linenum):
473 473 # looks through the hash and finds candidate lines. The
474 474 # result is a list of line numbers sorted based on distance
475 475 # from linenum
476 476
477 477 cand = self.hash.get(l, [])
478 478 if len(cand) > 1:
479 479 # resort our list of potentials forward then back.
480 480 cand.sort(key=lambda x: abs(x - linenum))
481 481 return cand
482 482
483 483 def hashlines(self):
484 484 self.hash = {}
485 485 for x, s in enumerate(self.lines):
486 486 self.hash.setdefault(s, []).append(x)
487 487
488 488 def write_rej(self):
489 489 # our rejects are a little different from patch(1). This always
490 490 # creates rejects in the same form as the original patch. A file
491 491 # header is inserted so that you can run the reject through patch again
492 492 # without having to type the filename.
493 493
494 494 if not self.rej:
495 495 return
496 496
497 497 fname = self.fname + ".rej"
498 498 self.ui.warn(
499 499 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
500 500 (len(self.rej), self.hunks, fname))
501 501
502 502 def rejlines():
503 503 base = os.path.basename(self.fname)
504 504 yield "--- %s\n+++ %s\n" % (base, base)
505 505 for x in self.rej:
506 506 for l in x.hunk:
507 507 yield l
508 508 if l[-1] != '\n':
509 509 yield "\n\ No newline at end of file\n"
510 510
511 511 self.writelines(fname, rejlines())
512 512
513 513 def apply(self, h):
514 514 if not h.complete():
515 515 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
516 516 (h.number, h.desc, len(h.a), h.lena, len(h.b),
517 517 h.lenb))
518 518
519 519 self.hunks += 1
520 520
521 521 if self.missing:
522 522 self.rej.append(h)
523 523 return -1
524 524
525 525 if self.exists and h.createfile():
526 526 self.ui.warn(_("file %s already exists\n") % self.fname)
527 527 self.rej.append(h)
528 528 return -1
529 529
530 530 if isinstance(h, binhunk):
531 531 if h.rmfile():
532 532 self.unlink(self.fname)
533 533 else:
534 534 self.lines[:] = h.new()
535 535 self.offset += len(h.new())
536 536 self.dirty = 1
537 537 return 0
538 538
539 539 horig = h
540 540 if (self.eolmode in ('crlf', 'lf')
541 541 or self.eolmode == 'auto' and self.eol):
542 542 # If new eols are going to be normalized, then normalize
543 543 # hunk data before patching. Otherwise, preserve input
544 544 # line-endings.
545 545 h = h.getnormalized()
546 546
547 547 # fast case first, no offsets, no fuzz
548 548 old = h.old()
549 549 # patch starts counting at 1 unless we are adding the file
550 550 if h.starta == 0:
551 551 start = 0
552 552 else:
553 553 start = h.starta + self.offset - 1
554 554 orig_start = start
555 555 # if there's skew we want to emit the "(offset %d lines)" even
556 556 # when the hunk cleanly applies at start + skew, so skip the
557 557 # fast case code
558 558 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
559 559 if h.rmfile():
560 560 self.unlink(self.fname)
561 561 else:
562 562 self.lines[start : start + h.lena] = h.new()
563 563 self.offset += h.lenb - h.lena
564 564 self.dirty = 1
565 565 return 0
566 566
567 567 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
568 568 self.hashlines()
569 569 if h.hunk[-1][0] != ' ':
570 570 # if the hunk tried to put something at the bottom of the file
571 571 # override the start line and use eof here
572 572 search_start = len(self.lines)
573 573 else:
574 574 search_start = orig_start + self.skew
575 575
576 576 for fuzzlen in xrange(3):
577 577 for toponly in [True, False]:
578 578 old = h.old(fuzzlen, toponly)
579 579
580 580 cand = self.findlines(old[0][1:], search_start)
581 581 for l in cand:
582 582 if diffhelpers.testhunk(old, self.lines, l) == 0:
583 583 newlines = h.new(fuzzlen, toponly)
584 584 self.lines[l : l + len(old)] = newlines
585 585 self.offset += len(newlines) - len(old)
586 586 self.skew = l - orig_start
587 587 self.dirty = 1
588 588 offset = l - orig_start - fuzzlen
589 589 if fuzzlen:
590 590 msg = _("Hunk #%d succeeded at %d "
591 591 "with fuzz %d "
592 592 "(offset %d lines).\n")
593 593 self.printfile(True)
594 594 self.ui.warn(msg %
595 595 (h.number, l + 1, fuzzlen, offset))
596 596 else:
597 597 msg = _("Hunk #%d succeeded at %d "
598 598 "(offset %d lines).\n")
599 599 self.ui.note(msg % (h.number, l + 1, offset))
600 600 return fuzzlen
601 601 self.printfile(True)
602 602 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
603 603 self.rej.append(horig)
604 604 return -1
605 605
606 606 class hunk(object):
607 607 def __init__(self, desc, num, lr, context, create=False, remove=False):
608 608 self.number = num
609 609 self.desc = desc
610 610 self.hunk = [desc]
611 611 self.a = []
612 612 self.b = []
613 613 self.starta = self.lena = None
614 614 self.startb = self.lenb = None
615 615 if lr is not None:
616 616 if context:
617 617 self.read_context_hunk(lr)
618 618 else:
619 619 self.read_unified_hunk(lr)
620 620 self.create = create
621 621 self.remove = remove and not create
622 622
623 623 def getnormalized(self):
624 624 """Return a copy with line endings normalized to LF."""
625 625
626 626 def normalize(lines):
627 627 nlines = []
628 628 for line in lines:
629 629 if line.endswith('\r\n'):
630 630 line = line[:-2] + '\n'
631 631 nlines.append(line)
632 632 return nlines
633 633
634 634 # Dummy object, it is rebuilt manually
635 635 nh = hunk(self.desc, self.number, None, None, False, False)
636 636 nh.number = self.number
637 637 nh.desc = self.desc
638 638 nh.hunk = self.hunk
639 639 nh.a = normalize(self.a)
640 640 nh.b = normalize(self.b)
641 641 nh.starta = self.starta
642 642 nh.startb = self.startb
643 643 nh.lena = self.lena
644 644 nh.lenb = self.lenb
645 645 nh.create = self.create
646 646 nh.remove = self.remove
647 647 return nh
648 648
649 649 def read_unified_hunk(self, lr):
650 650 m = unidesc.match(self.desc)
651 651 if not m:
652 652 raise PatchError(_("bad hunk #%d") % self.number)
653 653 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
654 654 if self.lena is None:
655 655 self.lena = 1
656 656 else:
657 657 self.lena = int(self.lena)
658 658 if self.lenb is None:
659 659 self.lenb = 1
660 660 else:
661 661 self.lenb = int(self.lenb)
662 662 self.starta = int(self.starta)
663 663 self.startb = int(self.startb)
664 664 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
665 665 # if we hit eof before finishing out the hunk, the last line will
666 666 # be zero length. Lets try to fix it up.
667 667 while len(self.hunk[-1]) == 0:
668 668 del self.hunk[-1]
669 669 del self.a[-1]
670 670 del self.b[-1]
671 671 self.lena -= 1
672 672 self.lenb -= 1
673 673
674 674 def read_context_hunk(self, lr):
675 675 self.desc = lr.readline()
676 676 m = contextdesc.match(self.desc)
677 677 if not m:
678 678 raise PatchError(_("bad hunk #%d") % self.number)
679 679 foo, self.starta, foo2, aend, foo3 = m.groups()
680 680 self.starta = int(self.starta)
681 681 if aend is None:
682 682 aend = self.starta
683 683 self.lena = int(aend) - self.starta
684 684 if self.starta:
685 685 self.lena += 1
686 686 for x in xrange(self.lena):
687 687 l = lr.readline()
688 688 if l.startswith('---'):
689 689 lr.push(l)
690 690 break
691 691 s = l[2:]
692 692 if l.startswith('- ') or l.startswith('! '):
693 693 u = '-' + s
694 694 elif l.startswith(' '):
695 695 u = ' ' + s
696 696 else:
697 697 raise PatchError(_("bad hunk #%d old text line %d") %
698 698 (self.number, x))
699 699 self.a.append(u)
700 700 self.hunk.append(u)
701 701
702 702 l = lr.readline()
703 703 if l.startswith('\ '):
704 704 s = self.a[-1][:-1]
705 705 self.a[-1] = s
706 706 self.hunk[-1] = s
707 707 l = lr.readline()
708 708 m = contextdesc.match(l)
709 709 if not m:
710 710 raise PatchError(_("bad hunk #%d") % self.number)
711 711 foo, self.startb, foo2, bend, foo3 = m.groups()
712 712 self.startb = int(self.startb)
713 713 if bend is None:
714 714 bend = self.startb
715 715 self.lenb = int(bend) - self.startb
716 716 if self.startb:
717 717 self.lenb += 1
718 718 hunki = 1
719 719 for x in xrange(self.lenb):
720 720 l = lr.readline()
721 721 if l.startswith('\ '):
722 722 s = self.b[-1][:-1]
723 723 self.b[-1] = s
724 724 self.hunk[hunki - 1] = s
725 725 continue
726 726 if not l:
727 727 lr.push(l)
728 728 break
729 729 s = l[2:]
730 730 if l.startswith('+ ') or l.startswith('! '):
731 731 u = '+' + s
732 732 elif l.startswith(' '):
733 733 u = ' ' + s
734 734 elif len(self.b) == 0:
735 735 # this can happen when the hunk does not add any lines
736 736 lr.push(l)
737 737 break
738 738 else:
739 739 raise PatchError(_("bad hunk #%d old text line %d") %
740 740 (self.number, x))
741 741 self.b.append(s)
742 742 while True:
743 743 if hunki >= len(self.hunk):
744 744 h = ""
745 745 else:
746 746 h = self.hunk[hunki]
747 747 hunki += 1
748 748 if h == u:
749 749 break
750 750 elif h.startswith('-'):
751 751 continue
752 752 else:
753 753 self.hunk.insert(hunki - 1, u)
754 754 break
755 755
756 756 if not self.a:
757 757 # this happens when lines were only added to the hunk
758 758 for x in self.hunk:
759 759 if x.startswith('-') or x.startswith(' '):
760 760 self.a.append(x)
761 761 if not self.b:
762 762 # this happens when lines were only deleted from the hunk
763 763 for x in self.hunk:
764 764 if x.startswith('+') or x.startswith(' '):
765 765 self.b.append(x[1:])
766 766 # @@ -start,len +start,len @@
767 767 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
768 768 self.startb, self.lenb)
769 769 self.hunk[0] = self.desc
770 770
771 771 def fix_newline(self):
772 772 diffhelpers.fix_newline(self.hunk, self.a, self.b)
773 773
774 774 def complete(self):
775 775 return len(self.a) == self.lena and len(self.b) == self.lenb
776 776
777 777 def createfile(self):
778 778 return self.starta == 0 and self.lena == 0 and self.create
779 779
780 780 def rmfile(self):
781 781 return self.startb == 0 and self.lenb == 0 and self.remove
782 782
783 783 def fuzzit(self, l, fuzz, toponly):
784 784 # this removes context lines from the top and bottom of list 'l'. It
785 785 # checks the hunk to make sure only context lines are removed, and then
786 786 # returns a new shortened list of lines.
787 787 fuzz = min(fuzz, len(l)-1)
788 788 if fuzz:
789 789 top = 0
790 790 bot = 0
791 791 hlen = len(self.hunk)
792 792 for x in xrange(hlen - 1):
793 793 # the hunk starts with the @@ line, so use x+1
794 794 if self.hunk[x + 1][0] == ' ':
795 795 top += 1
796 796 else:
797 797 break
798 798 if not toponly:
799 799 for x in xrange(hlen - 1):
800 800 if self.hunk[hlen - bot - 1][0] == ' ':
801 801 bot += 1
802 802 else:
803 803 break
804 804
805 805 # top and bot now count context in the hunk
806 806 # adjust them if either one is short
807 807 context = max(top, bot, 3)
808 808 if bot < context:
809 809 bot = max(0, fuzz - (context - bot))
810 810 else:
811 811 bot = min(fuzz, bot)
812 812 if top < context:
813 813 top = max(0, fuzz - (context - top))
814 814 else:
815 815 top = min(fuzz, top)
816 816
817 817 return l[top:len(l)-bot]
818 818 return l
819 819
820 820 def old(self, fuzz=0, toponly=False):
821 821 return self.fuzzit(self.a, fuzz, toponly)
822 822
823 823 def new(self, fuzz=0, toponly=False):
824 824 return self.fuzzit(self.b, fuzz, toponly)
825 825
826 826 class binhunk:
827 827 'A binary patch file. Only understands literals so far.'
828 828 def __init__(self, gitpatch):
829 829 self.gitpatch = gitpatch
830 830 self.text = None
831 831 self.hunk = ['GIT binary patch\n']
832 832
833 833 def createfile(self):
834 834 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
835 835
836 836 def rmfile(self):
837 837 return self.gitpatch.op == 'DELETE'
838 838
839 839 def complete(self):
840 840 return self.text is not None
841 841
842 842 def new(self):
843 843 return [self.text]
844 844
845 845 def extract(self, lr):
846 846 line = lr.readline()
847 847 self.hunk.append(line)
848 848 while line and not line.startswith('literal '):
849 849 line = lr.readline()
850 850 self.hunk.append(line)
851 851 if not line:
852 852 raise PatchError(_('could not extract binary patch'))
853 853 size = int(line[8:].rstrip())
854 854 dec = []
855 855 line = lr.readline()
856 856 self.hunk.append(line)
857 857 while len(line) > 1:
858 858 l = line[0]
859 859 if l <= 'Z' and l >= 'A':
860 860 l = ord(l) - ord('A') + 1
861 861 else:
862 862 l = ord(l) - ord('a') + 27
863 863 dec.append(base85.b85decode(line[1:-1])[:l])
864 864 line = lr.readline()
865 865 self.hunk.append(line)
866 866 text = zlib.decompress(''.join(dec))
867 867 if len(text) != size:
868 868 raise PatchError(_('binary patch is %d bytes, not %d') %
869 869 len(text), size)
870 870 self.text = text
871 871
872 872 def parsefilename(str):
873 873 # --- filename \t|space stuff
874 874 s = str[4:].rstrip('\r\n')
875 875 i = s.find('\t')
876 876 if i < 0:
877 877 i = s.find(' ')
878 878 if i < 0:
879 879 return s
880 880 return s[:i]
881 881
882 882 def pathstrip(path, strip):
883 883 pathlen = len(path)
884 884 i = 0
885 885 if strip == 0:
886 886 return '', path.rstrip()
887 887 count = strip
888 888 while count > 0:
889 889 i = path.find('/', i)
890 890 if i == -1:
891 891 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
892 892 (count, strip, path))
893 893 i += 1
894 894 # consume '//' in the path
895 895 while i < pathlen - 1 and path[i] == '/':
896 896 i += 1
897 897 count -= 1
898 898 return path[:i].lstrip(), path[i:].rstrip()
899 899
900 900 def selectfile(afile_orig, bfile_orig, hunk, strip):
901 901 nulla = afile_orig == "/dev/null"
902 902 nullb = bfile_orig == "/dev/null"
903 903 abase, afile = pathstrip(afile_orig, strip)
904 904 gooda = not nulla and os.path.lexists(afile)
905 905 bbase, bfile = pathstrip(bfile_orig, strip)
906 906 if afile == bfile:
907 907 goodb = gooda
908 908 else:
909 909 goodb = not nullb and os.path.lexists(bfile)
910 910 createfunc = hunk.createfile
911 911 missing = not goodb and not gooda and not createfunc()
912 912
913 913 # some diff programs apparently produce patches where the afile is
914 914 # not /dev/null, but afile starts with bfile
915 915 abasedir = afile[:afile.rfind('/') + 1]
916 916 bbasedir = bfile[:bfile.rfind('/') + 1]
917 917 if missing and abasedir == bbasedir and afile.startswith(bfile):
918 918 # this isn't very pretty
919 919 hunk.create = True
920 920 if createfunc():
921 921 missing = False
922 922 else:
923 923 hunk.create = False
924 924
925 925 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
926 926 # diff is between a file and its backup. In this case, the original
927 927 # file should be patched (see original mpatch code).
928 928 isbackup = (abase == bbase and bfile.startswith(afile))
929 929 fname = None
930 930 if not missing:
931 931 if gooda and goodb:
932 932 fname = isbackup and afile or bfile
933 933 elif gooda:
934 934 fname = afile
935 935
936 936 if not fname:
937 937 if not nullb:
938 938 fname = isbackup and afile or bfile
939 939 elif not nulla:
940 940 fname = afile
941 941 else:
942 942 raise PatchError(_("undefined source and destination files"))
943 943
944 944 return fname, missing
945 945
946 946 def scangitpatch(lr, firstline):
947 947 """
948 948 Git patches can emit:
949 949 - rename a to b
950 950 - change b
951 951 - copy a to c
952 952 - change c
953 953
954 954 We cannot apply this sequence as-is, the renamed 'a' could not be
955 955 found for it would have been renamed already. And we cannot copy
956 956 from 'b' instead because 'b' would have been changed already. So
957 957 we scan the git patch for copy and rename commands so we can
958 958 perform the copies ahead of time.
959 959 """
960 960 pos = 0
961 961 try:
962 962 pos = lr.fp.tell()
963 963 fp = lr.fp
964 964 except IOError:
965 965 fp = cStringIO.StringIO(lr.fp.read())
966 966 gitlr = linereader(fp, lr.textmode)
967 967 gitlr.push(firstline)
968 968 gitpatches = readgitpatch(gitlr)
969 969 fp.seek(pos)
970 970 return gitpatches
971 971
972 972 def iterhunks(ui, fp, sourcefile=None):
973 973 """Read a patch and yield the following events:
974 974 - ("file", afile, bfile, firsthunk): select a new target file.
975 975 - ("hunk", hunk): a new hunk is ready to be applied, follows a
976 976 "file" event.
977 977 - ("git", gitchanges): current diff is in git format, gitchanges
978 978 maps filenames to gitpatch records. Unique event.
979 979 """
980 980 changed = {}
981 981 current_hunk = None
982 982 afile = ""
983 983 bfile = ""
984 984 state = None
985 985 hunknum = 0
986 986 emitfile = False
987 987 git = False
988 988
989 989 # our states
990 990 BFILE = 1
991 991 context = None
992 992 lr = linereader(fp)
993 993 # gitworkdone is True if a git operation (copy, rename, ...) was
994 994 # performed already for the current file. Useful when the file
995 995 # section may have no hunk.
996 996 gitworkdone = False
997 997
998 998 while True:
999 999 newfile = newgitfile = False
1000 1000 x = lr.readline()
1001 1001 if not x:
1002 1002 break
1003 1003 if current_hunk:
1004 1004 if x.startswith('\ '):
1005 1005 current_hunk.fix_newline()
1006 1006 yield 'hunk', current_hunk
1007 1007 current_hunk = None
1008 1008 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1009 1009 ((context is not False) and x.startswith('***************')))):
1010 1010 try:
1011 1011 if context is None and x.startswith('***************'):
1012 1012 context = True
1013 1013 gpatch = changed.get(bfile)
1014 1014 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1015 1015 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1016 1016 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1017 1017 except PatchError, err:
1018 1018 ui.debug(err)
1019 1019 current_hunk = None
1020 1020 continue
1021 1021 hunknum += 1
1022 1022 if emitfile:
1023 1023 emitfile = False
1024 1024 yield 'file', (afile, bfile, current_hunk)
1025 1025 elif state == BFILE and x.startswith('GIT binary patch'):
1026 1026 current_hunk = binhunk(changed[bfile])
1027 1027 hunknum += 1
1028 1028 if emitfile:
1029 1029 emitfile = False
1030 1030 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1031 1031 current_hunk.extract(lr)
1032 1032 elif x.startswith('diff --git'):
1033 1033 # check for git diff, scanning the whole patch file if needed
1034 1034 m = gitre.match(x)
1035 1035 gitworkdone = False
1036 1036 if m:
1037 1037 afile, bfile = m.group(1, 2)
1038 1038 if not git:
1039 1039 git = True
1040 1040 gitpatches = scangitpatch(lr, x)
1041 1041 yield 'git', gitpatches
1042 1042 for gp in gitpatches:
1043 1043 changed[gp.path] = gp
1044 1044 # else error?
1045 1045 # copy/rename + modify should modify target, not source
1046 1046 gp = changed.get(bfile)
1047 1047 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1048 1048 or gp.mode):
1049 1049 afile = bfile
1050 1050 gitworkdone = True
1051 1051 newgitfile = True
1052 1052 elif x.startswith('---'):
1053 1053 # check for a unified diff
1054 1054 l2 = lr.readline()
1055 1055 if not l2.startswith('+++'):
1056 1056 lr.push(l2)
1057 1057 continue
1058 1058 newfile = True
1059 1059 context = False
1060 1060 afile = parsefilename(x)
1061 1061 bfile = parsefilename(l2)
1062 1062 elif x.startswith('***'):
1063 1063 # check for a context diff
1064 1064 l2 = lr.readline()
1065 1065 if not l2.startswith('---'):
1066 1066 lr.push(l2)
1067 1067 continue
1068 1068 l3 = lr.readline()
1069 1069 lr.push(l3)
1070 1070 if not l3.startswith("***************"):
1071 1071 lr.push(l2)
1072 1072 continue
1073 1073 newfile = True
1074 1074 context = True
1075 1075 afile = parsefilename(x)
1076 1076 bfile = parsefilename(l2)
1077 1077
1078 1078 if newfile:
1079 1079 gitworkdone = False
1080 1080
1081 1081 if newgitfile or newfile:
1082 1082 emitfile = True
1083 1083 state = BFILE
1084 1084 hunknum = 0
1085 1085 if current_hunk:
1086 1086 if current_hunk.complete():
1087 1087 yield 'hunk', current_hunk
1088 1088 else:
1089 1089 raise PatchError(_("malformed patch %s %s") % (afile,
1090 1090 current_hunk.desc))
1091 1091
1092 1092 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1093 1093 """Reads a patch from fp and tries to apply it.
1094 1094
1095 1095 The dict 'changed' is filled in with all of the filenames changed
1096 1096 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1097 1097 found and 1 if there was any fuzz.
1098 1098
1099 1099 If 'eolmode' is 'strict', the patch content and patched file are
1100 1100 read in binary mode. Otherwise, line endings are ignored when
1101 1101 patching then normalized according to 'eolmode'.
1102 1102
1103 1103 Callers probably want to call 'cmdutil.updatedir' after this to
1104 1104 apply certain categories of changes not done by this function.
1105 1105 """
1106 1106 return _applydiff(
1107 1107 ui, fp, patchfile, copyfile,
1108 1108 changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
1109 1109
1110 1110
1111 1111 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
1112 1112 sourcefile=None, eolmode='strict'):
1113 1113 rejects = 0
1114 1114 err = 0
1115 1115 current_file = None
1116 1116 cwd = os.getcwd()
1117 1117 opener = util.opener(cwd)
1118 1118
1119 1119 def closefile():
1120 1120 if not current_file:
1121 1121 return 0
1122 1122 if current_file.dirty:
1123 1123 current_file.writelines(current_file.fname, current_file.lines)
1124 1124 current_file.write_rej()
1125 1125 return len(current_file.rej)
1126 1126
1127 1127 for state, values in iterhunks(ui, fp, sourcefile):
1128 1128 if state == 'hunk':
1129 1129 if not current_file:
1130 1130 continue
1131 1131 ret = current_file.apply(values)
1132 1132 if ret >= 0:
1133 1133 changed.setdefault(current_file.fname, None)
1134 1134 if ret > 0:
1135 1135 err = 1
1136 1136 elif state == 'file':
1137 1137 rejects += closefile()
1138 1138 afile, bfile, first_hunk = values
1139 1139 try:
1140 1140 if sourcefile:
1141 1141 current_file = patcher(ui, sourcefile, opener,
1142 1142 eolmode=eolmode)
1143 1143 else:
1144 1144 current_file, missing = selectfile(afile, bfile,
1145 1145 first_hunk, strip)
1146 1146 current_file = patcher(ui, current_file, opener,
1147 1147 missing=missing, eolmode=eolmode)
1148 1148 except PatchError, err:
1149 1149 ui.warn(str(err) + '\n')
1150 1150 current_file = None
1151 1151 rejects += 1
1152 1152 continue
1153 1153 elif state == 'git':
1154 1154 for gp in values:
1155 1155 gp.path = pathstrip(gp.path, strip - 1)[1]
1156 1156 if gp.oldpath:
1157 1157 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1158 1158 # Binary patches really overwrite target files, copying them
1159 1159 # will just make it fails with "target file exists"
1160 1160 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1161 1161 copyfn(gp.oldpath, gp.path, cwd)
1162 1162 changed[gp.path] = gp
1163 1163 else:
1164 1164 raise util.Abort(_('unsupported parser state: %s') % state)
1165 1165
1166 1166 rejects += closefile()
1167 1167
1168 1168 if rejects:
1169 1169 return -1
1170 1170 return err
1171 1171
1172 1172 def externalpatch(patcher, patchname, ui, strip, cwd, files):
1173 1173 """use <patcher> to apply <patchname> to the working directory.
1174 1174 returns whether patch was applied with fuzz factor."""
1175 1175
1176 1176 fuzz = False
1177 1177 args = []
1178 1178 if cwd:
1179 1179 args.append('-d %s' % util.shellquote(cwd))
1180 1180 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1181 1181 util.shellquote(patchname)))
1182 1182
1183 1183 for line in fp:
1184 1184 line = line.rstrip()
1185 1185 ui.note(line + '\n')
1186 1186 if line.startswith('patching file '):
1187 1187 pf = util.parse_patch_output(line)
1188 1188 printed_file = False
1189 1189 files.setdefault(pf, None)
1190 1190 elif line.find('with fuzz') >= 0:
1191 1191 fuzz = True
1192 1192 if not printed_file:
1193 1193 ui.warn(pf + '\n')
1194 1194 printed_file = True
1195 1195 ui.warn(line + '\n')
1196 1196 elif line.find('saving rejects to file') >= 0:
1197 1197 ui.warn(line + '\n')
1198 1198 elif line.find('FAILED') >= 0:
1199 1199 if not printed_file:
1200 1200 ui.warn(pf + '\n')
1201 1201 printed_file = True
1202 1202 ui.warn(line + '\n')
1203 1203 code = fp.close()
1204 1204 if code:
1205 1205 raise PatchError(_("patch command failed: %s") %
1206 1206 util.explain_exit(code)[0])
1207 1207 return fuzz
1208 1208
1209 1209 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1210 1210 """use builtin patch to apply <patchobj> to the working directory.
1211 1211 returns whether patch was applied with fuzz factor."""
1212 1212
1213 1213 if files is None:
1214 1214 files = {}
1215 1215 if eolmode is None:
1216 1216 eolmode = ui.config('patch', 'eol', 'strict')
1217 1217 if eolmode.lower() not in eolmodes:
1218 1218 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1219 1219 eolmode = eolmode.lower()
1220 1220
1221 1221 try:
1222 1222 fp = open(patchobj, 'rb')
1223 1223 except TypeError:
1224 1224 fp = patchobj
1225 1225 if cwd:
1226 1226 curdir = os.getcwd()
1227 1227 os.chdir(cwd)
1228 1228 try:
1229 1229 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1230 1230 finally:
1231 1231 if cwd:
1232 1232 os.chdir(curdir)
1233 1233 if fp != patchobj:
1234 1234 fp.close()
1235 1235 if ret < 0:
1236 raise PatchError
1236 raise PatchError(_('patch failed to apply'))
1237 1237 return ret > 0
1238 1238
1239 1239 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1240 1240 """Apply <patchname> to the working directory.
1241 1241
1242 1242 'eolmode' specifies how end of lines should be handled. It can be:
1243 1243 - 'strict': inputs are read in binary mode, EOLs are preserved
1244 1244 - 'crlf': EOLs are ignored when patching and reset to CRLF
1245 1245 - 'lf': EOLs are ignored when patching and reset to LF
1246 1246 - None: get it from user settings, default to 'strict'
1247 1247 'eolmode' is ignored when using an external patcher program.
1248 1248
1249 1249 Returns whether patch was applied with fuzz factor.
1250 1250 """
1251 1251 patcher = ui.config('ui', 'patch')
1252 1252 if files is None:
1253 1253 files = {}
1254 1254 try:
1255 1255 if patcher:
1256 1256 return externalpatch(patcher, patchname, ui, strip, cwd, files)
1257 1257 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1258 1258 except PatchError, err:
1259 s = str(err)
1260 if s:
1261 raise util.Abort(s)
1262 else:
1263 raise util.Abort(_('patch failed to apply'))
1259 raise util.Abort(str(err))
1264 1260
1265 1261 def b85diff(to, tn):
1266 1262 '''print base85-encoded binary diff'''
1267 1263 def gitindex(text):
1268 1264 if not text:
1269 1265 return hex(nullid)
1270 1266 l = len(text)
1271 1267 s = util.sha1('blob %d\0' % l)
1272 1268 s.update(text)
1273 1269 return s.hexdigest()
1274 1270
1275 1271 def fmtline(line):
1276 1272 l = len(line)
1277 1273 if l <= 26:
1278 1274 l = chr(ord('A') + l - 1)
1279 1275 else:
1280 1276 l = chr(l - 26 + ord('a') - 1)
1281 1277 return '%c%s\n' % (l, base85.b85encode(line, True))
1282 1278
1283 1279 def chunk(text, csize=52):
1284 1280 l = len(text)
1285 1281 i = 0
1286 1282 while i < l:
1287 1283 yield text[i:i + csize]
1288 1284 i += csize
1289 1285
1290 1286 tohash = gitindex(to)
1291 1287 tnhash = gitindex(tn)
1292 1288 if tohash == tnhash:
1293 1289 return ""
1294 1290
1295 1291 # TODO: deltas
1296 1292 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1297 1293 (tohash, tnhash, len(tn))]
1298 1294 for l in chunk(zlib.compress(tn)):
1299 1295 ret.append(fmtline(l))
1300 1296 ret.append('\n')
1301 1297 return ''.join(ret)
1302 1298
1303 1299 class GitDiffRequired(Exception):
1304 1300 pass
1305 1301
1306 1302 def diffopts(ui, opts=None, untrusted=False):
1307 1303 def get(key, name=None, getter=ui.configbool):
1308 1304 return ((opts and opts.get(key)) or
1309 1305 getter('diff', name or key, None, untrusted=untrusted))
1310 1306 return mdiff.diffopts(
1311 1307 text=opts and opts.get('text'),
1312 1308 git=get('git'),
1313 1309 nodates=get('nodates'),
1314 1310 showfunc=get('show_function', 'showfunc'),
1315 1311 ignorews=get('ignore_all_space', 'ignorews'),
1316 1312 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1317 1313 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1318 1314 context=get('unified', getter=ui.config))
1319 1315
1320 1316 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1321 1317 losedatafn=None, prefix=''):
1322 1318 '''yields diff of changes to files between two nodes, or node and
1323 1319 working directory.
1324 1320
1325 1321 if node1 is None, use first dirstate parent instead.
1326 1322 if node2 is None, compare node1 with working directory.
1327 1323
1328 1324 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1329 1325 every time some change cannot be represented with the current
1330 1326 patch format. Return False to upgrade to git patch format, True to
1331 1327 accept the loss or raise an exception to abort the diff. It is
1332 1328 called with the name of current file being diffed as 'fn'. If set
1333 1329 to None, patches will always be upgraded to git format when
1334 1330 necessary.
1335 1331
1336 1332 prefix is a filename prefix that is prepended to all filenames on
1337 1333 display (used for subrepos).
1338 1334 '''
1339 1335
1340 1336 if opts is None:
1341 1337 opts = mdiff.defaultopts
1342 1338
1343 1339 if not node1 and not node2:
1344 1340 node1 = repo.dirstate.parents()[0]
1345 1341
1346 1342 def lrugetfilectx():
1347 1343 cache = {}
1348 1344 order = []
1349 1345 def getfilectx(f, ctx):
1350 1346 fctx = ctx.filectx(f, filelog=cache.get(f))
1351 1347 if f not in cache:
1352 1348 if len(cache) > 20:
1353 1349 del cache[order.pop(0)]
1354 1350 cache[f] = fctx.filelog()
1355 1351 else:
1356 1352 order.remove(f)
1357 1353 order.append(f)
1358 1354 return fctx
1359 1355 return getfilectx
1360 1356 getfilectx = lrugetfilectx()
1361 1357
1362 1358 ctx1 = repo[node1]
1363 1359 ctx2 = repo[node2]
1364 1360
1365 1361 if not changes:
1366 1362 changes = repo.status(ctx1, ctx2, match=match)
1367 1363 modified, added, removed = changes[:3]
1368 1364
1369 1365 if not modified and not added and not removed:
1370 1366 return []
1371 1367
1372 1368 revs = None
1373 1369 if not repo.ui.quiet:
1374 1370 hexfunc = repo.ui.debugflag and hex or short
1375 1371 revs = [hexfunc(node) for node in [node1, node2] if node]
1376 1372
1377 1373 copy = {}
1378 1374 if opts.git or opts.upgrade:
1379 1375 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1380 1376
1381 1377 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1382 1378 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1383 1379 if opts.upgrade and not opts.git:
1384 1380 try:
1385 1381 def losedata(fn):
1386 1382 if not losedatafn or not losedatafn(fn=fn):
1387 1383 raise GitDiffRequired()
1388 1384 # Buffer the whole output until we are sure it can be generated
1389 1385 return list(difffn(opts.copy(git=False), losedata))
1390 1386 except GitDiffRequired:
1391 1387 return difffn(opts.copy(git=True), None)
1392 1388 else:
1393 1389 return difffn(opts, None)
1394 1390
1395 1391 def difflabel(func, *args, **kw):
1396 1392 '''yields 2-tuples of (output, label) based on the output of func()'''
1397 1393 prefixes = [('diff', 'diff.diffline'),
1398 1394 ('copy', 'diff.extended'),
1399 1395 ('rename', 'diff.extended'),
1400 1396 ('old', 'diff.extended'),
1401 1397 ('new', 'diff.extended'),
1402 1398 ('deleted', 'diff.extended'),
1403 1399 ('---', 'diff.file_a'),
1404 1400 ('+++', 'diff.file_b'),
1405 1401 ('@@', 'diff.hunk'),
1406 1402 ('-', 'diff.deleted'),
1407 1403 ('+', 'diff.inserted')]
1408 1404
1409 1405 for chunk in func(*args, **kw):
1410 1406 lines = chunk.split('\n')
1411 1407 for i, line in enumerate(lines):
1412 1408 if i != 0:
1413 1409 yield ('\n', '')
1414 1410 stripline = line
1415 1411 if line and line[0] in '+-':
1416 1412 # highlight trailing whitespace, but only in changed lines
1417 1413 stripline = line.rstrip()
1418 1414 for prefix, label in prefixes:
1419 1415 if stripline.startswith(prefix):
1420 1416 yield (stripline, label)
1421 1417 break
1422 1418 else:
1423 1419 yield (line, '')
1424 1420 if line != stripline:
1425 1421 yield (line[len(stripline):], 'diff.trailingwhitespace')
1426 1422
1427 1423 def diffui(*args, **kw):
1428 1424 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1429 1425 return difflabel(diff, *args, **kw)
1430 1426
1431 1427
1432 1428 def _addmodehdr(header, omode, nmode):
1433 1429 if omode != nmode:
1434 1430 header.append('old mode %s\n' % omode)
1435 1431 header.append('new mode %s\n' % nmode)
1436 1432
1437 1433 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1438 1434 copy, getfilectx, opts, losedatafn, prefix):
1439 1435
1440 1436 def join(f):
1441 1437 return os.path.join(prefix, f)
1442 1438
1443 1439 date1 = util.datestr(ctx1.date())
1444 1440 man1 = ctx1.manifest()
1445 1441
1446 1442 gone = set()
1447 1443 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1448 1444
1449 1445 copyto = dict([(v, k) for k, v in copy.items()])
1450 1446
1451 1447 if opts.git:
1452 1448 revs = None
1453 1449
1454 1450 for f in sorted(modified + added + removed):
1455 1451 to = None
1456 1452 tn = None
1457 1453 dodiff = True
1458 1454 header = []
1459 1455 if f in man1:
1460 1456 to = getfilectx(f, ctx1).data()
1461 1457 if f not in removed:
1462 1458 tn = getfilectx(f, ctx2).data()
1463 1459 a, b = f, f
1464 1460 if opts.git or losedatafn:
1465 1461 if f in added:
1466 1462 mode = gitmode[ctx2.flags(f)]
1467 1463 if f in copy or f in copyto:
1468 1464 if opts.git:
1469 1465 if f in copy:
1470 1466 a = copy[f]
1471 1467 else:
1472 1468 a = copyto[f]
1473 1469 omode = gitmode[man1.flags(a)]
1474 1470 _addmodehdr(header, omode, mode)
1475 1471 if a in removed and a not in gone:
1476 1472 op = 'rename'
1477 1473 gone.add(a)
1478 1474 else:
1479 1475 op = 'copy'
1480 1476 header.append('%s from %s\n' % (op, join(a)))
1481 1477 header.append('%s to %s\n' % (op, join(f)))
1482 1478 to = getfilectx(a, ctx1).data()
1483 1479 else:
1484 1480 losedatafn(f)
1485 1481 else:
1486 1482 if opts.git:
1487 1483 header.append('new file mode %s\n' % mode)
1488 1484 elif ctx2.flags(f):
1489 1485 losedatafn(f)
1490 1486 # In theory, if tn was copied or renamed we should check
1491 1487 # if the source is binary too but the copy record already
1492 1488 # forces git mode.
1493 1489 if util.binary(tn):
1494 1490 if opts.git:
1495 1491 dodiff = 'binary'
1496 1492 else:
1497 1493 losedatafn(f)
1498 1494 if not opts.git and not tn:
1499 1495 # regular diffs cannot represent new empty file
1500 1496 losedatafn(f)
1501 1497 elif f in removed:
1502 1498 if opts.git:
1503 1499 # have we already reported a copy above?
1504 1500 if ((f in copy and copy[f] in added
1505 1501 and copyto[copy[f]] == f) or
1506 1502 (f in copyto and copyto[f] in added
1507 1503 and copy[copyto[f]] == f)):
1508 1504 dodiff = False
1509 1505 else:
1510 1506 header.append('deleted file mode %s\n' %
1511 1507 gitmode[man1.flags(f)])
1512 1508 elif not to or util.binary(to):
1513 1509 # regular diffs cannot represent empty file deletion
1514 1510 losedatafn(f)
1515 1511 else:
1516 1512 oflag = man1.flags(f)
1517 1513 nflag = ctx2.flags(f)
1518 1514 binary = util.binary(to) or util.binary(tn)
1519 1515 if opts.git:
1520 1516 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1521 1517 if binary:
1522 1518 dodiff = 'binary'
1523 1519 elif binary or nflag != oflag:
1524 1520 losedatafn(f)
1525 1521 if opts.git:
1526 1522 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1527 1523
1528 1524 if dodiff:
1529 1525 if dodiff == 'binary':
1530 1526 text = b85diff(to, tn)
1531 1527 else:
1532 1528 text = mdiff.unidiff(to, date1,
1533 1529 # ctx2 date may be dynamic
1534 1530 tn, util.datestr(ctx2.date()),
1535 1531 join(a), join(b), revs, opts=opts)
1536 1532 if header and (text or len(header) > 1):
1537 1533 yield ''.join(header)
1538 1534 if text:
1539 1535 yield text
1540 1536
1541 1537 def diffstatdata(lines):
1542 1538 filename, adds, removes = None, 0, 0
1543 1539 for line in lines:
1544 1540 if line.startswith('diff'):
1545 1541 if filename:
1546 1542 isbinary = adds == 0 and removes == 0
1547 1543 yield (filename, adds, removes, isbinary)
1548 1544 # set numbers to 0 anyway when starting new file
1549 1545 adds, removes = 0, 0
1550 1546 if line.startswith('diff --git'):
1551 1547 filename = gitre.search(line).group(1)
1552 1548 else:
1553 1549 # format: "diff -r ... -r ... filename"
1554 1550 filename = line.split(None, 5)[-1]
1555 1551 elif line.startswith('+') and not line.startswith('+++'):
1556 1552 adds += 1
1557 1553 elif line.startswith('-') and not line.startswith('---'):
1558 1554 removes += 1
1559 1555 if filename:
1560 1556 isbinary = adds == 0 and removes == 0
1561 1557 yield (filename, adds, removes, isbinary)
1562 1558
1563 1559 def diffstat(lines, width=80, git=False):
1564 1560 output = []
1565 1561 stats = list(diffstatdata(lines))
1566 1562
1567 1563 maxtotal, maxname = 0, 0
1568 1564 totaladds, totalremoves = 0, 0
1569 1565 hasbinary = False
1570 1566
1571 1567 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1572 1568 for filename, adds, removes, isbinary in stats]
1573 1569
1574 1570 for filename, adds, removes, isbinary, namewidth in sized:
1575 1571 totaladds += adds
1576 1572 totalremoves += removes
1577 1573 maxname = max(maxname, namewidth)
1578 1574 maxtotal = max(maxtotal, adds + removes)
1579 1575 if isbinary:
1580 1576 hasbinary = True
1581 1577
1582 1578 countwidth = len(str(maxtotal))
1583 1579 if hasbinary and countwidth < 3:
1584 1580 countwidth = 3
1585 1581 graphwidth = width - countwidth - maxname - 6
1586 1582 if graphwidth < 10:
1587 1583 graphwidth = 10
1588 1584
1589 1585 def scale(i):
1590 1586 if maxtotal <= graphwidth:
1591 1587 return i
1592 1588 # If diffstat runs out of room it doesn't print anything,
1593 1589 # which isn't very useful, so always print at least one + or -
1594 1590 # if there were at least some changes.
1595 1591 return max(i * graphwidth // maxtotal, int(bool(i)))
1596 1592
1597 1593 for filename, adds, removes, isbinary, namewidth in sized:
1598 1594 if git and isbinary:
1599 1595 count = 'Bin'
1600 1596 else:
1601 1597 count = adds + removes
1602 1598 pluses = '+' * scale(adds)
1603 1599 minuses = '-' * scale(removes)
1604 1600 output.append(' %s%s | %*s %s%s\n' %
1605 1601 (filename, ' ' * (maxname - namewidth),
1606 1602 countwidth, count,
1607 1603 pluses, minuses))
1608 1604
1609 1605 if stats:
1610 1606 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1611 1607 % (len(stats), totaladds, totalremoves))
1612 1608
1613 1609 return ''.join(output)
1614 1610
1615 1611 def diffstatui(*args, **kw):
1616 1612 '''like diffstat(), but yields 2-tuples of (output, label) for
1617 1613 ui.write()
1618 1614 '''
1619 1615
1620 1616 for line in diffstat(*args, **kw).splitlines():
1621 1617 if line and line[-1] in '+-':
1622 1618 name, graph = line.rsplit(' ', 1)
1623 1619 yield (name + ' ', '')
1624 1620 m = re.search(r'\++', graph)
1625 1621 if m:
1626 1622 yield (m.group(0), 'diffstat.inserted')
1627 1623 m = re.search(r'-+', graph)
1628 1624 if m:
1629 1625 yield (m.group(0), 'diffstat.deleted')
1630 1626 else:
1631 1627 yield (line, '')
1632 1628 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now