##// END OF EJS Templates
patch.internalpatch: accept a prefix parameter
Siddharth Agarwal -
r24254:60c279ab default
parent child Browse files
Show More
@@ -1,672 +1,672 b''
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 _
11 11 from mercurial import cmdutil, commands, extensions, hg, patch
12 12 from mercurial import util
13 13 import copy, cStringIO, errno, os, re, shutil, tempfile
14 14
15 15 cmdtable = {}
16 16 command = cmdutil.command(cmdtable)
17 17 testedwith = 'internal'
18 18
19 19 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
20 20
21 21 def scanpatch(fp):
22 22 """like patch.iterhunks, but yield different events
23 23
24 24 - ('file', [header_lines + fromfile + tofile])
25 25 - ('context', [context_lines])
26 26 - ('hunk', [hunk_lines])
27 27 - ('range', (-start,len, +start,len, proc))
28 28 """
29 29 lr = patch.linereader(fp)
30 30
31 31 def scanwhile(first, p):
32 32 """scan lr while predicate holds"""
33 33 lines = [first]
34 34 while True:
35 35 line = lr.readline()
36 36 if not line:
37 37 break
38 38 if p(line):
39 39 lines.append(line)
40 40 else:
41 41 lr.push(line)
42 42 break
43 43 return lines
44 44
45 45 while True:
46 46 line = lr.readline()
47 47 if not line:
48 48 break
49 49 if line.startswith('diff --git a/') or line.startswith('diff -r '):
50 50 def notheader(line):
51 51 s = line.split(None, 1)
52 52 return not s or s[0] not in ('---', 'diff')
53 53 header = scanwhile(line, notheader)
54 54 fromfile = lr.readline()
55 55 if fromfile.startswith('---'):
56 56 tofile = lr.readline()
57 57 header += [fromfile, tofile]
58 58 else:
59 59 lr.push(fromfile)
60 60 yield 'file', header
61 61 elif line[0] == ' ':
62 62 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
63 63 elif line[0] in '-+':
64 64 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
65 65 else:
66 66 m = lines_re.match(line)
67 67 if m:
68 68 yield 'range', m.groups()
69 69 else:
70 70 yield 'other', line
71 71
72 72 class header(object):
73 73 """patch header
74 74
75 75 XXX shouldn't we move this to mercurial/patch.py ?
76 76 """
77 77 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
78 78 diff_re = re.compile('diff -r .* (.*)$')
79 79 allhunks_re = re.compile('(?:index|deleted file) ')
80 80 pretty_re = re.compile('(?:new file|deleted file) ')
81 81 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
82 82
83 83 def __init__(self, header):
84 84 self.header = header
85 85 self.hunks = []
86 86
87 87 def binary(self):
88 88 return util.any(h.startswith('index ') for h in self.header)
89 89
90 90 def pretty(self, fp):
91 91 for h in self.header:
92 92 if h.startswith('index '):
93 93 fp.write(_('this modifies a binary file (all or nothing)\n'))
94 94 break
95 95 if self.pretty_re.match(h):
96 96 fp.write(h)
97 97 if self.binary():
98 98 fp.write(_('this is a binary file\n'))
99 99 break
100 100 if h.startswith('---'):
101 101 fp.write(_('%d hunks, %d lines changed\n') %
102 102 (len(self.hunks),
103 103 sum([max(h.added, h.removed) for h in self.hunks])))
104 104 break
105 105 fp.write(h)
106 106
107 107 def write(self, fp):
108 108 fp.write(''.join(self.header))
109 109
110 110 def allhunks(self):
111 111 return util.any(self.allhunks_re.match(h) for h in self.header)
112 112
113 113 def files(self):
114 114 match = self.diffgit_re.match(self.header[0])
115 115 if match:
116 116 fromfile, tofile = match.groups()
117 117 if fromfile == tofile:
118 118 return [fromfile]
119 119 return [fromfile, tofile]
120 120 else:
121 121 return self.diff_re.match(self.header[0]).groups()
122 122
123 123 def filename(self):
124 124 return self.files()[-1]
125 125
126 126 def __repr__(self):
127 127 return '<header %s>' % (' '.join(map(repr, self.files())))
128 128
129 129 def special(self):
130 130 return util.any(self.special_re.match(h) for h in self.header)
131 131
132 132 def countchanges(hunk):
133 133 """hunk -> (n+,n-)"""
134 134 add = len([h for h in hunk if h[0] == '+'])
135 135 rem = len([h for h in hunk if h[0] == '-'])
136 136 return add, rem
137 137
138 138 class hunk(object):
139 139 """patch hunk
140 140
141 141 XXX shouldn't we merge this with patch.hunk ?
142 142 """
143 143 maxcontext = 3
144 144
145 145 def __init__(self, header, fromline, toline, proc, before, hunk, after):
146 146 def trimcontext(number, lines):
147 147 delta = len(lines) - self.maxcontext
148 148 if False and delta > 0:
149 149 return number + delta, lines[:self.maxcontext]
150 150 return number, lines
151 151
152 152 self.header = header
153 153 self.fromline, self.before = trimcontext(fromline, before)
154 154 self.toline, self.after = trimcontext(toline, after)
155 155 self.proc = proc
156 156 self.hunk = hunk
157 157 self.added, self.removed = countchanges(self.hunk)
158 158
159 159 def write(self, fp):
160 160 delta = len(self.before) + len(self.after)
161 161 if self.after and self.after[-1] == '\\ No newline at end of file\n':
162 162 delta -= 1
163 163 fromlen = delta + self.removed
164 164 tolen = delta + self.added
165 165 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
166 166 (self.fromline, fromlen, self.toline, tolen,
167 167 self.proc and (' ' + self.proc)))
168 168 fp.write(''.join(self.before + self.hunk + self.after))
169 169
170 170 pretty = write
171 171
172 172 def filename(self):
173 173 return self.header.filename()
174 174
175 175 def __repr__(self):
176 176 return '<hunk %r@%d>' % (self.filename(), self.fromline)
177 177
178 178 def parsepatch(fp):
179 179 """patch -> [] of headers -> [] of hunks """
180 180 class parser(object):
181 181 """patch parsing state machine"""
182 182 def __init__(self):
183 183 self.fromline = 0
184 184 self.toline = 0
185 185 self.proc = ''
186 186 self.header = None
187 187 self.context = []
188 188 self.before = []
189 189 self.hunk = []
190 190 self.headers = []
191 191
192 192 def addrange(self, limits):
193 193 fromstart, fromend, tostart, toend, proc = limits
194 194 self.fromline = int(fromstart)
195 195 self.toline = int(tostart)
196 196 self.proc = proc
197 197
198 198 def addcontext(self, context):
199 199 if self.hunk:
200 200 h = hunk(self.header, self.fromline, self.toline, self.proc,
201 201 self.before, self.hunk, context)
202 202 self.header.hunks.append(h)
203 203 self.fromline += len(self.before) + h.removed
204 204 self.toline += len(self.before) + h.added
205 205 self.before = []
206 206 self.hunk = []
207 207 self.proc = ''
208 208 self.context = context
209 209
210 210 def addhunk(self, hunk):
211 211 if self.context:
212 212 self.before = self.context
213 213 self.context = []
214 214 self.hunk = hunk
215 215
216 216 def newfile(self, hdr):
217 217 self.addcontext([])
218 218 h = header(hdr)
219 219 self.headers.append(h)
220 220 self.header = h
221 221
222 222 def addother(self, line):
223 223 pass # 'other' lines are ignored
224 224
225 225 def finished(self):
226 226 self.addcontext([])
227 227 return self.headers
228 228
229 229 transitions = {
230 230 'file': {'context': addcontext,
231 231 'file': newfile,
232 232 'hunk': addhunk,
233 233 'range': addrange},
234 234 'context': {'file': newfile,
235 235 'hunk': addhunk,
236 236 'range': addrange,
237 237 'other': addother},
238 238 'hunk': {'context': addcontext,
239 239 'file': newfile,
240 240 'range': addrange},
241 241 'range': {'context': addcontext,
242 242 'hunk': addhunk},
243 243 'other': {'other': addother},
244 244 }
245 245
246 246 p = parser()
247 247
248 248 state = 'context'
249 249 for newstate, data in scanpatch(fp):
250 250 try:
251 251 p.transitions[state][newstate](p, data)
252 252 except KeyError:
253 253 raise patch.PatchError('unhandled transition: %s -> %s' %
254 254 (state, newstate))
255 255 state = newstate
256 256 return p.finished()
257 257
258 258 def filterpatch(ui, headers):
259 259 """Interactively filter patch chunks into applied-only chunks"""
260 260
261 261 def prompt(skipfile, skipall, query, chunk):
262 262 """prompt query, and process base inputs
263 263
264 264 - y/n for the rest of file
265 265 - y/n for the rest
266 266 - ? (help)
267 267 - q (quit)
268 268
269 269 Return True/False and possibly updated skipfile and skipall.
270 270 """
271 271 newpatches = None
272 272 if skipall is not None:
273 273 return skipall, skipfile, skipall, newpatches
274 274 if skipfile is not None:
275 275 return skipfile, skipfile, skipall, newpatches
276 276 while True:
277 277 resps = _('[Ynesfdaq?]'
278 278 '$$ &Yes, record this change'
279 279 '$$ &No, skip this change'
280 280 '$$ &Edit this change manually'
281 281 '$$ &Skip remaining changes to this file'
282 282 '$$ Record remaining changes to this &file'
283 283 '$$ &Done, skip remaining changes and files'
284 284 '$$ Record &all changes to all remaining files'
285 285 '$$ &Quit, recording no changes'
286 286 '$$ &? (display help)')
287 287 r = ui.promptchoice("%s %s" % (query, resps))
288 288 ui.write("\n")
289 289 if r == 8: # ?
290 290 for c, t in ui.extractchoices(resps)[1]:
291 291 ui.write('%s - %s\n' % (c, t.lower()))
292 292 continue
293 293 elif r == 0: # yes
294 294 ret = True
295 295 elif r == 1: # no
296 296 ret = False
297 297 elif r == 2: # Edit patch
298 298 if chunk is None:
299 299 ui.write(_('cannot edit patch for whole file'))
300 300 ui.write("\n")
301 301 continue
302 302 if chunk.header.binary():
303 303 ui.write(_('cannot edit patch for binary file'))
304 304 ui.write("\n")
305 305 continue
306 306 # Patch comment based on the Git one (based on comment at end of
307 307 # http://mercurial.selenic.com/wiki/RecordExtension)
308 308 phelp = '---' + _("""
309 309 To remove '-' lines, make them ' ' lines (context).
310 310 To remove '+' lines, delete them.
311 311 Lines starting with # will be removed from the patch.
312 312
313 313 If the patch applies cleanly, the edited hunk will immediately be
314 314 added to the record list. If it does not apply cleanly, a rejects
315 315 file will be generated: you can use that when you try again. If
316 316 all lines of the hunk are removed, then the edit is aborted and
317 317 the hunk is left unchanged.
318 318 """)
319 319 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
320 320 suffix=".diff", text=True)
321 321 ncpatchfp = None
322 322 try:
323 323 # Write the initial patch
324 324 f = os.fdopen(patchfd, "w")
325 325 chunk.header.write(f)
326 326 chunk.write(f)
327 327 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
328 328 f.close()
329 329 # Start the editor and wait for it to complete
330 330 editor = ui.geteditor()
331 331 ui.system("%s \"%s\"" % (editor, patchfn),
332 332 environ={'HGUSER': ui.username()},
333 333 onerr=util.Abort, errprefix=_("edit failed"))
334 334 # Remove comment lines
335 335 patchfp = open(patchfn)
336 336 ncpatchfp = cStringIO.StringIO()
337 337 for line in patchfp:
338 338 if not line.startswith('#'):
339 339 ncpatchfp.write(line)
340 340 patchfp.close()
341 341 ncpatchfp.seek(0)
342 342 newpatches = parsepatch(ncpatchfp)
343 343 finally:
344 344 os.unlink(patchfn)
345 345 del ncpatchfp
346 346 # Signal that the chunk shouldn't be applied as-is, but
347 347 # provide the new patch to be used instead.
348 348 ret = False
349 349 elif r == 3: # Skip
350 350 ret = skipfile = False
351 351 elif r == 4: # file (Record remaining)
352 352 ret = skipfile = True
353 353 elif r == 5: # done, skip remaining
354 354 ret = skipall = False
355 355 elif r == 6: # all
356 356 ret = skipall = True
357 357 elif r == 7: # quit
358 358 raise util.Abort(_('user quit'))
359 359 return ret, skipfile, skipall, newpatches
360 360
361 361 seen = set()
362 362 applied = {} # 'filename' -> [] of chunks
363 363 skipfile, skipall = None, None
364 364 pos, total = 1, sum(len(h.hunks) for h in headers)
365 365 for h in headers:
366 366 pos += len(h.hunks)
367 367 skipfile = None
368 368 fixoffset = 0
369 369 hdr = ''.join(h.header)
370 370 if hdr in seen:
371 371 continue
372 372 seen.add(hdr)
373 373 if skipall is None:
374 374 h.pretty(ui)
375 375 msg = (_('examine changes to %s?') %
376 376 _(' and ').join("'%s'" % f for f in h.files()))
377 377 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
378 378 if not r:
379 379 continue
380 380 applied[h.filename()] = [h]
381 381 if h.allhunks():
382 382 applied[h.filename()] += h.hunks
383 383 continue
384 384 for i, chunk in enumerate(h.hunks):
385 385 if skipfile is None and skipall is None:
386 386 chunk.pretty(ui)
387 387 if total == 1:
388 388 msg = _("record this change to '%s'?") % chunk.filename()
389 389 else:
390 390 idx = pos - len(h.hunks) + i
391 391 msg = _("record change %d/%d to '%s'?") % (idx, total,
392 392 chunk.filename())
393 393 r, skipfile, skipall, newpatches = prompt(skipfile,
394 394 skipall, msg, chunk)
395 395 if r:
396 396 if fixoffset:
397 397 chunk = copy.copy(chunk)
398 398 chunk.toline += fixoffset
399 399 applied[chunk.filename()].append(chunk)
400 400 elif newpatches is not None:
401 401 for newpatch in newpatches:
402 402 for newhunk in newpatch.hunks:
403 403 if fixoffset:
404 404 newhunk.toline += fixoffset
405 405 applied[newhunk.filename()].append(newhunk)
406 406 else:
407 407 fixoffset += chunk.removed - chunk.added
408 408 return sum([h for h in applied.itervalues()
409 409 if h[0].special() or len(h) > 1], [])
410 410
411 411 @command("record",
412 412 # same options as commit + white space diff options
413 413 commands.table['^commit|ci'][1][:] + commands.diffwsopts,
414 414 _('hg record [OPTION]... [FILE]...'))
415 415 def record(ui, repo, *pats, **opts):
416 416 '''interactively select changes to commit
417 417
418 418 If a list of files is omitted, all changes reported by :hg:`status`
419 419 will be candidates for recording.
420 420
421 421 See :hg:`help dates` for a list of formats valid for -d/--date.
422 422
423 423 You will be prompted for whether to record changes to each
424 424 modified file, and for files with multiple changes, for each
425 425 change to use. For each query, the following responses are
426 426 possible::
427 427
428 428 y - record this change
429 429 n - skip this change
430 430 e - edit this change manually
431 431
432 432 s - skip remaining changes to this file
433 433 f - record remaining changes to this file
434 434
435 435 d - done, skip remaining changes and files
436 436 a - record all changes to all remaining files
437 437 q - quit, recording no changes
438 438
439 439 ? - display help
440 440
441 441 This command is not available when committing a merge.'''
442 442
443 443 dorecord(ui, repo, commands.commit, 'commit', False, *pats, **opts)
444 444
445 445 def qrefresh(origfn, ui, repo, *pats, **opts):
446 446 if not opts['interactive']:
447 447 return origfn(ui, repo, *pats, **opts)
448 448
449 449 mq = extensions.find('mq')
450 450
451 451 def committomq(ui, repo, *pats, **opts):
452 452 # At this point the working copy contains only changes that
453 453 # were accepted. All other changes were reverted.
454 454 # We can't pass *pats here since qrefresh will undo all other
455 455 # changed files in the patch that aren't in pats.
456 456 mq.refresh(ui, repo, **opts)
457 457
458 458 # backup all changed files
459 459 dorecord(ui, repo, committomq, 'qrefresh', True, *pats, **opts)
460 460
461 461 # This command registration is replaced during uisetup().
462 462 @command('qrecord',
463 463 [],
464 464 _('hg qrecord [OPTION]... PATCH [FILE]...'),
465 465 inferrepo=True)
466 466 def qrecord(ui, repo, patch, *pats, **opts):
467 467 '''interactively record a new patch
468 468
469 469 See :hg:`help qnew` & :hg:`help record` for more information and
470 470 usage.
471 471 '''
472 472
473 473 try:
474 474 mq = extensions.find('mq')
475 475 except KeyError:
476 476 raise util.Abort(_("'mq' extension not loaded"))
477 477
478 478 repo.mq.checkpatchname(patch)
479 479
480 480 def committomq(ui, repo, *pats, **opts):
481 481 opts['checkname'] = False
482 482 mq.new(ui, repo, patch, *pats, **opts)
483 483
484 484 dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts)
485 485
486 486 def qnew(origfn, ui, repo, patch, *args, **opts):
487 487 if opts['interactive']:
488 488 return qrecord(ui, repo, patch, *args, **opts)
489 489 return origfn(ui, repo, patch, *args, **opts)
490 490
491 491 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts):
492 492 if not ui.interactive():
493 493 raise util.Abort(_('running non-interactively, use %s instead') %
494 494 cmdsuggest)
495 495
496 496 # make sure username is set before going interactive
497 497 if not opts.get('user'):
498 498 ui.username() # raise exception, username not provided
499 499
500 500 def recordfunc(ui, repo, message, match, opts):
501 501 """This is generic record driver.
502 502
503 503 Its job is to interactively filter local changes, and
504 504 accordingly prepare working directory into a state in which the
505 505 job can be delegated to a non-interactive commit command such as
506 506 'commit' or 'qrefresh'.
507 507
508 508 After the actual job is done by non-interactive command, the
509 509 working directory is restored to its original state.
510 510
511 511 In the end we'll record interesting changes, and everything else
512 512 will be left in place, so the user can continue working.
513 513 """
514 514
515 515 cmdutil.checkunfinished(repo, commit=True)
516 516 merge = len(repo[None].parents()) > 1
517 517 if merge:
518 518 raise util.Abort(_('cannot partially commit a merge '
519 519 '(use "hg commit" instead)'))
520 520
521 521 status = repo.status(match=match)
522 522 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
523 523 diffopts.nodates = True
524 524 diffopts.git = True
525 525 originalchunks = patch.diff(repo, changes=status, opts=diffopts)
526 526 fp = cStringIO.StringIO()
527 527 fp.write(''.join(originalchunks))
528 528 fp.seek(0)
529 529
530 530 # 1. filter patch, so we have intending-to apply subset of it
531 531 try:
532 532 chunks = filterpatch(ui, parsepatch(fp))
533 533 except patch.PatchError, err:
534 534 raise util.Abort(_('error parsing patch: %s') % err)
535 535
536 536 del fp
537 537
538 538 contenders = set()
539 539 for h in chunks:
540 540 try:
541 541 contenders.update(set(h.files()))
542 542 except AttributeError:
543 543 pass
544 544
545 545 changed = status.modified + status.added + status.removed
546 546 newfiles = [f for f in changed if f in contenders]
547 547 if not newfiles:
548 548 ui.status(_('no changes to record\n'))
549 549 return 0
550 550
551 551 newandmodifiedfiles = set()
552 552 for h in chunks:
553 553 ishunk = isinstance(h, hunk)
554 554 isnew = h.filename() in status.added
555 555 if ishunk and isnew and not h in originalchunks:
556 556 newandmodifiedfiles.add(h.filename())
557 557
558 558 modified = set(status.modified)
559 559
560 560 # 2. backup changed files, so we can restore them in the end
561 561
562 562 if backupall:
563 563 tobackup = changed
564 564 else:
565 565 tobackup = [f for f in newfiles
566 566 if f in modified or f in newandmodifiedfiles]
567 567
568 568 backups = {}
569 569 if tobackup:
570 570 backupdir = repo.join('record-backups')
571 571 try:
572 572 os.mkdir(backupdir)
573 573 except OSError, err:
574 574 if err.errno != errno.EEXIST:
575 575 raise
576 576 try:
577 577 # backup continues
578 578 for f in tobackup:
579 579 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
580 580 dir=backupdir)
581 581 os.close(fd)
582 582 ui.debug('backup %r as %r\n' % (f, tmpname))
583 583 util.copyfile(repo.wjoin(f), tmpname)
584 584 shutil.copystat(repo.wjoin(f), tmpname)
585 585 backups[f] = tmpname
586 586
587 587 fp = cStringIO.StringIO()
588 588 for c in chunks:
589 589 fname = c.filename()
590 590 if fname in backups or fname in newandmodifiedfiles:
591 591 c.write(fp)
592 592 dopatch = fp.tell()
593 593 fp.seek(0)
594 594
595 595 [os.unlink(c) for c in newandmodifiedfiles]
596 596
597 597 # 3a. apply filtered patch to clean repo (clean)
598 598 if backups:
599 599 hg.revert(repo, repo.dirstate.p1(),
600 600 lambda key: key in backups)
601 601
602 602 # 3b. (apply)
603 603 if dopatch:
604 604 try:
605 605 ui.debug('applying patch\n')
606 606 ui.debug(fp.getvalue())
607 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
607 patch.internalpatch(ui, repo, fp, 1, '', eolmode=None)
608 608 except patch.PatchError, err:
609 609 raise util.Abort(str(err))
610 610 del fp
611 611
612 612 # 4. We prepared working directory according to filtered
613 613 # patch. Now is the time to delegate the job to
614 614 # commit/qrefresh or the like!
615 615
616 616 # Make all of the pathnames absolute.
617 617 newfiles = [repo.wjoin(nf) for nf in newfiles]
618 618 commitfunc(ui, repo, *newfiles, **opts)
619 619
620 620 return 0
621 621 finally:
622 622 # 5. finally restore backed-up files
623 623 try:
624 624 for realname, tmpname in backups.iteritems():
625 625 ui.debug('restoring %r to %r\n' % (tmpname, realname))
626 626 util.copyfile(tmpname, repo.wjoin(realname))
627 627 # Our calls to copystat() here and above are a
628 628 # hack to trick any editors that have f open that
629 629 # we haven't modified them.
630 630 #
631 631 # Also note that this racy as an editor could
632 632 # notice the file's mtime before we've finished
633 633 # writing it.
634 634 shutil.copystat(tmpname, repo.wjoin(realname))
635 635 os.unlink(tmpname)
636 636 if tobackup:
637 637 os.rmdir(backupdir)
638 638 except OSError:
639 639 pass
640 640
641 641 # wrap ui.write so diff output can be labeled/colorized
642 642 def wrapwrite(orig, *args, **kw):
643 643 label = kw.pop('label', '')
644 644 for chunk, l in patch.difflabel(lambda: args):
645 645 orig(chunk, label=label + l)
646 646 oldwrite = ui.write
647 647 extensions.wrapfunction(ui, 'write', wrapwrite)
648 648 try:
649 649 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
650 650 finally:
651 651 ui.write = oldwrite
652 652
653 653 def uisetup(ui):
654 654 try:
655 655 mq = extensions.find('mq')
656 656 except KeyError:
657 657 return
658 658
659 659 cmdtable["qrecord"] = \
660 660 (qrecord,
661 661 # same options as qnew, but copy them so we don't get
662 662 # -i/--interactive for qrecord and add white space diff options
663 663 mq.cmdtable['^qnew'][1][:] + commands.diffwsopts,
664 664 _('hg qrecord [OPTION]... PATCH [FILE]...'))
665 665
666 666 _wrapcmd('qnew', mq.cmdtable, qnew, _("interactively record a new patch"))
667 667 _wrapcmd('qrefresh', mq.cmdtable, qrefresh,
668 668 _("interactively select changes to refresh"))
669 669
670 670 def _wrapcmd(cmd, table, wrapfn, msg):
671 671 entry = extensions.wrapcommand(table, cmd, wrapfn)
672 672 entry[1].append(('i', 'interactive', None, msg))
@@ -1,1990 +1,1990 b''
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, os, errno, re, posixpath
10 10 import tempfile, zlib, shutil
11 11 # On python2.4 you have to import these by name or they fail to
12 12 # load. This was not a problem on Python 2.7.
13 13 import email.Generator
14 14 import email.Parser
15 15
16 16 from i18n import _
17 17 from node import hex, short
18 18 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
19 19
20 20 gitre = re.compile('diff --git a/(.*) b/(.*)')
21 21 tabsplitter = re.compile(r'(\t+|[^\t]+)')
22 22
23 23 class PatchError(Exception):
24 24 pass
25 25
26 26
27 27 # public functions
28 28
29 29 def split(stream):
30 30 '''return an iterator of individual patches from a stream'''
31 31 def isheader(line, inheader):
32 32 if inheader and line[0] in (' ', '\t'):
33 33 # continuation
34 34 return True
35 35 if line[0] in (' ', '-', '+'):
36 36 # diff line - don't check for header pattern in there
37 37 return False
38 38 l = line.split(': ', 1)
39 39 return len(l) == 2 and ' ' not in l[0]
40 40
41 41 def chunk(lines):
42 42 return cStringIO.StringIO(''.join(lines))
43 43
44 44 def hgsplit(stream, cur):
45 45 inheader = True
46 46
47 47 for line in stream:
48 48 if not line.strip():
49 49 inheader = False
50 50 if not inheader and line.startswith('# HG changeset patch'):
51 51 yield chunk(cur)
52 52 cur = []
53 53 inheader = True
54 54
55 55 cur.append(line)
56 56
57 57 if cur:
58 58 yield chunk(cur)
59 59
60 60 def mboxsplit(stream, cur):
61 61 for line in stream:
62 62 if line.startswith('From '):
63 63 for c in split(chunk(cur[1:])):
64 64 yield c
65 65 cur = []
66 66
67 67 cur.append(line)
68 68
69 69 if cur:
70 70 for c in split(chunk(cur[1:])):
71 71 yield c
72 72
73 73 def mimesplit(stream, cur):
74 74 def msgfp(m):
75 75 fp = cStringIO.StringIO()
76 76 g = email.Generator.Generator(fp, mangle_from_=False)
77 77 g.flatten(m)
78 78 fp.seek(0)
79 79 return fp
80 80
81 81 for line in stream:
82 82 cur.append(line)
83 83 c = chunk(cur)
84 84
85 85 m = email.Parser.Parser().parse(c)
86 86 if not m.is_multipart():
87 87 yield msgfp(m)
88 88 else:
89 89 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
90 90 for part in m.walk():
91 91 ct = part.get_content_type()
92 92 if ct not in ok_types:
93 93 continue
94 94 yield msgfp(part)
95 95
96 96 def headersplit(stream, cur):
97 97 inheader = False
98 98
99 99 for line in stream:
100 100 if not inheader and isheader(line, inheader):
101 101 yield chunk(cur)
102 102 cur = []
103 103 inheader = True
104 104 if inheader and not isheader(line, inheader):
105 105 inheader = False
106 106
107 107 cur.append(line)
108 108
109 109 if cur:
110 110 yield chunk(cur)
111 111
112 112 def remainder(cur):
113 113 yield chunk(cur)
114 114
115 115 class fiter(object):
116 116 def __init__(self, fp):
117 117 self.fp = fp
118 118
119 119 def __iter__(self):
120 120 return self
121 121
122 122 def next(self):
123 123 l = self.fp.readline()
124 124 if not l:
125 125 raise StopIteration
126 126 return l
127 127
128 128 inheader = False
129 129 cur = []
130 130
131 131 mimeheaders = ['content-type']
132 132
133 133 if not util.safehasattr(stream, 'next'):
134 134 # http responses, for example, have readline but not next
135 135 stream = fiter(stream)
136 136
137 137 for line in stream:
138 138 cur.append(line)
139 139 if line.startswith('# HG changeset patch'):
140 140 return hgsplit(stream, cur)
141 141 elif line.startswith('From '):
142 142 return mboxsplit(stream, cur)
143 143 elif isheader(line, inheader):
144 144 inheader = True
145 145 if line.split(':', 1)[0].lower() in mimeheaders:
146 146 # let email parser handle this
147 147 return mimesplit(stream, cur)
148 148 elif line.startswith('--- ') and inheader:
149 149 # No evil headers seen by diff start, split by hand
150 150 return headersplit(stream, cur)
151 151 # Not enough info, keep reading
152 152
153 153 # if we are here, we have a very plain patch
154 154 return remainder(cur)
155 155
156 156 def extract(ui, fileobj):
157 157 '''extract patch from data read from fileobj.
158 158
159 159 patch can be a normal patch or contained in an email message.
160 160
161 161 return tuple (filename, message, user, date, branch, node, p1, p2).
162 162 Any item in the returned tuple can be None. If filename is None,
163 163 fileobj did not contain a patch. Caller must unlink filename when done.'''
164 164
165 165 # attempt to detect the start of a patch
166 166 # (this heuristic is borrowed from quilt)
167 167 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
168 168 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
169 169 r'---[ \t].*?^\+\+\+[ \t]|'
170 170 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
171 171
172 172 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
173 173 tmpfp = os.fdopen(fd, 'w')
174 174 try:
175 175 msg = email.Parser.Parser().parse(fileobj)
176 176
177 177 subject = msg['Subject']
178 178 user = msg['From']
179 179 if not subject and not user:
180 180 # Not an email, restore parsed headers if any
181 181 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
182 182
183 183 # should try to parse msg['Date']
184 184 date = None
185 185 nodeid = None
186 186 branch = None
187 187 parents = []
188 188
189 189 if subject:
190 190 if subject.startswith('[PATCH'):
191 191 pend = subject.find(']')
192 192 if pend >= 0:
193 193 subject = subject[pend + 1:].lstrip()
194 194 subject = re.sub(r'\n[ \t]+', ' ', subject)
195 195 ui.debug('Subject: %s\n' % subject)
196 196 if user:
197 197 ui.debug('From: %s\n' % user)
198 198 diffs_seen = 0
199 199 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
200 200 message = ''
201 201 for part in msg.walk():
202 202 content_type = part.get_content_type()
203 203 ui.debug('Content-Type: %s\n' % content_type)
204 204 if content_type not in ok_types:
205 205 continue
206 206 payload = part.get_payload(decode=True)
207 207 m = diffre.search(payload)
208 208 if m:
209 209 hgpatch = False
210 210 hgpatchheader = False
211 211 ignoretext = False
212 212
213 213 ui.debug('found patch at byte %d\n' % m.start(0))
214 214 diffs_seen += 1
215 215 cfp = cStringIO.StringIO()
216 216 for line in payload[:m.start(0)].splitlines():
217 217 if line.startswith('# HG changeset patch') and not hgpatch:
218 218 ui.debug('patch generated by hg export\n')
219 219 hgpatch = True
220 220 hgpatchheader = True
221 221 # drop earlier commit message content
222 222 cfp.seek(0)
223 223 cfp.truncate()
224 224 subject = None
225 225 elif hgpatchheader:
226 226 if line.startswith('# User '):
227 227 user = line[7:]
228 228 ui.debug('From: %s\n' % user)
229 229 elif line.startswith("# Date "):
230 230 date = line[7:]
231 231 elif line.startswith("# Branch "):
232 232 branch = line[9:]
233 233 elif line.startswith("# Node ID "):
234 234 nodeid = line[10:]
235 235 elif line.startswith("# Parent "):
236 236 parents.append(line[9:].lstrip())
237 237 elif not line.startswith("# "):
238 238 hgpatchheader = False
239 239 elif line == '---':
240 240 ignoretext = True
241 241 if not hgpatchheader and not ignoretext:
242 242 cfp.write(line)
243 243 cfp.write('\n')
244 244 message = cfp.getvalue()
245 245 if tmpfp:
246 246 tmpfp.write(payload)
247 247 if not payload.endswith('\n'):
248 248 tmpfp.write('\n')
249 249 elif not diffs_seen and message and content_type == 'text/plain':
250 250 message += '\n' + payload
251 251 except: # re-raises
252 252 tmpfp.close()
253 253 os.unlink(tmpname)
254 254 raise
255 255
256 256 if subject and not message.startswith(subject):
257 257 message = '%s\n%s' % (subject, message)
258 258 tmpfp.close()
259 259 if not diffs_seen:
260 260 os.unlink(tmpname)
261 261 return None, message, user, date, branch, None, None, None
262 262 p1 = parents and parents.pop(0) or None
263 263 p2 = parents and parents.pop(0) or None
264 264 return tmpname, message, user, date, branch, nodeid, p1, p2
265 265
266 266 class patchmeta(object):
267 267 """Patched file metadata
268 268
269 269 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
270 270 or COPY. 'path' is patched file path. 'oldpath' is set to the
271 271 origin file when 'op' is either COPY or RENAME, None otherwise. If
272 272 file mode is changed, 'mode' is a tuple (islink, isexec) where
273 273 'islink' is True if the file is a symlink and 'isexec' is True if
274 274 the file is executable. Otherwise, 'mode' is None.
275 275 """
276 276 def __init__(self, path):
277 277 self.path = path
278 278 self.oldpath = None
279 279 self.mode = None
280 280 self.op = 'MODIFY'
281 281 self.binary = False
282 282
283 283 def setmode(self, mode):
284 284 islink = mode & 020000
285 285 isexec = mode & 0100
286 286 self.mode = (islink, isexec)
287 287
288 288 def copy(self):
289 289 other = patchmeta(self.path)
290 290 other.oldpath = self.oldpath
291 291 other.mode = self.mode
292 292 other.op = self.op
293 293 other.binary = self.binary
294 294 return other
295 295
296 296 def _ispatchinga(self, afile):
297 297 if afile == '/dev/null':
298 298 return self.op == 'ADD'
299 299 return afile == 'a/' + (self.oldpath or self.path)
300 300
301 301 def _ispatchingb(self, bfile):
302 302 if bfile == '/dev/null':
303 303 return self.op == 'DELETE'
304 304 return bfile == 'b/' + self.path
305 305
306 306 def ispatching(self, afile, bfile):
307 307 return self._ispatchinga(afile) and self._ispatchingb(bfile)
308 308
309 309 def __repr__(self):
310 310 return "<patchmeta %s %r>" % (self.op, self.path)
311 311
312 312 def readgitpatch(lr):
313 313 """extract git-style metadata about patches from <patchname>"""
314 314
315 315 # Filter patch for git information
316 316 gp = None
317 317 gitpatches = []
318 318 for line in lr:
319 319 line = line.rstrip(' \r\n')
320 320 if line.startswith('diff --git a/'):
321 321 m = gitre.match(line)
322 322 if m:
323 323 if gp:
324 324 gitpatches.append(gp)
325 325 dst = m.group(2)
326 326 gp = patchmeta(dst)
327 327 elif gp:
328 328 if line.startswith('--- '):
329 329 gitpatches.append(gp)
330 330 gp = None
331 331 continue
332 332 if line.startswith('rename from '):
333 333 gp.op = 'RENAME'
334 334 gp.oldpath = line[12:]
335 335 elif line.startswith('rename to '):
336 336 gp.path = line[10:]
337 337 elif line.startswith('copy from '):
338 338 gp.op = 'COPY'
339 339 gp.oldpath = line[10:]
340 340 elif line.startswith('copy to '):
341 341 gp.path = line[8:]
342 342 elif line.startswith('deleted file'):
343 343 gp.op = 'DELETE'
344 344 elif line.startswith('new file mode '):
345 345 gp.op = 'ADD'
346 346 gp.setmode(int(line[-6:], 8))
347 347 elif line.startswith('new mode '):
348 348 gp.setmode(int(line[-6:], 8))
349 349 elif line.startswith('GIT binary patch'):
350 350 gp.binary = True
351 351 if gp:
352 352 gitpatches.append(gp)
353 353
354 354 return gitpatches
355 355
356 356 class linereader(object):
357 357 # simple class to allow pushing lines back into the input stream
358 358 def __init__(self, fp):
359 359 self.fp = fp
360 360 self.buf = []
361 361
362 362 def push(self, line):
363 363 if line is not None:
364 364 self.buf.append(line)
365 365
366 366 def readline(self):
367 367 if self.buf:
368 368 l = self.buf[0]
369 369 del self.buf[0]
370 370 return l
371 371 return self.fp.readline()
372 372
373 373 def __iter__(self):
374 374 while True:
375 375 l = self.readline()
376 376 if not l:
377 377 break
378 378 yield l
379 379
380 380 class abstractbackend(object):
381 381 def __init__(self, ui):
382 382 self.ui = ui
383 383
384 384 def getfile(self, fname):
385 385 """Return target file data and flags as a (data, (islink,
386 386 isexec)) tuple. Data is None if file is missing/deleted.
387 387 """
388 388 raise NotImplementedError
389 389
390 390 def setfile(self, fname, data, mode, copysource):
391 391 """Write data to target file fname and set its mode. mode is a
392 392 (islink, isexec) tuple. If data is None, the file content should
393 393 be left unchanged. If the file is modified after being copied,
394 394 copysource is set to the original file name.
395 395 """
396 396 raise NotImplementedError
397 397
398 398 def unlink(self, fname):
399 399 """Unlink target file."""
400 400 raise NotImplementedError
401 401
402 402 def writerej(self, fname, failed, total, lines):
403 403 """Write rejected lines for fname. total is the number of hunks
404 404 which failed to apply and total the total number of hunks for this
405 405 files.
406 406 """
407 407 pass
408 408
409 409 def exists(self, fname):
410 410 raise NotImplementedError
411 411
412 412 class fsbackend(abstractbackend):
413 413 def __init__(self, ui, basedir):
414 414 super(fsbackend, self).__init__(ui)
415 415 self.opener = scmutil.opener(basedir)
416 416
417 417 def _join(self, f):
418 418 return os.path.join(self.opener.base, f)
419 419
420 420 def getfile(self, fname):
421 421 if self.opener.islink(fname):
422 422 return (self.opener.readlink(fname), (True, False))
423 423
424 424 isexec = False
425 425 try:
426 426 isexec = self.opener.lstat(fname).st_mode & 0100 != 0
427 427 except OSError, e:
428 428 if e.errno != errno.ENOENT:
429 429 raise
430 430 try:
431 431 return (self.opener.read(fname), (False, isexec))
432 432 except IOError, e:
433 433 if e.errno != errno.ENOENT:
434 434 raise
435 435 return None, None
436 436
437 437 def setfile(self, fname, data, mode, copysource):
438 438 islink, isexec = mode
439 439 if data is None:
440 440 self.opener.setflags(fname, islink, isexec)
441 441 return
442 442 if islink:
443 443 self.opener.symlink(data, fname)
444 444 else:
445 445 self.opener.write(fname, data)
446 446 if isexec:
447 447 self.opener.setflags(fname, False, True)
448 448
449 449 def unlink(self, fname):
450 450 self.opener.unlinkpath(fname, ignoremissing=True)
451 451
452 452 def writerej(self, fname, failed, total, lines):
453 453 fname = fname + ".rej"
454 454 self.ui.warn(
455 455 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
456 456 (failed, total, fname))
457 457 fp = self.opener(fname, 'w')
458 458 fp.writelines(lines)
459 459 fp.close()
460 460
461 461 def exists(self, fname):
462 462 return self.opener.lexists(fname)
463 463
464 464 class workingbackend(fsbackend):
465 465 def __init__(self, ui, repo, similarity):
466 466 super(workingbackend, self).__init__(ui, repo.root)
467 467 self.repo = repo
468 468 self.similarity = similarity
469 469 self.removed = set()
470 470 self.changed = set()
471 471 self.copied = []
472 472
473 473 def _checkknown(self, fname):
474 474 if self.repo.dirstate[fname] == '?' and self.exists(fname):
475 475 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
476 476
477 477 def setfile(self, fname, data, mode, copysource):
478 478 self._checkknown(fname)
479 479 super(workingbackend, self).setfile(fname, data, mode, copysource)
480 480 if copysource is not None:
481 481 self.copied.append((copysource, fname))
482 482 self.changed.add(fname)
483 483
484 484 def unlink(self, fname):
485 485 self._checkknown(fname)
486 486 super(workingbackend, self).unlink(fname)
487 487 self.removed.add(fname)
488 488 self.changed.add(fname)
489 489
490 490 def close(self):
491 491 wctx = self.repo[None]
492 492 changed = set(self.changed)
493 493 for src, dst in self.copied:
494 494 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
495 495 if self.removed:
496 496 wctx.forget(sorted(self.removed))
497 497 for f in self.removed:
498 498 if f not in self.repo.dirstate:
499 499 # File was deleted and no longer belongs to the
500 500 # dirstate, it was probably marked added then
501 501 # deleted, and should not be considered by
502 502 # marktouched().
503 503 changed.discard(f)
504 504 if changed:
505 505 scmutil.marktouched(self.repo, changed, self.similarity)
506 506 return sorted(self.changed)
507 507
508 508 class filestore(object):
509 509 def __init__(self, maxsize=None):
510 510 self.opener = None
511 511 self.files = {}
512 512 self.created = 0
513 513 self.maxsize = maxsize
514 514 if self.maxsize is None:
515 515 self.maxsize = 4*(2**20)
516 516 self.size = 0
517 517 self.data = {}
518 518
519 519 def setfile(self, fname, data, mode, copied=None):
520 520 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
521 521 self.data[fname] = (data, mode, copied)
522 522 self.size += len(data)
523 523 else:
524 524 if self.opener is None:
525 525 root = tempfile.mkdtemp(prefix='hg-patch-')
526 526 self.opener = scmutil.opener(root)
527 527 # Avoid filename issues with these simple names
528 528 fn = str(self.created)
529 529 self.opener.write(fn, data)
530 530 self.created += 1
531 531 self.files[fname] = (fn, mode, copied)
532 532
533 533 def getfile(self, fname):
534 534 if fname in self.data:
535 535 return self.data[fname]
536 536 if not self.opener or fname not in self.files:
537 537 return None, None, None
538 538 fn, mode, copied = self.files[fname]
539 539 return self.opener.read(fn), mode, copied
540 540
541 541 def close(self):
542 542 if self.opener:
543 543 shutil.rmtree(self.opener.base)
544 544
545 545 class repobackend(abstractbackend):
546 546 def __init__(self, ui, repo, ctx, store):
547 547 super(repobackend, self).__init__(ui)
548 548 self.repo = repo
549 549 self.ctx = ctx
550 550 self.store = store
551 551 self.changed = set()
552 552 self.removed = set()
553 553 self.copied = {}
554 554
555 555 def _checkknown(self, fname):
556 556 if fname not in self.ctx:
557 557 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
558 558
559 559 def getfile(self, fname):
560 560 try:
561 561 fctx = self.ctx[fname]
562 562 except error.LookupError:
563 563 return None, None
564 564 flags = fctx.flags()
565 565 return fctx.data(), ('l' in flags, 'x' in flags)
566 566
567 567 def setfile(self, fname, data, mode, copysource):
568 568 if copysource:
569 569 self._checkknown(copysource)
570 570 if data is None:
571 571 data = self.ctx[fname].data()
572 572 self.store.setfile(fname, data, mode, copysource)
573 573 self.changed.add(fname)
574 574 if copysource:
575 575 self.copied[fname] = copysource
576 576
577 577 def unlink(self, fname):
578 578 self._checkknown(fname)
579 579 self.removed.add(fname)
580 580
581 581 def exists(self, fname):
582 582 return fname in self.ctx
583 583
584 584 def close(self):
585 585 return self.changed | self.removed
586 586
587 587 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
588 588 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
589 589 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
590 590 eolmodes = ['strict', 'crlf', 'lf', 'auto']
591 591
592 592 class patchfile(object):
593 593 def __init__(self, ui, gp, backend, store, eolmode='strict'):
594 594 self.fname = gp.path
595 595 self.eolmode = eolmode
596 596 self.eol = None
597 597 self.backend = backend
598 598 self.ui = ui
599 599 self.lines = []
600 600 self.exists = False
601 601 self.missing = True
602 602 self.mode = gp.mode
603 603 self.copysource = gp.oldpath
604 604 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
605 605 self.remove = gp.op == 'DELETE'
606 606 if self.copysource is None:
607 607 data, mode = backend.getfile(self.fname)
608 608 else:
609 609 data, mode = store.getfile(self.copysource)[:2]
610 610 if data is not None:
611 611 self.exists = self.copysource is None or backend.exists(self.fname)
612 612 self.missing = False
613 613 if data:
614 614 self.lines = mdiff.splitnewlines(data)
615 615 if self.mode is None:
616 616 self.mode = mode
617 617 if self.lines:
618 618 # Normalize line endings
619 619 if self.lines[0].endswith('\r\n'):
620 620 self.eol = '\r\n'
621 621 elif self.lines[0].endswith('\n'):
622 622 self.eol = '\n'
623 623 if eolmode != 'strict':
624 624 nlines = []
625 625 for l in self.lines:
626 626 if l.endswith('\r\n'):
627 627 l = l[:-2] + '\n'
628 628 nlines.append(l)
629 629 self.lines = nlines
630 630 else:
631 631 if self.create:
632 632 self.missing = False
633 633 if self.mode is None:
634 634 self.mode = (False, False)
635 635 if self.missing:
636 636 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
637 637
638 638 self.hash = {}
639 639 self.dirty = 0
640 640 self.offset = 0
641 641 self.skew = 0
642 642 self.rej = []
643 643 self.fileprinted = False
644 644 self.printfile(False)
645 645 self.hunks = 0
646 646
647 647 def writelines(self, fname, lines, mode):
648 648 if self.eolmode == 'auto':
649 649 eol = self.eol
650 650 elif self.eolmode == 'crlf':
651 651 eol = '\r\n'
652 652 else:
653 653 eol = '\n'
654 654
655 655 if self.eolmode != 'strict' and eol and eol != '\n':
656 656 rawlines = []
657 657 for l in lines:
658 658 if l and l[-1] == '\n':
659 659 l = l[:-1] + eol
660 660 rawlines.append(l)
661 661 lines = rawlines
662 662
663 663 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
664 664
665 665 def printfile(self, warn):
666 666 if self.fileprinted:
667 667 return
668 668 if warn or self.ui.verbose:
669 669 self.fileprinted = True
670 670 s = _("patching file %s\n") % self.fname
671 671 if warn:
672 672 self.ui.warn(s)
673 673 else:
674 674 self.ui.note(s)
675 675
676 676
677 677 def findlines(self, l, linenum):
678 678 # looks through the hash and finds candidate lines. The
679 679 # result is a list of line numbers sorted based on distance
680 680 # from linenum
681 681
682 682 cand = self.hash.get(l, [])
683 683 if len(cand) > 1:
684 684 # resort our list of potentials forward then back.
685 685 cand.sort(key=lambda x: abs(x - linenum))
686 686 return cand
687 687
688 688 def write_rej(self):
689 689 # our rejects are a little different from patch(1). This always
690 690 # creates rejects in the same form as the original patch. A file
691 691 # header is inserted so that you can run the reject through patch again
692 692 # without having to type the filename.
693 693 if not self.rej:
694 694 return
695 695 base = os.path.basename(self.fname)
696 696 lines = ["--- %s\n+++ %s\n" % (base, base)]
697 697 for x in self.rej:
698 698 for l in x.hunk:
699 699 lines.append(l)
700 700 if l[-1] != '\n':
701 701 lines.append("\n\ No newline at end of file\n")
702 702 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
703 703
704 704 def apply(self, h):
705 705 if not h.complete():
706 706 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
707 707 (h.number, h.desc, len(h.a), h.lena, len(h.b),
708 708 h.lenb))
709 709
710 710 self.hunks += 1
711 711
712 712 if self.missing:
713 713 self.rej.append(h)
714 714 return -1
715 715
716 716 if self.exists and self.create:
717 717 if self.copysource:
718 718 self.ui.warn(_("cannot create %s: destination already "
719 719 "exists\n") % self.fname)
720 720 else:
721 721 self.ui.warn(_("file %s already exists\n") % self.fname)
722 722 self.rej.append(h)
723 723 return -1
724 724
725 725 if isinstance(h, binhunk):
726 726 if self.remove:
727 727 self.backend.unlink(self.fname)
728 728 else:
729 729 l = h.new(self.lines)
730 730 self.lines[:] = l
731 731 self.offset += len(l)
732 732 self.dirty = True
733 733 return 0
734 734
735 735 horig = h
736 736 if (self.eolmode in ('crlf', 'lf')
737 737 or self.eolmode == 'auto' and self.eol):
738 738 # If new eols are going to be normalized, then normalize
739 739 # hunk data before patching. Otherwise, preserve input
740 740 # line-endings.
741 741 h = h.getnormalized()
742 742
743 743 # fast case first, no offsets, no fuzz
744 744 old, oldstart, new, newstart = h.fuzzit(0, False)
745 745 oldstart += self.offset
746 746 orig_start = oldstart
747 747 # if there's skew we want to emit the "(offset %d lines)" even
748 748 # when the hunk cleanly applies at start + skew, so skip the
749 749 # fast case code
750 750 if (self.skew == 0 and
751 751 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
752 752 if self.remove:
753 753 self.backend.unlink(self.fname)
754 754 else:
755 755 self.lines[oldstart:oldstart + len(old)] = new
756 756 self.offset += len(new) - len(old)
757 757 self.dirty = True
758 758 return 0
759 759
760 760 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
761 761 self.hash = {}
762 762 for x, s in enumerate(self.lines):
763 763 self.hash.setdefault(s, []).append(x)
764 764
765 765 for fuzzlen in xrange(3):
766 766 for toponly in [True, False]:
767 767 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
768 768 oldstart = oldstart + self.offset + self.skew
769 769 oldstart = min(oldstart, len(self.lines))
770 770 if old:
771 771 cand = self.findlines(old[0][1:], oldstart)
772 772 else:
773 773 # Only adding lines with no or fuzzed context, just
774 774 # take the skew in account
775 775 cand = [oldstart]
776 776
777 777 for l in cand:
778 778 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
779 779 self.lines[l : l + len(old)] = new
780 780 self.offset += len(new) - len(old)
781 781 self.skew = l - orig_start
782 782 self.dirty = True
783 783 offset = l - orig_start - fuzzlen
784 784 if fuzzlen:
785 785 msg = _("Hunk #%d succeeded at %d "
786 786 "with fuzz %d "
787 787 "(offset %d lines).\n")
788 788 self.printfile(True)
789 789 self.ui.warn(msg %
790 790 (h.number, l + 1, fuzzlen, offset))
791 791 else:
792 792 msg = _("Hunk #%d succeeded at %d "
793 793 "(offset %d lines).\n")
794 794 self.ui.note(msg % (h.number, l + 1, offset))
795 795 return fuzzlen
796 796 self.printfile(True)
797 797 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
798 798 self.rej.append(horig)
799 799 return -1
800 800
801 801 def close(self):
802 802 if self.dirty:
803 803 self.writelines(self.fname, self.lines, self.mode)
804 804 self.write_rej()
805 805 return len(self.rej)
806 806
807 807 class hunk(object):
808 808 def __init__(self, desc, num, lr, context):
809 809 self.number = num
810 810 self.desc = desc
811 811 self.hunk = [desc]
812 812 self.a = []
813 813 self.b = []
814 814 self.starta = self.lena = None
815 815 self.startb = self.lenb = None
816 816 if lr is not None:
817 817 if context:
818 818 self.read_context_hunk(lr)
819 819 else:
820 820 self.read_unified_hunk(lr)
821 821
822 822 def getnormalized(self):
823 823 """Return a copy with line endings normalized to LF."""
824 824
825 825 def normalize(lines):
826 826 nlines = []
827 827 for line in lines:
828 828 if line.endswith('\r\n'):
829 829 line = line[:-2] + '\n'
830 830 nlines.append(line)
831 831 return nlines
832 832
833 833 # Dummy object, it is rebuilt manually
834 834 nh = hunk(self.desc, self.number, None, None)
835 835 nh.number = self.number
836 836 nh.desc = self.desc
837 837 nh.hunk = self.hunk
838 838 nh.a = normalize(self.a)
839 839 nh.b = normalize(self.b)
840 840 nh.starta = self.starta
841 841 nh.startb = self.startb
842 842 nh.lena = self.lena
843 843 nh.lenb = self.lenb
844 844 return nh
845 845
846 846 def read_unified_hunk(self, lr):
847 847 m = unidesc.match(self.desc)
848 848 if not m:
849 849 raise PatchError(_("bad hunk #%d") % self.number)
850 850 self.starta, self.lena, self.startb, self.lenb = m.groups()
851 851 if self.lena is None:
852 852 self.lena = 1
853 853 else:
854 854 self.lena = int(self.lena)
855 855 if self.lenb is None:
856 856 self.lenb = 1
857 857 else:
858 858 self.lenb = int(self.lenb)
859 859 self.starta = int(self.starta)
860 860 self.startb = int(self.startb)
861 861 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
862 862 self.b)
863 863 # if we hit eof before finishing out the hunk, the last line will
864 864 # be zero length. Lets try to fix it up.
865 865 while len(self.hunk[-1]) == 0:
866 866 del self.hunk[-1]
867 867 del self.a[-1]
868 868 del self.b[-1]
869 869 self.lena -= 1
870 870 self.lenb -= 1
871 871 self._fixnewline(lr)
872 872
873 873 def read_context_hunk(self, lr):
874 874 self.desc = lr.readline()
875 875 m = contextdesc.match(self.desc)
876 876 if not m:
877 877 raise PatchError(_("bad hunk #%d") % self.number)
878 878 self.starta, aend = m.groups()
879 879 self.starta = int(self.starta)
880 880 if aend is None:
881 881 aend = self.starta
882 882 self.lena = int(aend) - self.starta
883 883 if self.starta:
884 884 self.lena += 1
885 885 for x in xrange(self.lena):
886 886 l = lr.readline()
887 887 if l.startswith('---'):
888 888 # lines addition, old block is empty
889 889 lr.push(l)
890 890 break
891 891 s = l[2:]
892 892 if l.startswith('- ') or l.startswith('! '):
893 893 u = '-' + s
894 894 elif l.startswith(' '):
895 895 u = ' ' + s
896 896 else:
897 897 raise PatchError(_("bad hunk #%d old text line %d") %
898 898 (self.number, x))
899 899 self.a.append(u)
900 900 self.hunk.append(u)
901 901
902 902 l = lr.readline()
903 903 if l.startswith('\ '):
904 904 s = self.a[-1][:-1]
905 905 self.a[-1] = s
906 906 self.hunk[-1] = s
907 907 l = lr.readline()
908 908 m = contextdesc.match(l)
909 909 if not m:
910 910 raise PatchError(_("bad hunk #%d") % self.number)
911 911 self.startb, bend = m.groups()
912 912 self.startb = int(self.startb)
913 913 if bend is None:
914 914 bend = self.startb
915 915 self.lenb = int(bend) - self.startb
916 916 if self.startb:
917 917 self.lenb += 1
918 918 hunki = 1
919 919 for x in xrange(self.lenb):
920 920 l = lr.readline()
921 921 if l.startswith('\ '):
922 922 # XXX: the only way to hit this is with an invalid line range.
923 923 # The no-eol marker is not counted in the line range, but I
924 924 # guess there are diff(1) out there which behave differently.
925 925 s = self.b[-1][:-1]
926 926 self.b[-1] = s
927 927 self.hunk[hunki - 1] = s
928 928 continue
929 929 if not l:
930 930 # line deletions, new block is empty and we hit EOF
931 931 lr.push(l)
932 932 break
933 933 s = l[2:]
934 934 if l.startswith('+ ') or l.startswith('! '):
935 935 u = '+' + s
936 936 elif l.startswith(' '):
937 937 u = ' ' + s
938 938 elif len(self.b) == 0:
939 939 # line deletions, new block is empty
940 940 lr.push(l)
941 941 break
942 942 else:
943 943 raise PatchError(_("bad hunk #%d old text line %d") %
944 944 (self.number, x))
945 945 self.b.append(s)
946 946 while True:
947 947 if hunki >= len(self.hunk):
948 948 h = ""
949 949 else:
950 950 h = self.hunk[hunki]
951 951 hunki += 1
952 952 if h == u:
953 953 break
954 954 elif h.startswith('-'):
955 955 continue
956 956 else:
957 957 self.hunk.insert(hunki - 1, u)
958 958 break
959 959
960 960 if not self.a:
961 961 # this happens when lines were only added to the hunk
962 962 for x in self.hunk:
963 963 if x.startswith('-') or x.startswith(' '):
964 964 self.a.append(x)
965 965 if not self.b:
966 966 # this happens when lines were only deleted from the hunk
967 967 for x in self.hunk:
968 968 if x.startswith('+') or x.startswith(' '):
969 969 self.b.append(x[1:])
970 970 # @@ -start,len +start,len @@
971 971 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
972 972 self.startb, self.lenb)
973 973 self.hunk[0] = self.desc
974 974 self._fixnewline(lr)
975 975
976 976 def _fixnewline(self, lr):
977 977 l = lr.readline()
978 978 if l.startswith('\ '):
979 979 diffhelpers.fix_newline(self.hunk, self.a, self.b)
980 980 else:
981 981 lr.push(l)
982 982
983 983 def complete(self):
984 984 return len(self.a) == self.lena and len(self.b) == self.lenb
985 985
986 986 def _fuzzit(self, old, new, fuzz, toponly):
987 987 # this removes context lines from the top and bottom of list 'l'. It
988 988 # checks the hunk to make sure only context lines are removed, and then
989 989 # returns a new shortened list of lines.
990 990 fuzz = min(fuzz, len(old))
991 991 if fuzz:
992 992 top = 0
993 993 bot = 0
994 994 hlen = len(self.hunk)
995 995 for x in xrange(hlen - 1):
996 996 # the hunk starts with the @@ line, so use x+1
997 997 if self.hunk[x + 1][0] == ' ':
998 998 top += 1
999 999 else:
1000 1000 break
1001 1001 if not toponly:
1002 1002 for x in xrange(hlen - 1):
1003 1003 if self.hunk[hlen - bot - 1][0] == ' ':
1004 1004 bot += 1
1005 1005 else:
1006 1006 break
1007 1007
1008 1008 bot = min(fuzz, bot)
1009 1009 top = min(fuzz, top)
1010 1010 return old[top:len(old) - bot], new[top:len(new) - bot], top
1011 1011 return old, new, 0
1012 1012
1013 1013 def fuzzit(self, fuzz, toponly):
1014 1014 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1015 1015 oldstart = self.starta + top
1016 1016 newstart = self.startb + top
1017 1017 # zero length hunk ranges already have their start decremented
1018 1018 if self.lena and oldstart > 0:
1019 1019 oldstart -= 1
1020 1020 if self.lenb and newstart > 0:
1021 1021 newstart -= 1
1022 1022 return old, oldstart, new, newstart
1023 1023
1024 1024 class binhunk(object):
1025 1025 'A binary patch file.'
1026 1026 def __init__(self, lr, fname):
1027 1027 self.text = None
1028 1028 self.delta = False
1029 1029 self.hunk = ['GIT binary patch\n']
1030 1030 self._fname = fname
1031 1031 self._read(lr)
1032 1032
1033 1033 def complete(self):
1034 1034 return self.text is not None
1035 1035
1036 1036 def new(self, lines):
1037 1037 if self.delta:
1038 1038 return [applybindelta(self.text, ''.join(lines))]
1039 1039 return [self.text]
1040 1040
1041 1041 def _read(self, lr):
1042 1042 def getline(lr, hunk):
1043 1043 l = lr.readline()
1044 1044 hunk.append(l)
1045 1045 return l.rstrip('\r\n')
1046 1046
1047 1047 size = 0
1048 1048 while True:
1049 1049 line = getline(lr, self.hunk)
1050 1050 if not line:
1051 1051 raise PatchError(_('could not extract "%s" binary data')
1052 1052 % self._fname)
1053 1053 if line.startswith('literal '):
1054 1054 size = int(line[8:].rstrip())
1055 1055 break
1056 1056 if line.startswith('delta '):
1057 1057 size = int(line[6:].rstrip())
1058 1058 self.delta = True
1059 1059 break
1060 1060 dec = []
1061 1061 line = getline(lr, self.hunk)
1062 1062 while len(line) > 1:
1063 1063 l = line[0]
1064 1064 if l <= 'Z' and l >= 'A':
1065 1065 l = ord(l) - ord('A') + 1
1066 1066 else:
1067 1067 l = ord(l) - ord('a') + 27
1068 1068 try:
1069 1069 dec.append(base85.b85decode(line[1:])[:l])
1070 1070 except ValueError, e:
1071 1071 raise PatchError(_('could not decode "%s" binary patch: %s')
1072 1072 % (self._fname, str(e)))
1073 1073 line = getline(lr, self.hunk)
1074 1074 text = zlib.decompress(''.join(dec))
1075 1075 if len(text) != size:
1076 1076 raise PatchError(_('"%s" length is %d bytes, should be %d')
1077 1077 % (self._fname, len(text), size))
1078 1078 self.text = text
1079 1079
1080 1080 def parsefilename(str):
1081 1081 # --- filename \t|space stuff
1082 1082 s = str[4:].rstrip('\r\n')
1083 1083 i = s.find('\t')
1084 1084 if i < 0:
1085 1085 i = s.find(' ')
1086 1086 if i < 0:
1087 1087 return s
1088 1088 return s[:i]
1089 1089
1090 1090 def pathtransform(path, strip, prefix):
1091 1091 '''turn a path from a patch into a path suitable for the repository
1092 1092
1093 1093 prefix, if not empty, is expected to be normalized with a / at the end.
1094 1094
1095 1095 Returns (stripped components, path in repository).
1096 1096
1097 1097 >>> pathtransform('a/b/c', 0, '')
1098 1098 ('', 'a/b/c')
1099 1099 >>> pathtransform(' a/b/c ', 0, '')
1100 1100 ('', ' a/b/c')
1101 1101 >>> pathtransform(' a/b/c ', 2, '')
1102 1102 ('a/b/', 'c')
1103 1103 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1104 1104 ('a//b/', 'd/e/c')
1105 1105 >>> pathtransform('a/b/c', 3, '')
1106 1106 Traceback (most recent call last):
1107 1107 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1108 1108 '''
1109 1109 pathlen = len(path)
1110 1110 i = 0
1111 1111 if strip == 0:
1112 1112 return '', path.rstrip()
1113 1113 count = strip
1114 1114 while count > 0:
1115 1115 i = path.find('/', i)
1116 1116 if i == -1:
1117 1117 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1118 1118 (count, strip, path))
1119 1119 i += 1
1120 1120 # consume '//' in the path
1121 1121 while i < pathlen - 1 and path[i] == '/':
1122 1122 i += 1
1123 1123 count -= 1
1124 1124 return path[:i].lstrip(), prefix + path[i:].rstrip()
1125 1125
1126 1126 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1127 1127 nulla = afile_orig == "/dev/null"
1128 1128 nullb = bfile_orig == "/dev/null"
1129 1129 create = nulla and hunk.starta == 0 and hunk.lena == 0
1130 1130 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1131 1131 abase, afile = pathtransform(afile_orig, strip, prefix)
1132 1132 gooda = not nulla and backend.exists(afile)
1133 1133 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1134 1134 if afile == bfile:
1135 1135 goodb = gooda
1136 1136 else:
1137 1137 goodb = not nullb and backend.exists(bfile)
1138 1138 missing = not goodb and not gooda and not create
1139 1139
1140 1140 # some diff programs apparently produce patches where the afile is
1141 1141 # not /dev/null, but afile starts with bfile
1142 1142 abasedir = afile[:afile.rfind('/') + 1]
1143 1143 bbasedir = bfile[:bfile.rfind('/') + 1]
1144 1144 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1145 1145 and hunk.starta == 0 and hunk.lena == 0):
1146 1146 create = True
1147 1147 missing = False
1148 1148
1149 1149 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1150 1150 # diff is between a file and its backup. In this case, the original
1151 1151 # file should be patched (see original mpatch code).
1152 1152 isbackup = (abase == bbase and bfile.startswith(afile))
1153 1153 fname = None
1154 1154 if not missing:
1155 1155 if gooda and goodb:
1156 1156 fname = isbackup and afile or bfile
1157 1157 elif gooda:
1158 1158 fname = afile
1159 1159
1160 1160 if not fname:
1161 1161 if not nullb:
1162 1162 fname = isbackup and afile or bfile
1163 1163 elif not nulla:
1164 1164 fname = afile
1165 1165 else:
1166 1166 raise PatchError(_("undefined source and destination files"))
1167 1167
1168 1168 gp = patchmeta(fname)
1169 1169 if create:
1170 1170 gp.op = 'ADD'
1171 1171 elif remove:
1172 1172 gp.op = 'DELETE'
1173 1173 return gp
1174 1174
1175 1175 def scangitpatch(lr, firstline):
1176 1176 """
1177 1177 Git patches can emit:
1178 1178 - rename a to b
1179 1179 - change b
1180 1180 - copy a to c
1181 1181 - change c
1182 1182
1183 1183 We cannot apply this sequence as-is, the renamed 'a' could not be
1184 1184 found for it would have been renamed already. And we cannot copy
1185 1185 from 'b' instead because 'b' would have been changed already. So
1186 1186 we scan the git patch for copy and rename commands so we can
1187 1187 perform the copies ahead of time.
1188 1188 """
1189 1189 pos = 0
1190 1190 try:
1191 1191 pos = lr.fp.tell()
1192 1192 fp = lr.fp
1193 1193 except IOError:
1194 1194 fp = cStringIO.StringIO(lr.fp.read())
1195 1195 gitlr = linereader(fp)
1196 1196 gitlr.push(firstline)
1197 1197 gitpatches = readgitpatch(gitlr)
1198 1198 fp.seek(pos)
1199 1199 return gitpatches
1200 1200
1201 1201 def iterhunks(fp):
1202 1202 """Read a patch and yield the following events:
1203 1203 - ("file", afile, bfile, firsthunk): select a new target file.
1204 1204 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1205 1205 "file" event.
1206 1206 - ("git", gitchanges): current diff is in git format, gitchanges
1207 1207 maps filenames to gitpatch records. Unique event.
1208 1208 """
1209 1209 afile = ""
1210 1210 bfile = ""
1211 1211 state = None
1212 1212 hunknum = 0
1213 1213 emitfile = newfile = False
1214 1214 gitpatches = None
1215 1215
1216 1216 # our states
1217 1217 BFILE = 1
1218 1218 context = None
1219 1219 lr = linereader(fp)
1220 1220
1221 1221 while True:
1222 1222 x = lr.readline()
1223 1223 if not x:
1224 1224 break
1225 1225 if state == BFILE and (
1226 1226 (not context and x[0] == '@')
1227 1227 or (context is not False and x.startswith('***************'))
1228 1228 or x.startswith('GIT binary patch')):
1229 1229 gp = None
1230 1230 if (gitpatches and
1231 1231 gitpatches[-1].ispatching(afile, bfile)):
1232 1232 gp = gitpatches.pop()
1233 1233 if x.startswith('GIT binary patch'):
1234 1234 h = binhunk(lr, gp.path)
1235 1235 else:
1236 1236 if context is None and x.startswith('***************'):
1237 1237 context = True
1238 1238 h = hunk(x, hunknum + 1, lr, context)
1239 1239 hunknum += 1
1240 1240 if emitfile:
1241 1241 emitfile = False
1242 1242 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1243 1243 yield 'hunk', h
1244 1244 elif x.startswith('diff --git a/'):
1245 1245 m = gitre.match(x.rstrip(' \r\n'))
1246 1246 if not m:
1247 1247 continue
1248 1248 if gitpatches is None:
1249 1249 # scan whole input for git metadata
1250 1250 gitpatches = scangitpatch(lr, x)
1251 1251 yield 'git', [g.copy() for g in gitpatches
1252 1252 if g.op in ('COPY', 'RENAME')]
1253 1253 gitpatches.reverse()
1254 1254 afile = 'a/' + m.group(1)
1255 1255 bfile = 'b/' + m.group(2)
1256 1256 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1257 1257 gp = gitpatches.pop()
1258 1258 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1259 1259 if not gitpatches:
1260 1260 raise PatchError(_('failed to synchronize metadata for "%s"')
1261 1261 % afile[2:])
1262 1262 gp = gitpatches[-1]
1263 1263 newfile = True
1264 1264 elif x.startswith('---'):
1265 1265 # check for a unified diff
1266 1266 l2 = lr.readline()
1267 1267 if not l2.startswith('+++'):
1268 1268 lr.push(l2)
1269 1269 continue
1270 1270 newfile = True
1271 1271 context = False
1272 1272 afile = parsefilename(x)
1273 1273 bfile = parsefilename(l2)
1274 1274 elif x.startswith('***'):
1275 1275 # check for a context diff
1276 1276 l2 = lr.readline()
1277 1277 if not l2.startswith('---'):
1278 1278 lr.push(l2)
1279 1279 continue
1280 1280 l3 = lr.readline()
1281 1281 lr.push(l3)
1282 1282 if not l3.startswith("***************"):
1283 1283 lr.push(l2)
1284 1284 continue
1285 1285 newfile = True
1286 1286 context = True
1287 1287 afile = parsefilename(x)
1288 1288 bfile = parsefilename(l2)
1289 1289
1290 1290 if newfile:
1291 1291 newfile = False
1292 1292 emitfile = True
1293 1293 state = BFILE
1294 1294 hunknum = 0
1295 1295
1296 1296 while gitpatches:
1297 1297 gp = gitpatches.pop()
1298 1298 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1299 1299
1300 1300 def applybindelta(binchunk, data):
1301 1301 """Apply a binary delta hunk
1302 1302 The algorithm used is the algorithm from git's patch-delta.c
1303 1303 """
1304 1304 def deltahead(binchunk):
1305 1305 i = 0
1306 1306 for c in binchunk:
1307 1307 i += 1
1308 1308 if not (ord(c) & 0x80):
1309 1309 return i
1310 1310 return i
1311 1311 out = ""
1312 1312 s = deltahead(binchunk)
1313 1313 binchunk = binchunk[s:]
1314 1314 s = deltahead(binchunk)
1315 1315 binchunk = binchunk[s:]
1316 1316 i = 0
1317 1317 while i < len(binchunk):
1318 1318 cmd = ord(binchunk[i])
1319 1319 i += 1
1320 1320 if (cmd & 0x80):
1321 1321 offset = 0
1322 1322 size = 0
1323 1323 if (cmd & 0x01):
1324 1324 offset = ord(binchunk[i])
1325 1325 i += 1
1326 1326 if (cmd & 0x02):
1327 1327 offset |= ord(binchunk[i]) << 8
1328 1328 i += 1
1329 1329 if (cmd & 0x04):
1330 1330 offset |= ord(binchunk[i]) << 16
1331 1331 i += 1
1332 1332 if (cmd & 0x08):
1333 1333 offset |= ord(binchunk[i]) << 24
1334 1334 i += 1
1335 1335 if (cmd & 0x10):
1336 1336 size = ord(binchunk[i])
1337 1337 i += 1
1338 1338 if (cmd & 0x20):
1339 1339 size |= ord(binchunk[i]) << 8
1340 1340 i += 1
1341 1341 if (cmd & 0x40):
1342 1342 size |= ord(binchunk[i]) << 16
1343 1343 i += 1
1344 1344 if size == 0:
1345 1345 size = 0x10000
1346 1346 offset_end = offset + size
1347 1347 out += data[offset:offset_end]
1348 1348 elif cmd != 0:
1349 1349 offset_end = i + cmd
1350 1350 out += binchunk[i:offset_end]
1351 1351 i += cmd
1352 1352 else:
1353 1353 raise PatchError(_('unexpected delta opcode 0'))
1354 1354 return out
1355 1355
1356 1356 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1357 1357 """Reads a patch from fp and tries to apply it.
1358 1358
1359 1359 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1360 1360 there was any fuzz.
1361 1361
1362 1362 If 'eolmode' is 'strict', the patch content and patched file are
1363 1363 read in binary mode. Otherwise, line endings are ignored when
1364 1364 patching then normalized according to 'eolmode'.
1365 1365 """
1366 1366 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1367 1367 prefix=prefix, eolmode=eolmode)
1368 1368
1369 1369 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1370 1370 eolmode='strict'):
1371 1371
1372 1372 if prefix:
1373 1373 # clean up double slashes, lack of trailing slashes, etc
1374 1374 prefix = util.normpath(prefix) + '/'
1375 1375 def pstrip(p):
1376 1376 return pathtransform(p, strip - 1, prefix)[1]
1377 1377
1378 1378 rejects = 0
1379 1379 err = 0
1380 1380 current_file = None
1381 1381
1382 1382 for state, values in iterhunks(fp):
1383 1383 if state == 'hunk':
1384 1384 if not current_file:
1385 1385 continue
1386 1386 ret = current_file.apply(values)
1387 1387 if ret > 0:
1388 1388 err = 1
1389 1389 elif state == 'file':
1390 1390 if current_file:
1391 1391 rejects += current_file.close()
1392 1392 current_file = None
1393 1393 afile, bfile, first_hunk, gp = values
1394 1394 if gp:
1395 1395 gp.path = pstrip(gp.path)
1396 1396 if gp.oldpath:
1397 1397 gp.oldpath = pstrip(gp.oldpath)
1398 1398 else:
1399 1399 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1400 1400 prefix)
1401 1401 if gp.op == 'RENAME':
1402 1402 backend.unlink(gp.oldpath)
1403 1403 if not first_hunk:
1404 1404 if gp.op == 'DELETE':
1405 1405 backend.unlink(gp.path)
1406 1406 continue
1407 1407 data, mode = None, None
1408 1408 if gp.op in ('RENAME', 'COPY'):
1409 1409 data, mode = store.getfile(gp.oldpath)[:2]
1410 1410 # FIXME: failing getfile has never been handled here
1411 1411 assert data is not None
1412 1412 if gp.mode:
1413 1413 mode = gp.mode
1414 1414 if gp.op == 'ADD':
1415 1415 # Added files without content have no hunk and
1416 1416 # must be created
1417 1417 data = ''
1418 1418 if data or mode:
1419 1419 if (gp.op in ('ADD', 'RENAME', 'COPY')
1420 1420 and backend.exists(gp.path)):
1421 1421 raise PatchError(_("cannot create %s: destination "
1422 1422 "already exists") % gp.path)
1423 1423 backend.setfile(gp.path, data, mode, gp.oldpath)
1424 1424 continue
1425 1425 try:
1426 1426 current_file = patcher(ui, gp, backend, store,
1427 1427 eolmode=eolmode)
1428 1428 except PatchError, inst:
1429 1429 ui.warn(str(inst) + '\n')
1430 1430 current_file = None
1431 1431 rejects += 1
1432 1432 continue
1433 1433 elif state == 'git':
1434 1434 for gp in values:
1435 1435 path = pstrip(gp.oldpath)
1436 1436 data, mode = backend.getfile(path)
1437 1437 if data is None:
1438 1438 # The error ignored here will trigger a getfile()
1439 1439 # error in a place more appropriate for error
1440 1440 # handling, and will not interrupt the patching
1441 1441 # process.
1442 1442 pass
1443 1443 else:
1444 1444 store.setfile(path, data, mode)
1445 1445 else:
1446 1446 raise util.Abort(_('unsupported parser state: %s') % state)
1447 1447
1448 1448 if current_file:
1449 1449 rejects += current_file.close()
1450 1450
1451 1451 if rejects:
1452 1452 return -1
1453 1453 return err
1454 1454
1455 1455 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1456 1456 similarity):
1457 1457 """use <patcher> to apply <patchname> to the working directory.
1458 1458 returns whether patch was applied with fuzz factor."""
1459 1459
1460 1460 fuzz = False
1461 1461 args = []
1462 1462 cwd = repo.root
1463 1463 if cwd:
1464 1464 args.append('-d %s' % util.shellquote(cwd))
1465 1465 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1466 1466 util.shellquote(patchname)))
1467 1467 try:
1468 1468 for line in fp:
1469 1469 line = line.rstrip()
1470 1470 ui.note(line + '\n')
1471 1471 if line.startswith('patching file '):
1472 1472 pf = util.parsepatchoutput(line)
1473 1473 printed_file = False
1474 1474 files.add(pf)
1475 1475 elif line.find('with fuzz') >= 0:
1476 1476 fuzz = True
1477 1477 if not printed_file:
1478 1478 ui.warn(pf + '\n')
1479 1479 printed_file = True
1480 1480 ui.warn(line + '\n')
1481 1481 elif line.find('saving rejects to file') >= 0:
1482 1482 ui.warn(line + '\n')
1483 1483 elif line.find('FAILED') >= 0:
1484 1484 if not printed_file:
1485 1485 ui.warn(pf + '\n')
1486 1486 printed_file = True
1487 1487 ui.warn(line + '\n')
1488 1488 finally:
1489 1489 if files:
1490 1490 scmutil.marktouched(repo, files, similarity)
1491 1491 code = fp.close()
1492 1492 if code:
1493 1493 raise PatchError(_("patch command failed: %s") %
1494 1494 util.explainexit(code)[0])
1495 1495 return fuzz
1496 1496
1497 1497 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
1498 1498 eolmode='strict'):
1499 1499 if files is None:
1500 1500 files = set()
1501 1501 if eolmode is None:
1502 1502 eolmode = ui.config('patch', 'eol', 'strict')
1503 1503 if eolmode.lower() not in eolmodes:
1504 1504 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1505 1505 eolmode = eolmode.lower()
1506 1506
1507 1507 store = filestore()
1508 1508 try:
1509 1509 fp = open(patchobj, 'rb')
1510 1510 except TypeError:
1511 1511 fp = patchobj
1512 1512 try:
1513 1513 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
1514 1514 eolmode=eolmode)
1515 1515 finally:
1516 1516 if fp != patchobj:
1517 1517 fp.close()
1518 1518 files.update(backend.close())
1519 1519 store.close()
1520 1520 if ret < 0:
1521 1521 raise PatchError(_('patch failed to apply'))
1522 1522 return ret > 0
1523 1523
1524 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1525 similarity=0):
1524 def internalpatch(ui, repo, patchobj, strip, prefix, files=None,
1525 eolmode='strict', similarity=0):
1526 1526 """use builtin patch to apply <patchobj> to the working directory.
1527 1527 returns whether patch was applied with fuzz factor."""
1528 1528 backend = workingbackend(ui, repo, similarity)
1529 return patchbackend(ui, backend, patchobj, strip, '', files, eolmode)
1529 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1530 1530
1531 1531 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1532 1532 eolmode='strict'):
1533 1533 backend = repobackend(ui, repo, ctx, store)
1534 1534 return patchbackend(ui, backend, patchobj, strip, '', files, eolmode)
1535 1535
1536 1536 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1537 1537 similarity=0):
1538 1538 """Apply <patchname> to the working directory.
1539 1539
1540 1540 'eolmode' specifies how end of lines should be handled. It can be:
1541 1541 - 'strict': inputs are read in binary mode, EOLs are preserved
1542 1542 - 'crlf': EOLs are ignored when patching and reset to CRLF
1543 1543 - 'lf': EOLs are ignored when patching and reset to LF
1544 1544 - None: get it from user settings, default to 'strict'
1545 1545 'eolmode' is ignored when using an external patcher program.
1546 1546
1547 1547 Returns whether patch was applied with fuzz factor.
1548 1548 """
1549 1549 patcher = ui.config('ui', 'patch')
1550 1550 if files is None:
1551 1551 files = set()
1552 1552 if patcher:
1553 1553 return _externalpatch(ui, repo, patcher, patchname, strip,
1554 1554 files, similarity)
1555 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1555 return internalpatch(ui, repo, patchname, strip, '', files, eolmode,
1556 1556 similarity)
1557 1557
1558 1558 def changedfiles(ui, repo, patchpath, strip=1):
1559 1559 backend = fsbackend(ui, repo.root)
1560 1560 fp = open(patchpath, 'rb')
1561 1561 try:
1562 1562 changed = set()
1563 1563 for state, values in iterhunks(fp):
1564 1564 if state == 'file':
1565 1565 afile, bfile, first_hunk, gp = values
1566 1566 if gp:
1567 1567 gp.path = pathtransform(gp.path, strip - 1, '')[1]
1568 1568 if gp.oldpath:
1569 1569 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
1570 1570 else:
1571 1571 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1572 1572 '')
1573 1573 changed.add(gp.path)
1574 1574 if gp.op == 'RENAME':
1575 1575 changed.add(gp.oldpath)
1576 1576 elif state not in ('hunk', 'git'):
1577 1577 raise util.Abort(_('unsupported parser state: %s') % state)
1578 1578 return changed
1579 1579 finally:
1580 1580 fp.close()
1581 1581
1582 1582 class GitDiffRequired(Exception):
1583 1583 pass
1584 1584
1585 1585 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
1586 1586 '''return diffopts with all features supported and parsed'''
1587 1587 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
1588 1588 git=True, whitespace=True, formatchanging=True)
1589 1589
1590 1590 diffopts = diffallopts
1591 1591
1592 1592 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
1593 1593 whitespace=False, formatchanging=False):
1594 1594 '''return diffopts with only opted-in features parsed
1595 1595
1596 1596 Features:
1597 1597 - git: git-style diffs
1598 1598 - whitespace: whitespace options like ignoreblanklines and ignorews
1599 1599 - formatchanging: options that will likely break or cause correctness issues
1600 1600 with most diff parsers
1601 1601 '''
1602 1602 def get(key, name=None, getter=ui.configbool, forceplain=None):
1603 1603 if opts:
1604 1604 v = opts.get(key)
1605 1605 if v:
1606 1606 return v
1607 1607 if forceplain is not None and ui.plain():
1608 1608 return forceplain
1609 1609 return getter(section, name or key, None, untrusted=untrusted)
1610 1610
1611 1611 # core options, expected to be understood by every diff parser
1612 1612 buildopts = {
1613 1613 'nodates': get('nodates'),
1614 1614 'showfunc': get('show_function', 'showfunc'),
1615 1615 'context': get('unified', getter=ui.config),
1616 1616 }
1617 1617
1618 1618 if git:
1619 1619 buildopts['git'] = get('git')
1620 1620 if whitespace:
1621 1621 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
1622 1622 buildopts['ignorewsamount'] = get('ignore_space_change',
1623 1623 'ignorewsamount')
1624 1624 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
1625 1625 'ignoreblanklines')
1626 1626 if formatchanging:
1627 1627 buildopts['text'] = opts and opts.get('text')
1628 1628 buildopts['nobinary'] = get('nobinary')
1629 1629 buildopts['noprefix'] = get('noprefix', forceplain=False)
1630 1630
1631 1631 return mdiff.diffopts(**buildopts)
1632 1632
1633 1633 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1634 1634 losedatafn=None, prefix=''):
1635 1635 '''yields diff of changes to files between two nodes, or node and
1636 1636 working directory.
1637 1637
1638 1638 if node1 is None, use first dirstate parent instead.
1639 1639 if node2 is None, compare node1 with working directory.
1640 1640
1641 1641 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1642 1642 every time some change cannot be represented with the current
1643 1643 patch format. Return False to upgrade to git patch format, True to
1644 1644 accept the loss or raise an exception to abort the diff. It is
1645 1645 called with the name of current file being diffed as 'fn'. If set
1646 1646 to None, patches will always be upgraded to git format when
1647 1647 necessary.
1648 1648
1649 1649 prefix is a filename prefix that is prepended to all filenames on
1650 1650 display (used for subrepos).
1651 1651 '''
1652 1652
1653 1653 if opts is None:
1654 1654 opts = mdiff.defaultopts
1655 1655
1656 1656 if not node1 and not node2:
1657 1657 node1 = repo.dirstate.p1()
1658 1658
1659 1659 def lrugetfilectx():
1660 1660 cache = {}
1661 1661 order = util.deque()
1662 1662 def getfilectx(f, ctx):
1663 1663 fctx = ctx.filectx(f, filelog=cache.get(f))
1664 1664 if f not in cache:
1665 1665 if len(cache) > 20:
1666 1666 del cache[order.popleft()]
1667 1667 cache[f] = fctx.filelog()
1668 1668 else:
1669 1669 order.remove(f)
1670 1670 order.append(f)
1671 1671 return fctx
1672 1672 return getfilectx
1673 1673 getfilectx = lrugetfilectx()
1674 1674
1675 1675 ctx1 = repo[node1]
1676 1676 ctx2 = repo[node2]
1677 1677
1678 1678 if not changes:
1679 1679 changes = repo.status(ctx1, ctx2, match=match)
1680 1680 modified, added, removed = changes[:3]
1681 1681
1682 1682 if not modified and not added and not removed:
1683 1683 return []
1684 1684
1685 1685 hexfunc = repo.ui.debugflag and hex or short
1686 1686 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
1687 1687
1688 1688 copy = {}
1689 1689 if opts.git or opts.upgrade:
1690 1690 copy = copies.pathcopies(ctx1, ctx2)
1691 1691
1692 1692 def difffn(opts, losedata):
1693 1693 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1694 1694 copy, getfilectx, opts, losedata, prefix)
1695 1695 if opts.upgrade and not opts.git:
1696 1696 try:
1697 1697 def losedata(fn):
1698 1698 if not losedatafn or not losedatafn(fn=fn):
1699 1699 raise GitDiffRequired
1700 1700 # Buffer the whole output until we are sure it can be generated
1701 1701 return list(difffn(opts.copy(git=False), losedata))
1702 1702 except GitDiffRequired:
1703 1703 return difffn(opts.copy(git=True), None)
1704 1704 else:
1705 1705 return difffn(opts, None)
1706 1706
1707 1707 def difflabel(func, *args, **kw):
1708 1708 '''yields 2-tuples of (output, label) based on the output of func()'''
1709 1709 headprefixes = [('diff', 'diff.diffline'),
1710 1710 ('copy', 'diff.extended'),
1711 1711 ('rename', 'diff.extended'),
1712 1712 ('old', 'diff.extended'),
1713 1713 ('new', 'diff.extended'),
1714 1714 ('deleted', 'diff.extended'),
1715 1715 ('---', 'diff.file_a'),
1716 1716 ('+++', 'diff.file_b')]
1717 1717 textprefixes = [('@', 'diff.hunk'),
1718 1718 ('-', 'diff.deleted'),
1719 1719 ('+', 'diff.inserted')]
1720 1720 head = False
1721 1721 for chunk in func(*args, **kw):
1722 1722 lines = chunk.split('\n')
1723 1723 for i, line in enumerate(lines):
1724 1724 if i != 0:
1725 1725 yield ('\n', '')
1726 1726 if head:
1727 1727 if line.startswith('@'):
1728 1728 head = False
1729 1729 else:
1730 1730 if line and line[0] not in ' +-@\\':
1731 1731 head = True
1732 1732 stripline = line
1733 1733 diffline = False
1734 1734 if not head and line and line[0] in '+-':
1735 1735 # highlight tabs and trailing whitespace, but only in
1736 1736 # changed lines
1737 1737 stripline = line.rstrip()
1738 1738 diffline = True
1739 1739
1740 1740 prefixes = textprefixes
1741 1741 if head:
1742 1742 prefixes = headprefixes
1743 1743 for prefix, label in prefixes:
1744 1744 if stripline.startswith(prefix):
1745 1745 if diffline:
1746 1746 for token in tabsplitter.findall(stripline):
1747 1747 if '\t' == token[0]:
1748 1748 yield (token, 'diff.tab')
1749 1749 else:
1750 1750 yield (token, label)
1751 1751 else:
1752 1752 yield (stripline, label)
1753 1753 break
1754 1754 else:
1755 1755 yield (line, '')
1756 1756 if line != stripline:
1757 1757 yield (line[len(stripline):], 'diff.trailingwhitespace')
1758 1758
1759 1759 def diffui(*args, **kw):
1760 1760 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1761 1761 return difflabel(diff, *args, **kw)
1762 1762
1763 1763 def _filepairs(ctx1, modified, added, removed, copy, opts):
1764 1764 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
1765 1765 before and f2 is the the name after. For added files, f1 will be None,
1766 1766 and for removed files, f2 will be None. copyop may be set to None, 'copy'
1767 1767 or 'rename' (the latter two only if opts.git is set).'''
1768 1768 gone = set()
1769 1769
1770 1770 copyto = dict([(v, k) for k, v in copy.items()])
1771 1771
1772 1772 addedset, removedset = set(added), set(removed)
1773 1773 # Fix up added, since merged-in additions appear as
1774 1774 # modifications during merges
1775 1775 for f in modified:
1776 1776 if f not in ctx1:
1777 1777 addedset.add(f)
1778 1778
1779 1779 for f in sorted(modified + added + removed):
1780 1780 copyop = None
1781 1781 f1, f2 = f, f
1782 1782 if f in addedset:
1783 1783 f1 = None
1784 1784 if f in copy:
1785 1785 if opts.git:
1786 1786 f1 = copy[f]
1787 1787 if f1 in removedset and f1 not in gone:
1788 1788 copyop = 'rename'
1789 1789 gone.add(f1)
1790 1790 else:
1791 1791 copyop = 'copy'
1792 1792 elif f in removedset:
1793 1793 f2 = None
1794 1794 if opts.git:
1795 1795 # have we already reported a copy above?
1796 1796 if (f in copyto and copyto[f] in addedset
1797 1797 and copy[copyto[f]] == f):
1798 1798 continue
1799 1799 yield f1, f2, copyop
1800 1800
1801 1801 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1802 1802 copy, getfilectx, opts, losedatafn, prefix):
1803 1803
1804 1804 def gitindex(text):
1805 1805 if not text:
1806 1806 text = ""
1807 1807 l = len(text)
1808 1808 s = util.sha1('blob %d\0' % l)
1809 1809 s.update(text)
1810 1810 return s.hexdigest()
1811 1811
1812 1812 if opts.noprefix:
1813 1813 aprefix = bprefix = ''
1814 1814 else:
1815 1815 aprefix = 'a/'
1816 1816 bprefix = 'b/'
1817 1817
1818 1818 def diffline(f, revs):
1819 1819 revinfo = ' '.join(["-r %s" % rev for rev in revs])
1820 1820 return 'diff %s %s' % (revinfo, f)
1821 1821
1822 1822 date1 = util.datestr(ctx1.date())
1823 1823 date2 = util.datestr(ctx2.date())
1824 1824
1825 1825 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1826 1826
1827 1827 for f1, f2, copyop in _filepairs(
1828 1828 ctx1, modified, added, removed, copy, opts):
1829 1829 content1 = None
1830 1830 content2 = None
1831 1831 flag1 = None
1832 1832 flag2 = None
1833 1833 if f1:
1834 1834 content1 = getfilectx(f1, ctx1).data()
1835 1835 if opts.git or losedatafn:
1836 1836 flag1 = ctx1.flags(f1)
1837 1837 if f2:
1838 1838 content2 = getfilectx(f2, ctx2).data()
1839 1839 if opts.git or losedatafn:
1840 1840 flag2 = ctx2.flags(f2)
1841 1841 binary = False
1842 1842 if opts.git or losedatafn:
1843 1843 binary = util.binary(content1) or util.binary(content2)
1844 1844
1845 1845 if losedatafn and not opts.git:
1846 1846 if (binary or
1847 1847 # copy/rename
1848 1848 f2 in copy or
1849 1849 # empty file creation
1850 1850 (not f1 and not content2) or
1851 1851 # empty file deletion
1852 1852 (not content1 and not f2) or
1853 1853 # create with flags
1854 1854 (not f1 and flag2) or
1855 1855 # change flags
1856 1856 (f1 and f2 and flag1 != flag2)):
1857 1857 losedatafn(f2 or f1)
1858 1858
1859 1859 path1 = posixpath.join(prefix, f1 or f2)
1860 1860 path2 = posixpath.join(prefix, f2 or f1)
1861 1861 header = []
1862 1862 if opts.git:
1863 1863 header.append('diff --git %s%s %s%s' %
1864 1864 (aprefix, path1, bprefix, path2))
1865 1865 if not f1: # added
1866 1866 header.append('new file mode %s' % gitmode[flag2])
1867 1867 elif not f2: # removed
1868 1868 header.append('deleted file mode %s' % gitmode[flag1])
1869 1869 else: # modified/copied/renamed
1870 1870 mode1, mode2 = gitmode[flag1], gitmode[flag2]
1871 1871 if mode1 != mode2:
1872 1872 header.append('old mode %s' % mode1)
1873 1873 header.append('new mode %s' % mode2)
1874 1874 if copyop is not None:
1875 1875 header.append('%s from %s' % (copyop, path1))
1876 1876 header.append('%s to %s' % (copyop, path2))
1877 1877 elif revs and not repo.ui.quiet:
1878 1878 header.append(diffline(path1, revs))
1879 1879
1880 1880 if binary and opts.git and not opts.nobinary:
1881 1881 text = mdiff.b85diff(content1, content2)
1882 1882 if text:
1883 1883 header.append('index %s..%s' %
1884 1884 (gitindex(content1), gitindex(content2)))
1885 1885 else:
1886 1886 text = mdiff.unidiff(content1, date1,
1887 1887 content2, date2,
1888 1888 path1, path2, opts=opts)
1889 1889 if header and (text or len(header) > 1):
1890 1890 yield '\n'.join(header) + '\n'
1891 1891 if text:
1892 1892 yield text
1893 1893
1894 1894 def diffstatsum(stats):
1895 1895 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1896 1896 for f, a, r, b in stats:
1897 1897 maxfile = max(maxfile, encoding.colwidth(f))
1898 1898 maxtotal = max(maxtotal, a + r)
1899 1899 addtotal += a
1900 1900 removetotal += r
1901 1901 binary = binary or b
1902 1902
1903 1903 return maxfile, maxtotal, addtotal, removetotal, binary
1904 1904
1905 1905 def diffstatdata(lines):
1906 1906 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1907 1907
1908 1908 results = []
1909 1909 filename, adds, removes, isbinary = None, 0, 0, False
1910 1910
1911 1911 def addresult():
1912 1912 if filename:
1913 1913 results.append((filename, adds, removes, isbinary))
1914 1914
1915 1915 for line in lines:
1916 1916 if line.startswith('diff'):
1917 1917 addresult()
1918 1918 # set numbers to 0 anyway when starting new file
1919 1919 adds, removes, isbinary = 0, 0, False
1920 1920 if line.startswith('diff --git a/'):
1921 1921 filename = gitre.search(line).group(2)
1922 1922 elif line.startswith('diff -r'):
1923 1923 # format: "diff -r ... -r ... filename"
1924 1924 filename = diffre.search(line).group(1)
1925 1925 elif line.startswith('+') and not line.startswith('+++ '):
1926 1926 adds += 1
1927 1927 elif line.startswith('-') and not line.startswith('--- '):
1928 1928 removes += 1
1929 1929 elif (line.startswith('GIT binary patch') or
1930 1930 line.startswith('Binary file')):
1931 1931 isbinary = True
1932 1932 addresult()
1933 1933 return results
1934 1934
1935 1935 def diffstat(lines, width=80, git=False):
1936 1936 output = []
1937 1937 stats = diffstatdata(lines)
1938 1938 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1939 1939
1940 1940 countwidth = len(str(maxtotal))
1941 1941 if hasbinary and countwidth < 3:
1942 1942 countwidth = 3
1943 1943 graphwidth = width - countwidth - maxname - 6
1944 1944 if graphwidth < 10:
1945 1945 graphwidth = 10
1946 1946
1947 1947 def scale(i):
1948 1948 if maxtotal <= graphwidth:
1949 1949 return i
1950 1950 # If diffstat runs out of room it doesn't print anything,
1951 1951 # which isn't very useful, so always print at least one + or -
1952 1952 # if there were at least some changes.
1953 1953 return max(i * graphwidth // maxtotal, int(bool(i)))
1954 1954
1955 1955 for filename, adds, removes, isbinary in stats:
1956 1956 if isbinary:
1957 1957 count = 'Bin'
1958 1958 else:
1959 1959 count = adds + removes
1960 1960 pluses = '+' * scale(adds)
1961 1961 minuses = '-' * scale(removes)
1962 1962 output.append(' %s%s | %*s %s%s\n' %
1963 1963 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1964 1964 countwidth, count, pluses, minuses))
1965 1965
1966 1966 if stats:
1967 1967 output.append(_(' %d files changed, %d insertions(+), '
1968 1968 '%d deletions(-)\n')
1969 1969 % (len(stats), totaladds, totalremoves))
1970 1970
1971 1971 return ''.join(output)
1972 1972
1973 1973 def diffstatui(*args, **kw):
1974 1974 '''like diffstat(), but yields 2-tuples of (output, label) for
1975 1975 ui.write()
1976 1976 '''
1977 1977
1978 1978 for line in diffstat(*args, **kw).splitlines():
1979 1979 if line and line[-1] in '+-':
1980 1980 name, graph = line.rsplit(' ', 1)
1981 1981 yield (name + ' ', '')
1982 1982 m = re.search(r'\++', graph)
1983 1983 if m:
1984 1984 yield (m.group(0), 'diffstat.inserted')
1985 1985 m = re.search(r'-+', graph)
1986 1986 if m:
1987 1987 yield (m.group(0), 'diffstat.deleted')
1988 1988 else:
1989 1989 yield (line, '')
1990 1990 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now