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