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