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