##// END OF EJS Templates
record: add comparison methods for recordhunk class
Laurent Charignon -
r24346:31edcea5 default
parent child Browse files
Show More
@@ -1,2401 +1,2416 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 def __eq__(self, v):
897 if not isinstance(v, recordhunk):
898 return False
899
900 return ((v.hunk == self.hunk) and
901 (v.proc == self.proc) and
902 (self.fromline == v.fromline) and
903 (self.header.files() == v.header.files()))
904
905 def __hash__(self):
906 return hash((tuple(self.hunk),
907 tuple(self.header.files()),
908 self.fromline,
909 self.proc))
910
896 911 def countchanges(self, hunk):
897 912 """hunk -> (n+,n-)"""
898 913 add = len([h for h in hunk if h[0] == '+'])
899 914 rem = len([h for h in hunk if h[0] == '-'])
900 915 return add, rem
901 916
902 917 def write(self, fp):
903 918 delta = len(self.before) + len(self.after)
904 919 if self.after and self.after[-1] == '\\ No newline at end of file\n':
905 920 delta -= 1
906 921 fromlen = delta + self.removed
907 922 tolen = delta + self.added
908 923 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
909 924 (self.fromline, fromlen, self.toline, tolen,
910 925 self.proc and (' ' + self.proc)))
911 926 fp.write(''.join(self.before + self.hunk + self.after))
912 927
913 928 pretty = write
914 929
915 930 def filename(self):
916 931 return self.header.filename()
917 932
918 933 def __repr__(self):
919 934 return '<hunk %r@%d>' % (self.filename(), self.fromline)
920 935
921 936 def filterpatch(ui, headers):
922 937 """Interactively filter patch chunks into applied-only chunks"""
923 938
924 939 def prompt(skipfile, skipall, query, chunk):
925 940 """prompt query, and process base inputs
926 941
927 942 - y/n for the rest of file
928 943 - y/n for the rest
929 944 - ? (help)
930 945 - q (quit)
931 946
932 947 Return True/False and possibly updated skipfile and skipall.
933 948 """
934 949 newpatches = None
935 950 if skipall is not None:
936 951 return skipall, skipfile, skipall, newpatches
937 952 if skipfile is not None:
938 953 return skipfile, skipfile, skipall, newpatches
939 954 while True:
940 955 resps = _('[Ynesfdaq?]'
941 956 '$$ &Yes, record this change'
942 957 '$$ &No, skip this change'
943 958 '$$ &Edit this change manually'
944 959 '$$ &Skip remaining changes to this file'
945 960 '$$ Record remaining changes to this &file'
946 961 '$$ &Done, skip remaining changes and files'
947 962 '$$ Record &all changes to all remaining files'
948 963 '$$ &Quit, recording no changes'
949 964 '$$ &? (display help)')
950 965 r = ui.promptchoice("%s %s" % (query, resps))
951 966 ui.write("\n")
952 967 if r == 8: # ?
953 968 for c, t in ui.extractchoices(resps)[1]:
954 969 ui.write('%s - %s\n' % (c, t.lower()))
955 970 continue
956 971 elif r == 0: # yes
957 972 ret = True
958 973 elif r == 1: # no
959 974 ret = False
960 975 elif r == 2: # Edit patch
961 976 if chunk is None:
962 977 ui.write(_('cannot edit patch for whole file'))
963 978 ui.write("\n")
964 979 continue
965 980 if chunk.header.binary():
966 981 ui.write(_('cannot edit patch for binary file'))
967 982 ui.write("\n")
968 983 continue
969 984 # Patch comment based on the Git one (based on comment at end of
970 985 # http://mercurial.selenic.com/wiki/RecordExtension)
971 986 phelp = '---' + _("""
972 987 To remove '-' lines, make them ' ' lines (context).
973 988 To remove '+' lines, delete them.
974 989 Lines starting with # will be removed from the patch.
975 990
976 991 If the patch applies cleanly, the edited hunk will immediately be
977 992 added to the record list. If it does not apply cleanly, a rejects
978 993 file will be generated: you can use that when you try again. If
979 994 all lines of the hunk are removed, then the edit is aborted and
980 995 the hunk is left unchanged.
981 996 """)
982 997 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
983 998 suffix=".diff", text=True)
984 999 ncpatchfp = None
985 1000 try:
986 1001 # Write the initial patch
987 1002 f = os.fdopen(patchfd, "w")
988 1003 chunk.header.write(f)
989 1004 chunk.write(f)
990 1005 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
991 1006 f.close()
992 1007 # Start the editor and wait for it to complete
993 1008 editor = ui.geteditor()
994 1009 ui.system("%s \"%s\"" % (editor, patchfn),
995 1010 environ={'HGUSER': ui.username()},
996 1011 onerr=util.Abort, errprefix=_("edit failed"))
997 1012 # Remove comment lines
998 1013 patchfp = open(patchfn)
999 1014 ncpatchfp = cStringIO.StringIO()
1000 1015 for line in patchfp:
1001 1016 if not line.startswith('#'):
1002 1017 ncpatchfp.write(line)
1003 1018 patchfp.close()
1004 1019 ncpatchfp.seek(0)
1005 1020 newpatches = parsepatch(ncpatchfp)
1006 1021 finally:
1007 1022 os.unlink(patchfn)
1008 1023 del ncpatchfp
1009 1024 # Signal that the chunk shouldn't be applied as-is, but
1010 1025 # provide the new patch to be used instead.
1011 1026 ret = False
1012 1027 elif r == 3: # Skip
1013 1028 ret = skipfile = False
1014 1029 elif r == 4: # file (Record remaining)
1015 1030 ret = skipfile = True
1016 1031 elif r == 5: # done, skip remaining
1017 1032 ret = skipall = False
1018 1033 elif r == 6: # all
1019 1034 ret = skipall = True
1020 1035 elif r == 7: # quit
1021 1036 raise util.Abort(_('user quit'))
1022 1037 return ret, skipfile, skipall, newpatches
1023 1038
1024 1039 seen = set()
1025 1040 applied = {} # 'filename' -> [] of chunks
1026 1041 skipfile, skipall = None, None
1027 1042 pos, total = 1, sum(len(h.hunks) for h in headers)
1028 1043 for h in headers:
1029 1044 pos += len(h.hunks)
1030 1045 skipfile = None
1031 1046 fixoffset = 0
1032 1047 hdr = ''.join(h.header)
1033 1048 if hdr in seen:
1034 1049 continue
1035 1050 seen.add(hdr)
1036 1051 if skipall is None:
1037 1052 h.pretty(ui)
1038 1053 msg = (_('examine changes to %s?') %
1039 1054 _(' and ').join("'%s'" % f for f in h.files()))
1040 1055 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1041 1056 if not r:
1042 1057 continue
1043 1058 applied[h.filename()] = [h]
1044 1059 if h.allhunks():
1045 1060 applied[h.filename()] += h.hunks
1046 1061 continue
1047 1062 for i, chunk in enumerate(h.hunks):
1048 1063 if skipfile is None and skipall is None:
1049 1064 chunk.pretty(ui)
1050 1065 if total == 1:
1051 1066 msg = _("record this change to '%s'?") % chunk.filename()
1052 1067 else:
1053 1068 idx = pos - len(h.hunks) + i
1054 1069 msg = _("record change %d/%d to '%s'?") % (idx, total,
1055 1070 chunk.filename())
1056 1071 r, skipfile, skipall, newpatches = prompt(skipfile,
1057 1072 skipall, msg, chunk)
1058 1073 if r:
1059 1074 if fixoffset:
1060 1075 chunk = copy.copy(chunk)
1061 1076 chunk.toline += fixoffset
1062 1077 applied[chunk.filename()].append(chunk)
1063 1078 elif newpatches is not None:
1064 1079 for newpatch in newpatches:
1065 1080 for newhunk in newpatch.hunks:
1066 1081 if fixoffset:
1067 1082 newhunk.toline += fixoffset
1068 1083 applied[newhunk.filename()].append(newhunk)
1069 1084 else:
1070 1085 fixoffset += chunk.removed - chunk.added
1071 1086 return sum([h for h in applied.itervalues()
1072 1087 if h[0].special() or len(h) > 1], [])
1073 1088 class hunk(object):
1074 1089 def __init__(self, desc, num, lr, context):
1075 1090 self.number = num
1076 1091 self.desc = desc
1077 1092 self.hunk = [desc]
1078 1093 self.a = []
1079 1094 self.b = []
1080 1095 self.starta = self.lena = None
1081 1096 self.startb = self.lenb = None
1082 1097 if lr is not None:
1083 1098 if context:
1084 1099 self.read_context_hunk(lr)
1085 1100 else:
1086 1101 self.read_unified_hunk(lr)
1087 1102
1088 1103 def getnormalized(self):
1089 1104 """Return a copy with line endings normalized to LF."""
1090 1105
1091 1106 def normalize(lines):
1092 1107 nlines = []
1093 1108 for line in lines:
1094 1109 if line.endswith('\r\n'):
1095 1110 line = line[:-2] + '\n'
1096 1111 nlines.append(line)
1097 1112 return nlines
1098 1113
1099 1114 # Dummy object, it is rebuilt manually
1100 1115 nh = hunk(self.desc, self.number, None, None)
1101 1116 nh.number = self.number
1102 1117 nh.desc = self.desc
1103 1118 nh.hunk = self.hunk
1104 1119 nh.a = normalize(self.a)
1105 1120 nh.b = normalize(self.b)
1106 1121 nh.starta = self.starta
1107 1122 nh.startb = self.startb
1108 1123 nh.lena = self.lena
1109 1124 nh.lenb = self.lenb
1110 1125 return nh
1111 1126
1112 1127 def read_unified_hunk(self, lr):
1113 1128 m = unidesc.match(self.desc)
1114 1129 if not m:
1115 1130 raise PatchError(_("bad hunk #%d") % self.number)
1116 1131 self.starta, self.lena, self.startb, self.lenb = m.groups()
1117 1132 if self.lena is None:
1118 1133 self.lena = 1
1119 1134 else:
1120 1135 self.lena = int(self.lena)
1121 1136 if self.lenb is None:
1122 1137 self.lenb = 1
1123 1138 else:
1124 1139 self.lenb = int(self.lenb)
1125 1140 self.starta = int(self.starta)
1126 1141 self.startb = int(self.startb)
1127 1142 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1128 1143 self.b)
1129 1144 # if we hit eof before finishing out the hunk, the last line will
1130 1145 # be zero length. Lets try to fix it up.
1131 1146 while len(self.hunk[-1]) == 0:
1132 1147 del self.hunk[-1]
1133 1148 del self.a[-1]
1134 1149 del self.b[-1]
1135 1150 self.lena -= 1
1136 1151 self.lenb -= 1
1137 1152 self._fixnewline(lr)
1138 1153
1139 1154 def read_context_hunk(self, lr):
1140 1155 self.desc = lr.readline()
1141 1156 m = contextdesc.match(self.desc)
1142 1157 if not m:
1143 1158 raise PatchError(_("bad hunk #%d") % self.number)
1144 1159 self.starta, aend = m.groups()
1145 1160 self.starta = int(self.starta)
1146 1161 if aend is None:
1147 1162 aend = self.starta
1148 1163 self.lena = int(aend) - self.starta
1149 1164 if self.starta:
1150 1165 self.lena += 1
1151 1166 for x in xrange(self.lena):
1152 1167 l = lr.readline()
1153 1168 if l.startswith('---'):
1154 1169 # lines addition, old block is empty
1155 1170 lr.push(l)
1156 1171 break
1157 1172 s = l[2:]
1158 1173 if l.startswith('- ') or l.startswith('! '):
1159 1174 u = '-' + s
1160 1175 elif l.startswith(' '):
1161 1176 u = ' ' + s
1162 1177 else:
1163 1178 raise PatchError(_("bad hunk #%d old text line %d") %
1164 1179 (self.number, x))
1165 1180 self.a.append(u)
1166 1181 self.hunk.append(u)
1167 1182
1168 1183 l = lr.readline()
1169 1184 if l.startswith('\ '):
1170 1185 s = self.a[-1][:-1]
1171 1186 self.a[-1] = s
1172 1187 self.hunk[-1] = s
1173 1188 l = lr.readline()
1174 1189 m = contextdesc.match(l)
1175 1190 if not m:
1176 1191 raise PatchError(_("bad hunk #%d") % self.number)
1177 1192 self.startb, bend = m.groups()
1178 1193 self.startb = int(self.startb)
1179 1194 if bend is None:
1180 1195 bend = self.startb
1181 1196 self.lenb = int(bend) - self.startb
1182 1197 if self.startb:
1183 1198 self.lenb += 1
1184 1199 hunki = 1
1185 1200 for x in xrange(self.lenb):
1186 1201 l = lr.readline()
1187 1202 if l.startswith('\ '):
1188 1203 # XXX: the only way to hit this is with an invalid line range.
1189 1204 # The no-eol marker is not counted in the line range, but I
1190 1205 # guess there are diff(1) out there which behave differently.
1191 1206 s = self.b[-1][:-1]
1192 1207 self.b[-1] = s
1193 1208 self.hunk[hunki - 1] = s
1194 1209 continue
1195 1210 if not l:
1196 1211 # line deletions, new block is empty and we hit EOF
1197 1212 lr.push(l)
1198 1213 break
1199 1214 s = l[2:]
1200 1215 if l.startswith('+ ') or l.startswith('! '):
1201 1216 u = '+' + s
1202 1217 elif l.startswith(' '):
1203 1218 u = ' ' + s
1204 1219 elif len(self.b) == 0:
1205 1220 # line deletions, new block is empty
1206 1221 lr.push(l)
1207 1222 break
1208 1223 else:
1209 1224 raise PatchError(_("bad hunk #%d old text line %d") %
1210 1225 (self.number, x))
1211 1226 self.b.append(s)
1212 1227 while True:
1213 1228 if hunki >= len(self.hunk):
1214 1229 h = ""
1215 1230 else:
1216 1231 h = self.hunk[hunki]
1217 1232 hunki += 1
1218 1233 if h == u:
1219 1234 break
1220 1235 elif h.startswith('-'):
1221 1236 continue
1222 1237 else:
1223 1238 self.hunk.insert(hunki - 1, u)
1224 1239 break
1225 1240
1226 1241 if not self.a:
1227 1242 # this happens when lines were only added to the hunk
1228 1243 for x in self.hunk:
1229 1244 if x.startswith('-') or x.startswith(' '):
1230 1245 self.a.append(x)
1231 1246 if not self.b:
1232 1247 # this happens when lines were only deleted from the hunk
1233 1248 for x in self.hunk:
1234 1249 if x.startswith('+') or x.startswith(' '):
1235 1250 self.b.append(x[1:])
1236 1251 # @@ -start,len +start,len @@
1237 1252 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1238 1253 self.startb, self.lenb)
1239 1254 self.hunk[0] = self.desc
1240 1255 self._fixnewline(lr)
1241 1256
1242 1257 def _fixnewline(self, lr):
1243 1258 l = lr.readline()
1244 1259 if l.startswith('\ '):
1245 1260 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1246 1261 else:
1247 1262 lr.push(l)
1248 1263
1249 1264 def complete(self):
1250 1265 return len(self.a) == self.lena and len(self.b) == self.lenb
1251 1266
1252 1267 def _fuzzit(self, old, new, fuzz, toponly):
1253 1268 # this removes context lines from the top and bottom of list 'l'. It
1254 1269 # checks the hunk to make sure only context lines are removed, and then
1255 1270 # returns a new shortened list of lines.
1256 1271 fuzz = min(fuzz, len(old))
1257 1272 if fuzz:
1258 1273 top = 0
1259 1274 bot = 0
1260 1275 hlen = len(self.hunk)
1261 1276 for x in xrange(hlen - 1):
1262 1277 # the hunk starts with the @@ line, so use x+1
1263 1278 if self.hunk[x + 1][0] == ' ':
1264 1279 top += 1
1265 1280 else:
1266 1281 break
1267 1282 if not toponly:
1268 1283 for x in xrange(hlen - 1):
1269 1284 if self.hunk[hlen - bot - 1][0] == ' ':
1270 1285 bot += 1
1271 1286 else:
1272 1287 break
1273 1288
1274 1289 bot = min(fuzz, bot)
1275 1290 top = min(fuzz, top)
1276 1291 return old[top:len(old) - bot], new[top:len(new) - bot], top
1277 1292 return old, new, 0
1278 1293
1279 1294 def fuzzit(self, fuzz, toponly):
1280 1295 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1281 1296 oldstart = self.starta + top
1282 1297 newstart = self.startb + top
1283 1298 # zero length hunk ranges already have their start decremented
1284 1299 if self.lena and oldstart > 0:
1285 1300 oldstart -= 1
1286 1301 if self.lenb and newstart > 0:
1287 1302 newstart -= 1
1288 1303 return old, oldstart, new, newstart
1289 1304
1290 1305 class binhunk(object):
1291 1306 'A binary patch file.'
1292 1307 def __init__(self, lr, fname):
1293 1308 self.text = None
1294 1309 self.delta = False
1295 1310 self.hunk = ['GIT binary patch\n']
1296 1311 self._fname = fname
1297 1312 self._read(lr)
1298 1313
1299 1314 def complete(self):
1300 1315 return self.text is not None
1301 1316
1302 1317 def new(self, lines):
1303 1318 if self.delta:
1304 1319 return [applybindelta(self.text, ''.join(lines))]
1305 1320 return [self.text]
1306 1321
1307 1322 def _read(self, lr):
1308 1323 def getline(lr, hunk):
1309 1324 l = lr.readline()
1310 1325 hunk.append(l)
1311 1326 return l.rstrip('\r\n')
1312 1327
1313 1328 size = 0
1314 1329 while True:
1315 1330 line = getline(lr, self.hunk)
1316 1331 if not line:
1317 1332 raise PatchError(_('could not extract "%s" binary data')
1318 1333 % self._fname)
1319 1334 if line.startswith('literal '):
1320 1335 size = int(line[8:].rstrip())
1321 1336 break
1322 1337 if line.startswith('delta '):
1323 1338 size = int(line[6:].rstrip())
1324 1339 self.delta = True
1325 1340 break
1326 1341 dec = []
1327 1342 line = getline(lr, self.hunk)
1328 1343 while len(line) > 1:
1329 1344 l = line[0]
1330 1345 if l <= 'Z' and l >= 'A':
1331 1346 l = ord(l) - ord('A') + 1
1332 1347 else:
1333 1348 l = ord(l) - ord('a') + 27
1334 1349 try:
1335 1350 dec.append(base85.b85decode(line[1:])[:l])
1336 1351 except ValueError, e:
1337 1352 raise PatchError(_('could not decode "%s" binary patch: %s')
1338 1353 % (self._fname, str(e)))
1339 1354 line = getline(lr, self.hunk)
1340 1355 text = zlib.decompress(''.join(dec))
1341 1356 if len(text) != size:
1342 1357 raise PatchError(_('"%s" length is %d bytes, should be %d')
1343 1358 % (self._fname, len(text), size))
1344 1359 self.text = text
1345 1360
1346 1361 def parsefilename(str):
1347 1362 # --- filename \t|space stuff
1348 1363 s = str[4:].rstrip('\r\n')
1349 1364 i = s.find('\t')
1350 1365 if i < 0:
1351 1366 i = s.find(' ')
1352 1367 if i < 0:
1353 1368 return s
1354 1369 return s[:i]
1355 1370
1356 1371 def parsepatch(originalchunks):
1357 1372 """patch -> [] of headers -> [] of hunks """
1358 1373 class parser(object):
1359 1374 """patch parsing state machine"""
1360 1375 def __init__(self):
1361 1376 self.fromline = 0
1362 1377 self.toline = 0
1363 1378 self.proc = ''
1364 1379 self.header = None
1365 1380 self.context = []
1366 1381 self.before = []
1367 1382 self.hunk = []
1368 1383 self.headers = []
1369 1384
1370 1385 def addrange(self, limits):
1371 1386 fromstart, fromend, tostart, toend, proc = limits
1372 1387 self.fromline = int(fromstart)
1373 1388 self.toline = int(tostart)
1374 1389 self.proc = proc
1375 1390
1376 1391 def addcontext(self, context):
1377 1392 if self.hunk:
1378 1393 h = recordhunk(self.header, self.fromline, self.toline,
1379 1394 self.proc, self.before, self.hunk, context)
1380 1395 self.header.hunks.append(h)
1381 1396 self.fromline += len(self.before) + h.removed
1382 1397 self.toline += len(self.before) + h.added
1383 1398 self.before = []
1384 1399 self.hunk = []
1385 1400 self.proc = ''
1386 1401 self.context = context
1387 1402
1388 1403 def addhunk(self, hunk):
1389 1404 if self.context:
1390 1405 self.before = self.context
1391 1406 self.context = []
1392 1407 self.hunk = hunk
1393 1408
1394 1409 def newfile(self, hdr):
1395 1410 self.addcontext([])
1396 1411 h = header(hdr)
1397 1412 self.headers.append(h)
1398 1413 self.header = h
1399 1414
1400 1415 def addother(self, line):
1401 1416 pass # 'other' lines are ignored
1402 1417
1403 1418 def finished(self):
1404 1419 self.addcontext([])
1405 1420 return self.headers
1406 1421
1407 1422 transitions = {
1408 1423 'file': {'context': addcontext,
1409 1424 'file': newfile,
1410 1425 'hunk': addhunk,
1411 1426 'range': addrange},
1412 1427 'context': {'file': newfile,
1413 1428 'hunk': addhunk,
1414 1429 'range': addrange,
1415 1430 'other': addother},
1416 1431 'hunk': {'context': addcontext,
1417 1432 'file': newfile,
1418 1433 'range': addrange},
1419 1434 'range': {'context': addcontext,
1420 1435 'hunk': addhunk},
1421 1436 'other': {'other': addother},
1422 1437 }
1423 1438
1424 1439 p = parser()
1425 1440 fp = cStringIO.StringIO()
1426 1441 fp.write(''.join(originalchunks))
1427 1442 fp.seek(0)
1428 1443
1429 1444 state = 'context'
1430 1445 for newstate, data in scanpatch(fp):
1431 1446 try:
1432 1447 p.transitions[state][newstate](p, data)
1433 1448 except KeyError:
1434 1449 raise PatchError('unhandled transition: %s -> %s' %
1435 1450 (state, newstate))
1436 1451 state = newstate
1437 1452 del fp
1438 1453 return p.finished()
1439 1454
1440 1455 def pathtransform(path, strip, prefix):
1441 1456 '''turn a path from a patch into a path suitable for the repository
1442 1457
1443 1458 prefix, if not empty, is expected to be normalized with a / at the end.
1444 1459
1445 1460 Returns (stripped components, path in repository).
1446 1461
1447 1462 >>> pathtransform('a/b/c', 0, '')
1448 1463 ('', 'a/b/c')
1449 1464 >>> pathtransform(' a/b/c ', 0, '')
1450 1465 ('', ' a/b/c')
1451 1466 >>> pathtransform(' a/b/c ', 2, '')
1452 1467 ('a/b/', 'c')
1453 1468 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1454 1469 ('a//b/', 'd/e/c')
1455 1470 >>> pathtransform('a/b/c', 3, '')
1456 1471 Traceback (most recent call last):
1457 1472 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1458 1473 '''
1459 1474 pathlen = len(path)
1460 1475 i = 0
1461 1476 if strip == 0:
1462 1477 return '', path.rstrip()
1463 1478 count = strip
1464 1479 while count > 0:
1465 1480 i = path.find('/', i)
1466 1481 if i == -1:
1467 1482 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1468 1483 (count, strip, path))
1469 1484 i += 1
1470 1485 # consume '//' in the path
1471 1486 while i < pathlen - 1 and path[i] == '/':
1472 1487 i += 1
1473 1488 count -= 1
1474 1489 return path[:i].lstrip(), prefix + path[i:].rstrip()
1475 1490
1476 1491 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1477 1492 nulla = afile_orig == "/dev/null"
1478 1493 nullb = bfile_orig == "/dev/null"
1479 1494 create = nulla and hunk.starta == 0 and hunk.lena == 0
1480 1495 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1481 1496 abase, afile = pathtransform(afile_orig, strip, prefix)
1482 1497 gooda = not nulla and backend.exists(afile)
1483 1498 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1484 1499 if afile == bfile:
1485 1500 goodb = gooda
1486 1501 else:
1487 1502 goodb = not nullb and backend.exists(bfile)
1488 1503 missing = not goodb and not gooda and not create
1489 1504
1490 1505 # some diff programs apparently produce patches where the afile is
1491 1506 # not /dev/null, but afile starts with bfile
1492 1507 abasedir = afile[:afile.rfind('/') + 1]
1493 1508 bbasedir = bfile[:bfile.rfind('/') + 1]
1494 1509 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1495 1510 and hunk.starta == 0 and hunk.lena == 0):
1496 1511 create = True
1497 1512 missing = False
1498 1513
1499 1514 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1500 1515 # diff is between a file and its backup. In this case, the original
1501 1516 # file should be patched (see original mpatch code).
1502 1517 isbackup = (abase == bbase and bfile.startswith(afile))
1503 1518 fname = None
1504 1519 if not missing:
1505 1520 if gooda and goodb:
1506 1521 if isbackup:
1507 1522 fname = afile
1508 1523 else:
1509 1524 fname = bfile
1510 1525 elif gooda:
1511 1526 fname = afile
1512 1527
1513 1528 if not fname:
1514 1529 if not nullb:
1515 1530 if isbackup:
1516 1531 fname = afile
1517 1532 else:
1518 1533 fname = bfile
1519 1534 elif not nulla:
1520 1535 fname = afile
1521 1536 else:
1522 1537 raise PatchError(_("undefined source and destination files"))
1523 1538
1524 1539 gp = patchmeta(fname)
1525 1540 if create:
1526 1541 gp.op = 'ADD'
1527 1542 elif remove:
1528 1543 gp.op = 'DELETE'
1529 1544 return gp
1530 1545
1531 1546 def scanpatch(fp):
1532 1547 """like patch.iterhunks, but yield different events
1533 1548
1534 1549 - ('file', [header_lines + fromfile + tofile])
1535 1550 - ('context', [context_lines])
1536 1551 - ('hunk', [hunk_lines])
1537 1552 - ('range', (-start,len, +start,len, proc))
1538 1553 """
1539 1554 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1540 1555 lr = linereader(fp)
1541 1556
1542 1557 def scanwhile(first, p):
1543 1558 """scan lr while predicate holds"""
1544 1559 lines = [first]
1545 1560 while True:
1546 1561 line = lr.readline()
1547 1562 if not line:
1548 1563 break
1549 1564 if p(line):
1550 1565 lines.append(line)
1551 1566 else:
1552 1567 lr.push(line)
1553 1568 break
1554 1569 return lines
1555 1570
1556 1571 while True:
1557 1572 line = lr.readline()
1558 1573 if not line:
1559 1574 break
1560 1575 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1561 1576 def notheader(line):
1562 1577 s = line.split(None, 1)
1563 1578 return not s or s[0] not in ('---', 'diff')
1564 1579 header = scanwhile(line, notheader)
1565 1580 fromfile = lr.readline()
1566 1581 if fromfile.startswith('---'):
1567 1582 tofile = lr.readline()
1568 1583 header += [fromfile, tofile]
1569 1584 else:
1570 1585 lr.push(fromfile)
1571 1586 yield 'file', header
1572 1587 elif line[0] == ' ':
1573 1588 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1574 1589 elif line[0] in '-+':
1575 1590 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1576 1591 else:
1577 1592 m = lines_re.match(line)
1578 1593 if m:
1579 1594 yield 'range', m.groups()
1580 1595 else:
1581 1596 yield 'other', line
1582 1597
1583 1598 def scangitpatch(lr, firstline):
1584 1599 """
1585 1600 Git patches can emit:
1586 1601 - rename a to b
1587 1602 - change b
1588 1603 - copy a to c
1589 1604 - change c
1590 1605
1591 1606 We cannot apply this sequence as-is, the renamed 'a' could not be
1592 1607 found for it would have been renamed already. And we cannot copy
1593 1608 from 'b' instead because 'b' would have been changed already. So
1594 1609 we scan the git patch for copy and rename commands so we can
1595 1610 perform the copies ahead of time.
1596 1611 """
1597 1612 pos = 0
1598 1613 try:
1599 1614 pos = lr.fp.tell()
1600 1615 fp = lr.fp
1601 1616 except IOError:
1602 1617 fp = cStringIO.StringIO(lr.fp.read())
1603 1618 gitlr = linereader(fp)
1604 1619 gitlr.push(firstline)
1605 1620 gitpatches = readgitpatch(gitlr)
1606 1621 fp.seek(pos)
1607 1622 return gitpatches
1608 1623
1609 1624 def iterhunks(fp):
1610 1625 """Read a patch and yield the following events:
1611 1626 - ("file", afile, bfile, firsthunk): select a new target file.
1612 1627 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1613 1628 "file" event.
1614 1629 - ("git", gitchanges): current diff is in git format, gitchanges
1615 1630 maps filenames to gitpatch records. Unique event.
1616 1631 """
1617 1632 afile = ""
1618 1633 bfile = ""
1619 1634 state = None
1620 1635 hunknum = 0
1621 1636 emitfile = newfile = False
1622 1637 gitpatches = None
1623 1638
1624 1639 # our states
1625 1640 BFILE = 1
1626 1641 context = None
1627 1642 lr = linereader(fp)
1628 1643
1629 1644 while True:
1630 1645 x = lr.readline()
1631 1646 if not x:
1632 1647 break
1633 1648 if state == BFILE and (
1634 1649 (not context and x[0] == '@')
1635 1650 or (context is not False and x.startswith('***************'))
1636 1651 or x.startswith('GIT binary patch')):
1637 1652 gp = None
1638 1653 if (gitpatches and
1639 1654 gitpatches[-1].ispatching(afile, bfile)):
1640 1655 gp = gitpatches.pop()
1641 1656 if x.startswith('GIT binary patch'):
1642 1657 h = binhunk(lr, gp.path)
1643 1658 else:
1644 1659 if context is None and x.startswith('***************'):
1645 1660 context = True
1646 1661 h = hunk(x, hunknum + 1, lr, context)
1647 1662 hunknum += 1
1648 1663 if emitfile:
1649 1664 emitfile = False
1650 1665 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1651 1666 yield 'hunk', h
1652 1667 elif x.startswith('diff --git a/'):
1653 1668 m = gitre.match(x.rstrip(' \r\n'))
1654 1669 if not m:
1655 1670 continue
1656 1671 if gitpatches is None:
1657 1672 # scan whole input for git metadata
1658 1673 gitpatches = scangitpatch(lr, x)
1659 1674 yield 'git', [g.copy() for g in gitpatches
1660 1675 if g.op in ('COPY', 'RENAME')]
1661 1676 gitpatches.reverse()
1662 1677 afile = 'a/' + m.group(1)
1663 1678 bfile = 'b/' + m.group(2)
1664 1679 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1665 1680 gp = gitpatches.pop()
1666 1681 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1667 1682 if not gitpatches:
1668 1683 raise PatchError(_('failed to synchronize metadata for "%s"')
1669 1684 % afile[2:])
1670 1685 gp = gitpatches[-1]
1671 1686 newfile = True
1672 1687 elif x.startswith('---'):
1673 1688 # check for a unified diff
1674 1689 l2 = lr.readline()
1675 1690 if not l2.startswith('+++'):
1676 1691 lr.push(l2)
1677 1692 continue
1678 1693 newfile = True
1679 1694 context = False
1680 1695 afile = parsefilename(x)
1681 1696 bfile = parsefilename(l2)
1682 1697 elif x.startswith('***'):
1683 1698 # check for a context diff
1684 1699 l2 = lr.readline()
1685 1700 if not l2.startswith('---'):
1686 1701 lr.push(l2)
1687 1702 continue
1688 1703 l3 = lr.readline()
1689 1704 lr.push(l3)
1690 1705 if not l3.startswith("***************"):
1691 1706 lr.push(l2)
1692 1707 continue
1693 1708 newfile = True
1694 1709 context = True
1695 1710 afile = parsefilename(x)
1696 1711 bfile = parsefilename(l2)
1697 1712
1698 1713 if newfile:
1699 1714 newfile = False
1700 1715 emitfile = True
1701 1716 state = BFILE
1702 1717 hunknum = 0
1703 1718
1704 1719 while gitpatches:
1705 1720 gp = gitpatches.pop()
1706 1721 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1707 1722
1708 1723 def applybindelta(binchunk, data):
1709 1724 """Apply a binary delta hunk
1710 1725 The algorithm used is the algorithm from git's patch-delta.c
1711 1726 """
1712 1727 def deltahead(binchunk):
1713 1728 i = 0
1714 1729 for c in binchunk:
1715 1730 i += 1
1716 1731 if not (ord(c) & 0x80):
1717 1732 return i
1718 1733 return i
1719 1734 out = ""
1720 1735 s = deltahead(binchunk)
1721 1736 binchunk = binchunk[s:]
1722 1737 s = deltahead(binchunk)
1723 1738 binchunk = binchunk[s:]
1724 1739 i = 0
1725 1740 while i < len(binchunk):
1726 1741 cmd = ord(binchunk[i])
1727 1742 i += 1
1728 1743 if (cmd & 0x80):
1729 1744 offset = 0
1730 1745 size = 0
1731 1746 if (cmd & 0x01):
1732 1747 offset = ord(binchunk[i])
1733 1748 i += 1
1734 1749 if (cmd & 0x02):
1735 1750 offset |= ord(binchunk[i]) << 8
1736 1751 i += 1
1737 1752 if (cmd & 0x04):
1738 1753 offset |= ord(binchunk[i]) << 16
1739 1754 i += 1
1740 1755 if (cmd & 0x08):
1741 1756 offset |= ord(binchunk[i]) << 24
1742 1757 i += 1
1743 1758 if (cmd & 0x10):
1744 1759 size = ord(binchunk[i])
1745 1760 i += 1
1746 1761 if (cmd & 0x20):
1747 1762 size |= ord(binchunk[i]) << 8
1748 1763 i += 1
1749 1764 if (cmd & 0x40):
1750 1765 size |= ord(binchunk[i]) << 16
1751 1766 i += 1
1752 1767 if size == 0:
1753 1768 size = 0x10000
1754 1769 offset_end = offset + size
1755 1770 out += data[offset:offset_end]
1756 1771 elif cmd != 0:
1757 1772 offset_end = i + cmd
1758 1773 out += binchunk[i:offset_end]
1759 1774 i += cmd
1760 1775 else:
1761 1776 raise PatchError(_('unexpected delta opcode 0'))
1762 1777 return out
1763 1778
1764 1779 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1765 1780 """Reads a patch from fp and tries to apply it.
1766 1781
1767 1782 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1768 1783 there was any fuzz.
1769 1784
1770 1785 If 'eolmode' is 'strict', the patch content and patched file are
1771 1786 read in binary mode. Otherwise, line endings are ignored when
1772 1787 patching then normalized according to 'eolmode'.
1773 1788 """
1774 1789 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1775 1790 prefix=prefix, eolmode=eolmode)
1776 1791
1777 1792 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1778 1793 eolmode='strict'):
1779 1794
1780 1795 if prefix:
1781 1796 # clean up double slashes, lack of trailing slashes, etc
1782 1797 prefix = util.normpath(prefix) + '/'
1783 1798 def pstrip(p):
1784 1799 return pathtransform(p, strip - 1, prefix)[1]
1785 1800
1786 1801 rejects = 0
1787 1802 err = 0
1788 1803 current_file = None
1789 1804
1790 1805 for state, values in iterhunks(fp):
1791 1806 if state == 'hunk':
1792 1807 if not current_file:
1793 1808 continue
1794 1809 ret = current_file.apply(values)
1795 1810 if ret > 0:
1796 1811 err = 1
1797 1812 elif state == 'file':
1798 1813 if current_file:
1799 1814 rejects += current_file.close()
1800 1815 current_file = None
1801 1816 afile, bfile, first_hunk, gp = values
1802 1817 if gp:
1803 1818 gp.path = pstrip(gp.path)
1804 1819 if gp.oldpath:
1805 1820 gp.oldpath = pstrip(gp.oldpath)
1806 1821 else:
1807 1822 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1808 1823 prefix)
1809 1824 if gp.op == 'RENAME':
1810 1825 backend.unlink(gp.oldpath)
1811 1826 if not first_hunk:
1812 1827 if gp.op == 'DELETE':
1813 1828 backend.unlink(gp.path)
1814 1829 continue
1815 1830 data, mode = None, None
1816 1831 if gp.op in ('RENAME', 'COPY'):
1817 1832 data, mode = store.getfile(gp.oldpath)[:2]
1818 1833 # FIXME: failing getfile has never been handled here
1819 1834 assert data is not None
1820 1835 if gp.mode:
1821 1836 mode = gp.mode
1822 1837 if gp.op == 'ADD':
1823 1838 # Added files without content have no hunk and
1824 1839 # must be created
1825 1840 data = ''
1826 1841 if data or mode:
1827 1842 if (gp.op in ('ADD', 'RENAME', 'COPY')
1828 1843 and backend.exists(gp.path)):
1829 1844 raise PatchError(_("cannot create %s: destination "
1830 1845 "already exists") % gp.path)
1831 1846 backend.setfile(gp.path, data, mode, gp.oldpath)
1832 1847 continue
1833 1848 try:
1834 1849 current_file = patcher(ui, gp, backend, store,
1835 1850 eolmode=eolmode)
1836 1851 except PatchError, inst:
1837 1852 ui.warn(str(inst) + '\n')
1838 1853 current_file = None
1839 1854 rejects += 1
1840 1855 continue
1841 1856 elif state == 'git':
1842 1857 for gp in values:
1843 1858 path = pstrip(gp.oldpath)
1844 1859 data, mode = backend.getfile(path)
1845 1860 if data is None:
1846 1861 # The error ignored here will trigger a getfile()
1847 1862 # error in a place more appropriate for error
1848 1863 # handling, and will not interrupt the patching
1849 1864 # process.
1850 1865 pass
1851 1866 else:
1852 1867 store.setfile(path, data, mode)
1853 1868 else:
1854 1869 raise util.Abort(_('unsupported parser state: %s') % state)
1855 1870
1856 1871 if current_file:
1857 1872 rejects += current_file.close()
1858 1873
1859 1874 if rejects:
1860 1875 return -1
1861 1876 return err
1862 1877
1863 1878 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1864 1879 similarity):
1865 1880 """use <patcher> to apply <patchname> to the working directory.
1866 1881 returns whether patch was applied with fuzz factor."""
1867 1882
1868 1883 fuzz = False
1869 1884 args = []
1870 1885 cwd = repo.root
1871 1886 if cwd:
1872 1887 args.append('-d %s' % util.shellquote(cwd))
1873 1888 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1874 1889 util.shellquote(patchname)))
1875 1890 try:
1876 1891 for line in fp:
1877 1892 line = line.rstrip()
1878 1893 ui.note(line + '\n')
1879 1894 if line.startswith('patching file '):
1880 1895 pf = util.parsepatchoutput(line)
1881 1896 printed_file = False
1882 1897 files.add(pf)
1883 1898 elif line.find('with fuzz') >= 0:
1884 1899 fuzz = True
1885 1900 if not printed_file:
1886 1901 ui.warn(pf + '\n')
1887 1902 printed_file = True
1888 1903 ui.warn(line + '\n')
1889 1904 elif line.find('saving rejects to file') >= 0:
1890 1905 ui.warn(line + '\n')
1891 1906 elif line.find('FAILED') >= 0:
1892 1907 if not printed_file:
1893 1908 ui.warn(pf + '\n')
1894 1909 printed_file = True
1895 1910 ui.warn(line + '\n')
1896 1911 finally:
1897 1912 if files:
1898 1913 scmutil.marktouched(repo, files, similarity)
1899 1914 code = fp.close()
1900 1915 if code:
1901 1916 raise PatchError(_("patch command failed: %s") %
1902 1917 util.explainexit(code)[0])
1903 1918 return fuzz
1904 1919
1905 1920 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
1906 1921 eolmode='strict'):
1907 1922 if files is None:
1908 1923 files = set()
1909 1924 if eolmode is None:
1910 1925 eolmode = ui.config('patch', 'eol', 'strict')
1911 1926 if eolmode.lower() not in eolmodes:
1912 1927 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1913 1928 eolmode = eolmode.lower()
1914 1929
1915 1930 store = filestore()
1916 1931 try:
1917 1932 fp = open(patchobj, 'rb')
1918 1933 except TypeError:
1919 1934 fp = patchobj
1920 1935 try:
1921 1936 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
1922 1937 eolmode=eolmode)
1923 1938 finally:
1924 1939 if fp != patchobj:
1925 1940 fp.close()
1926 1941 files.update(backend.close())
1927 1942 store.close()
1928 1943 if ret < 0:
1929 1944 raise PatchError(_('patch failed to apply'))
1930 1945 return ret > 0
1931 1946
1932 1947 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
1933 1948 eolmode='strict', similarity=0):
1934 1949 """use builtin patch to apply <patchobj> to the working directory.
1935 1950 returns whether patch was applied with fuzz factor."""
1936 1951 backend = workingbackend(ui, repo, similarity)
1937 1952 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1938 1953
1939 1954 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
1940 1955 eolmode='strict'):
1941 1956 backend = repobackend(ui, repo, ctx, store)
1942 1957 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1943 1958
1944 1959 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
1945 1960 similarity=0):
1946 1961 """Apply <patchname> to the working directory.
1947 1962
1948 1963 'eolmode' specifies how end of lines should be handled. It can be:
1949 1964 - 'strict': inputs are read in binary mode, EOLs are preserved
1950 1965 - 'crlf': EOLs are ignored when patching and reset to CRLF
1951 1966 - 'lf': EOLs are ignored when patching and reset to LF
1952 1967 - None: get it from user settings, default to 'strict'
1953 1968 'eolmode' is ignored when using an external patcher program.
1954 1969
1955 1970 Returns whether patch was applied with fuzz factor.
1956 1971 """
1957 1972 patcher = ui.config('ui', 'patch')
1958 1973 if files is None:
1959 1974 files = set()
1960 1975 if patcher:
1961 1976 return _externalpatch(ui, repo, patcher, patchname, strip,
1962 1977 files, similarity)
1963 1978 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
1964 1979 similarity)
1965 1980
1966 1981 def changedfiles(ui, repo, patchpath, strip=1):
1967 1982 backend = fsbackend(ui, repo.root)
1968 1983 fp = open(patchpath, 'rb')
1969 1984 try:
1970 1985 changed = set()
1971 1986 for state, values in iterhunks(fp):
1972 1987 if state == 'file':
1973 1988 afile, bfile, first_hunk, gp = values
1974 1989 if gp:
1975 1990 gp.path = pathtransform(gp.path, strip - 1, '')[1]
1976 1991 if gp.oldpath:
1977 1992 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
1978 1993 else:
1979 1994 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1980 1995 '')
1981 1996 changed.add(gp.path)
1982 1997 if gp.op == 'RENAME':
1983 1998 changed.add(gp.oldpath)
1984 1999 elif state not in ('hunk', 'git'):
1985 2000 raise util.Abort(_('unsupported parser state: %s') % state)
1986 2001 return changed
1987 2002 finally:
1988 2003 fp.close()
1989 2004
1990 2005 class GitDiffRequired(Exception):
1991 2006 pass
1992 2007
1993 2008 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
1994 2009 '''return diffopts with all features supported and parsed'''
1995 2010 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
1996 2011 git=True, whitespace=True, formatchanging=True)
1997 2012
1998 2013 diffopts = diffallopts
1999 2014
2000 2015 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2001 2016 whitespace=False, formatchanging=False):
2002 2017 '''return diffopts with only opted-in features parsed
2003 2018
2004 2019 Features:
2005 2020 - git: git-style diffs
2006 2021 - whitespace: whitespace options like ignoreblanklines and ignorews
2007 2022 - formatchanging: options that will likely break or cause correctness issues
2008 2023 with most diff parsers
2009 2024 '''
2010 2025 def get(key, name=None, getter=ui.configbool, forceplain=None):
2011 2026 if opts:
2012 2027 v = opts.get(key)
2013 2028 if v:
2014 2029 return v
2015 2030 if forceplain is not None and ui.plain():
2016 2031 return forceplain
2017 2032 return getter(section, name or key, None, untrusted=untrusted)
2018 2033
2019 2034 # core options, expected to be understood by every diff parser
2020 2035 buildopts = {
2021 2036 'nodates': get('nodates'),
2022 2037 'showfunc': get('show_function', 'showfunc'),
2023 2038 'context': get('unified', getter=ui.config),
2024 2039 }
2025 2040
2026 2041 if git:
2027 2042 buildopts['git'] = get('git')
2028 2043 if whitespace:
2029 2044 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2030 2045 buildopts['ignorewsamount'] = get('ignore_space_change',
2031 2046 'ignorewsamount')
2032 2047 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2033 2048 'ignoreblanklines')
2034 2049 if formatchanging:
2035 2050 buildopts['text'] = opts and opts.get('text')
2036 2051 buildopts['nobinary'] = get('nobinary')
2037 2052 buildopts['noprefix'] = get('noprefix', forceplain=False)
2038 2053
2039 2054 return mdiff.diffopts(**buildopts)
2040 2055
2041 2056 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2042 2057 losedatafn=None, prefix=''):
2043 2058 '''yields diff of changes to files between two nodes, or node and
2044 2059 working directory.
2045 2060
2046 2061 if node1 is None, use first dirstate parent instead.
2047 2062 if node2 is None, compare node1 with working directory.
2048 2063
2049 2064 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2050 2065 every time some change cannot be represented with the current
2051 2066 patch format. Return False to upgrade to git patch format, True to
2052 2067 accept the loss or raise an exception to abort the diff. It is
2053 2068 called with the name of current file being diffed as 'fn'. If set
2054 2069 to None, patches will always be upgraded to git format when
2055 2070 necessary.
2056 2071
2057 2072 prefix is a filename prefix that is prepended to all filenames on
2058 2073 display (used for subrepos).
2059 2074 '''
2060 2075
2061 2076 if opts is None:
2062 2077 opts = mdiff.defaultopts
2063 2078
2064 2079 if not node1 and not node2:
2065 2080 node1 = repo.dirstate.p1()
2066 2081
2067 2082 def lrugetfilectx():
2068 2083 cache = {}
2069 2084 order = util.deque()
2070 2085 def getfilectx(f, ctx):
2071 2086 fctx = ctx.filectx(f, filelog=cache.get(f))
2072 2087 if f not in cache:
2073 2088 if len(cache) > 20:
2074 2089 del cache[order.popleft()]
2075 2090 cache[f] = fctx.filelog()
2076 2091 else:
2077 2092 order.remove(f)
2078 2093 order.append(f)
2079 2094 return fctx
2080 2095 return getfilectx
2081 2096 getfilectx = lrugetfilectx()
2082 2097
2083 2098 ctx1 = repo[node1]
2084 2099 ctx2 = repo[node2]
2085 2100
2086 2101 if not changes:
2087 2102 changes = repo.status(ctx1, ctx2, match=match)
2088 2103 modified, added, removed = changes[:3]
2089 2104
2090 2105 if not modified and not added and not removed:
2091 2106 return []
2092 2107
2093 2108 if repo.ui.debugflag:
2094 2109 hexfunc = hex
2095 2110 else:
2096 2111 hexfunc = short
2097 2112 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2098 2113
2099 2114 copy = {}
2100 2115 if opts.git or opts.upgrade:
2101 2116 copy = copies.pathcopies(ctx1, ctx2)
2102 2117
2103 2118 def difffn(opts, losedata):
2104 2119 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2105 2120 copy, getfilectx, opts, losedata, prefix)
2106 2121 if opts.upgrade and not opts.git:
2107 2122 try:
2108 2123 def losedata(fn):
2109 2124 if not losedatafn or not losedatafn(fn=fn):
2110 2125 raise GitDiffRequired
2111 2126 # Buffer the whole output until we are sure it can be generated
2112 2127 return list(difffn(opts.copy(git=False), losedata))
2113 2128 except GitDiffRequired:
2114 2129 return difffn(opts.copy(git=True), None)
2115 2130 else:
2116 2131 return difffn(opts, None)
2117 2132
2118 2133 def difflabel(func, *args, **kw):
2119 2134 '''yields 2-tuples of (output, label) based on the output of func()'''
2120 2135 headprefixes = [('diff', 'diff.diffline'),
2121 2136 ('copy', 'diff.extended'),
2122 2137 ('rename', 'diff.extended'),
2123 2138 ('old', 'diff.extended'),
2124 2139 ('new', 'diff.extended'),
2125 2140 ('deleted', 'diff.extended'),
2126 2141 ('---', 'diff.file_a'),
2127 2142 ('+++', 'diff.file_b')]
2128 2143 textprefixes = [('@', 'diff.hunk'),
2129 2144 ('-', 'diff.deleted'),
2130 2145 ('+', 'diff.inserted')]
2131 2146 head = False
2132 2147 for chunk in func(*args, **kw):
2133 2148 lines = chunk.split('\n')
2134 2149 for i, line in enumerate(lines):
2135 2150 if i != 0:
2136 2151 yield ('\n', '')
2137 2152 if head:
2138 2153 if line.startswith('@'):
2139 2154 head = False
2140 2155 else:
2141 2156 if line and line[0] not in ' +-@\\':
2142 2157 head = True
2143 2158 stripline = line
2144 2159 diffline = False
2145 2160 if not head and line and line[0] in '+-':
2146 2161 # highlight tabs and trailing whitespace, but only in
2147 2162 # changed lines
2148 2163 stripline = line.rstrip()
2149 2164 diffline = True
2150 2165
2151 2166 prefixes = textprefixes
2152 2167 if head:
2153 2168 prefixes = headprefixes
2154 2169 for prefix, label in prefixes:
2155 2170 if stripline.startswith(prefix):
2156 2171 if diffline:
2157 2172 for token in tabsplitter.findall(stripline):
2158 2173 if '\t' == token[0]:
2159 2174 yield (token, 'diff.tab')
2160 2175 else:
2161 2176 yield (token, label)
2162 2177 else:
2163 2178 yield (stripline, label)
2164 2179 break
2165 2180 else:
2166 2181 yield (line, '')
2167 2182 if line != stripline:
2168 2183 yield (line[len(stripline):], 'diff.trailingwhitespace')
2169 2184
2170 2185 def diffui(*args, **kw):
2171 2186 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2172 2187 return difflabel(diff, *args, **kw)
2173 2188
2174 2189 def _filepairs(ctx1, modified, added, removed, copy, opts):
2175 2190 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2176 2191 before and f2 is the the name after. For added files, f1 will be None,
2177 2192 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2178 2193 or 'rename' (the latter two only if opts.git is set).'''
2179 2194 gone = set()
2180 2195
2181 2196 copyto = dict([(v, k) for k, v in copy.items()])
2182 2197
2183 2198 addedset, removedset = set(added), set(removed)
2184 2199 # Fix up added, since merged-in additions appear as
2185 2200 # modifications during merges
2186 2201 for f in modified:
2187 2202 if f not in ctx1:
2188 2203 addedset.add(f)
2189 2204
2190 2205 for f in sorted(modified + added + removed):
2191 2206 copyop = None
2192 2207 f1, f2 = f, f
2193 2208 if f in addedset:
2194 2209 f1 = None
2195 2210 if f in copy:
2196 2211 if opts.git:
2197 2212 f1 = copy[f]
2198 2213 if f1 in removedset and f1 not in gone:
2199 2214 copyop = 'rename'
2200 2215 gone.add(f1)
2201 2216 else:
2202 2217 copyop = 'copy'
2203 2218 elif f in removedset:
2204 2219 f2 = None
2205 2220 if opts.git:
2206 2221 # have we already reported a copy above?
2207 2222 if (f in copyto and copyto[f] in addedset
2208 2223 and copy[copyto[f]] == f):
2209 2224 continue
2210 2225 yield f1, f2, copyop
2211 2226
2212 2227 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2213 2228 copy, getfilectx, opts, losedatafn, prefix):
2214 2229
2215 2230 def gitindex(text):
2216 2231 if not text:
2217 2232 text = ""
2218 2233 l = len(text)
2219 2234 s = util.sha1('blob %d\0' % l)
2220 2235 s.update(text)
2221 2236 return s.hexdigest()
2222 2237
2223 2238 if opts.noprefix:
2224 2239 aprefix = bprefix = ''
2225 2240 else:
2226 2241 aprefix = 'a/'
2227 2242 bprefix = 'b/'
2228 2243
2229 2244 def diffline(f, revs):
2230 2245 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2231 2246 return 'diff %s %s' % (revinfo, f)
2232 2247
2233 2248 date1 = util.datestr(ctx1.date())
2234 2249 date2 = util.datestr(ctx2.date())
2235 2250
2236 2251 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2237 2252
2238 2253 for f1, f2, copyop in _filepairs(
2239 2254 ctx1, modified, added, removed, copy, opts):
2240 2255 content1 = None
2241 2256 content2 = None
2242 2257 flag1 = None
2243 2258 flag2 = None
2244 2259 if f1:
2245 2260 content1 = getfilectx(f1, ctx1).data()
2246 2261 if opts.git or losedatafn:
2247 2262 flag1 = ctx1.flags(f1)
2248 2263 if f2:
2249 2264 content2 = getfilectx(f2, ctx2).data()
2250 2265 if opts.git or losedatafn:
2251 2266 flag2 = ctx2.flags(f2)
2252 2267 binary = False
2253 2268 if opts.git or losedatafn:
2254 2269 binary = util.binary(content1) or util.binary(content2)
2255 2270
2256 2271 if losedatafn and not opts.git:
2257 2272 if (binary or
2258 2273 # copy/rename
2259 2274 f2 in copy or
2260 2275 # empty file creation
2261 2276 (not f1 and not content2) or
2262 2277 # empty file deletion
2263 2278 (not content1 and not f2) or
2264 2279 # create with flags
2265 2280 (not f1 and flag2) or
2266 2281 # change flags
2267 2282 (f1 and f2 and flag1 != flag2)):
2268 2283 losedatafn(f2 or f1)
2269 2284
2270 2285 path1 = posixpath.join(prefix, f1 or f2)
2271 2286 path2 = posixpath.join(prefix, f2 or f1)
2272 2287 header = []
2273 2288 if opts.git:
2274 2289 header.append('diff --git %s%s %s%s' %
2275 2290 (aprefix, path1, bprefix, path2))
2276 2291 if not f1: # added
2277 2292 header.append('new file mode %s' % gitmode[flag2])
2278 2293 elif not f2: # removed
2279 2294 header.append('deleted file mode %s' % gitmode[flag1])
2280 2295 else: # modified/copied/renamed
2281 2296 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2282 2297 if mode1 != mode2:
2283 2298 header.append('old mode %s' % mode1)
2284 2299 header.append('new mode %s' % mode2)
2285 2300 if copyop is not None:
2286 2301 header.append('%s from %s' % (copyop, path1))
2287 2302 header.append('%s to %s' % (copyop, path2))
2288 2303 elif revs and not repo.ui.quiet:
2289 2304 header.append(diffline(path1, revs))
2290 2305
2291 2306 if binary and opts.git and not opts.nobinary:
2292 2307 text = mdiff.b85diff(content1, content2)
2293 2308 if text:
2294 2309 header.append('index %s..%s' %
2295 2310 (gitindex(content1), gitindex(content2)))
2296 2311 else:
2297 2312 text = mdiff.unidiff(content1, date1,
2298 2313 content2, date2,
2299 2314 path1, path2, opts=opts)
2300 2315 if header and (text or len(header) > 1):
2301 2316 yield '\n'.join(header) + '\n'
2302 2317 if text:
2303 2318 yield text
2304 2319
2305 2320 def diffstatsum(stats):
2306 2321 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2307 2322 for f, a, r, b in stats:
2308 2323 maxfile = max(maxfile, encoding.colwidth(f))
2309 2324 maxtotal = max(maxtotal, a + r)
2310 2325 addtotal += a
2311 2326 removetotal += r
2312 2327 binary = binary or b
2313 2328
2314 2329 return maxfile, maxtotal, addtotal, removetotal, binary
2315 2330
2316 2331 def diffstatdata(lines):
2317 2332 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2318 2333
2319 2334 results = []
2320 2335 filename, adds, removes, isbinary = None, 0, 0, False
2321 2336
2322 2337 def addresult():
2323 2338 if filename:
2324 2339 results.append((filename, adds, removes, isbinary))
2325 2340
2326 2341 for line in lines:
2327 2342 if line.startswith('diff'):
2328 2343 addresult()
2329 2344 # set numbers to 0 anyway when starting new file
2330 2345 adds, removes, isbinary = 0, 0, False
2331 2346 if line.startswith('diff --git a/'):
2332 2347 filename = gitre.search(line).group(2)
2333 2348 elif line.startswith('diff -r'):
2334 2349 # format: "diff -r ... -r ... filename"
2335 2350 filename = diffre.search(line).group(1)
2336 2351 elif line.startswith('+') and not line.startswith('+++ '):
2337 2352 adds += 1
2338 2353 elif line.startswith('-') and not line.startswith('--- '):
2339 2354 removes += 1
2340 2355 elif (line.startswith('GIT binary patch') or
2341 2356 line.startswith('Binary file')):
2342 2357 isbinary = True
2343 2358 addresult()
2344 2359 return results
2345 2360
2346 2361 def diffstat(lines, width=80, git=False):
2347 2362 output = []
2348 2363 stats = diffstatdata(lines)
2349 2364 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2350 2365
2351 2366 countwidth = len(str(maxtotal))
2352 2367 if hasbinary and countwidth < 3:
2353 2368 countwidth = 3
2354 2369 graphwidth = width - countwidth - maxname - 6
2355 2370 if graphwidth < 10:
2356 2371 graphwidth = 10
2357 2372
2358 2373 def scale(i):
2359 2374 if maxtotal <= graphwidth:
2360 2375 return i
2361 2376 # If diffstat runs out of room it doesn't print anything,
2362 2377 # which isn't very useful, so always print at least one + or -
2363 2378 # if there were at least some changes.
2364 2379 return max(i * graphwidth // maxtotal, int(bool(i)))
2365 2380
2366 2381 for filename, adds, removes, isbinary in stats:
2367 2382 if isbinary:
2368 2383 count = 'Bin'
2369 2384 else:
2370 2385 count = adds + removes
2371 2386 pluses = '+' * scale(adds)
2372 2387 minuses = '-' * scale(removes)
2373 2388 output.append(' %s%s | %*s %s%s\n' %
2374 2389 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2375 2390 countwidth, count, pluses, minuses))
2376 2391
2377 2392 if stats:
2378 2393 output.append(_(' %d files changed, %d insertions(+), '
2379 2394 '%d deletions(-)\n')
2380 2395 % (len(stats), totaladds, totalremoves))
2381 2396
2382 2397 return ''.join(output)
2383 2398
2384 2399 def diffstatui(*args, **kw):
2385 2400 '''like diffstat(), but yields 2-tuples of (output, label) for
2386 2401 ui.write()
2387 2402 '''
2388 2403
2389 2404 for line in diffstat(*args, **kw).splitlines():
2390 2405 if line and line[-1] in '+-':
2391 2406 name, graph = line.rsplit(' ', 1)
2392 2407 yield (name + ' ', '')
2393 2408 m = re.search(r'\++', graph)
2394 2409 if m:
2395 2410 yield (m.group(0), 'diffstat.inserted')
2396 2411 m = re.search(r'-+', graph)
2397 2412 if m:
2398 2413 yield (m.group(0), 'diffstat.deleted')
2399 2414 else:
2400 2415 yield (line, '')
2401 2416 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now