##// END OF EJS Templates
patch: fix patchmeta/hunk synchronization in iterhunks()...
Patrick Mezard -
r14534:ecc79816 default
parent child Browse files
Show More
@@ -1,1784 +1,1785 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.Parser, os, errno, re
10 10 import tempfile, zlib, shutil
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
15 15
16 16 gitre = re.compile('diff --git a/(.*) b/(.*)')
17 17
18 18 class PatchError(Exception):
19 19 pass
20 20
21 21
22 22 # public functions
23 23
24 24 def split(stream):
25 25 '''return an iterator of individual patches from a stream'''
26 26 def isheader(line, inheader):
27 27 if inheader and line[0] in (' ', '\t'):
28 28 # continuation
29 29 return True
30 30 if line[0] in (' ', '-', '+'):
31 31 # diff line - don't check for header pattern in there
32 32 return False
33 33 l = line.split(': ', 1)
34 34 return len(l) == 2 and ' ' not in l[0]
35 35
36 36 def chunk(lines):
37 37 return cStringIO.StringIO(''.join(lines))
38 38
39 39 def hgsplit(stream, cur):
40 40 inheader = True
41 41
42 42 for line in stream:
43 43 if not line.strip():
44 44 inheader = False
45 45 if not inheader and line.startswith('# HG changeset patch'):
46 46 yield chunk(cur)
47 47 cur = []
48 48 inheader = True
49 49
50 50 cur.append(line)
51 51
52 52 if cur:
53 53 yield chunk(cur)
54 54
55 55 def mboxsplit(stream, cur):
56 56 for line in stream:
57 57 if line.startswith('From '):
58 58 for c in split(chunk(cur[1:])):
59 59 yield c
60 60 cur = []
61 61
62 62 cur.append(line)
63 63
64 64 if cur:
65 65 for c in split(chunk(cur[1:])):
66 66 yield c
67 67
68 68 def mimesplit(stream, cur):
69 69 def msgfp(m):
70 70 fp = cStringIO.StringIO()
71 71 g = email.Generator.Generator(fp, mangle_from_=False)
72 72 g.flatten(m)
73 73 fp.seek(0)
74 74 return fp
75 75
76 76 for line in stream:
77 77 cur.append(line)
78 78 c = chunk(cur)
79 79
80 80 m = email.Parser.Parser().parse(c)
81 81 if not m.is_multipart():
82 82 yield msgfp(m)
83 83 else:
84 84 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
85 85 for part in m.walk():
86 86 ct = part.get_content_type()
87 87 if ct not in ok_types:
88 88 continue
89 89 yield msgfp(part)
90 90
91 91 def headersplit(stream, cur):
92 92 inheader = False
93 93
94 94 for line in stream:
95 95 if not inheader and isheader(line, inheader):
96 96 yield chunk(cur)
97 97 cur = []
98 98 inheader = True
99 99 if inheader and not isheader(line, inheader):
100 100 inheader = False
101 101
102 102 cur.append(line)
103 103
104 104 if cur:
105 105 yield chunk(cur)
106 106
107 107 def remainder(cur):
108 108 yield chunk(cur)
109 109
110 110 class fiter(object):
111 111 def __init__(self, fp):
112 112 self.fp = fp
113 113
114 114 def __iter__(self):
115 115 return self
116 116
117 117 def next(self):
118 118 l = self.fp.readline()
119 119 if not l:
120 120 raise StopIteration
121 121 return l
122 122
123 123 inheader = False
124 124 cur = []
125 125
126 126 mimeheaders = ['content-type']
127 127
128 128 if not hasattr(stream, 'next'):
129 129 # http responses, for example, have readline but not next
130 130 stream = fiter(stream)
131 131
132 132 for line in stream:
133 133 cur.append(line)
134 134 if line.startswith('# HG changeset patch'):
135 135 return hgsplit(stream, cur)
136 136 elif line.startswith('From '):
137 137 return mboxsplit(stream, cur)
138 138 elif isheader(line, inheader):
139 139 inheader = True
140 140 if line.split(':', 1)[0].lower() in mimeheaders:
141 141 # let email parser handle this
142 142 return mimesplit(stream, cur)
143 143 elif line.startswith('--- ') and inheader:
144 144 # No evil headers seen by diff start, split by hand
145 145 return headersplit(stream, cur)
146 146 # Not enough info, keep reading
147 147
148 148 # if we are here, we have a very plain patch
149 149 return remainder(cur)
150 150
151 151 def extract(ui, fileobj):
152 152 '''extract patch from data read from fileobj.
153 153
154 154 patch can be a normal patch or contained in an email message.
155 155
156 156 return tuple (filename, message, user, date, branch, node, p1, p2).
157 157 Any item in the returned tuple can be None. If filename is None,
158 158 fileobj did not contain a patch. Caller must unlink filename when done.'''
159 159
160 160 # attempt to detect the start of a patch
161 161 # (this heuristic is borrowed from quilt)
162 162 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
163 163 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
164 164 r'---[ \t].*?^\+\+\+[ \t]|'
165 165 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
166 166
167 167 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
168 168 tmpfp = os.fdopen(fd, 'w')
169 169 try:
170 170 msg = email.Parser.Parser().parse(fileobj)
171 171
172 172 subject = msg['Subject']
173 173 user = msg['From']
174 174 if not subject and not user:
175 175 # Not an email, restore parsed headers if any
176 176 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
177 177
178 178 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
179 179 # should try to parse msg['Date']
180 180 date = None
181 181 nodeid = None
182 182 branch = None
183 183 parents = []
184 184
185 185 if subject:
186 186 if subject.startswith('[PATCH'):
187 187 pend = subject.find(']')
188 188 if pend >= 0:
189 189 subject = subject[pend + 1:].lstrip()
190 190 subject = subject.replace('\n\t', ' ')
191 191 ui.debug('Subject: %s\n' % subject)
192 192 if user:
193 193 ui.debug('From: %s\n' % user)
194 194 diffs_seen = 0
195 195 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
196 196 message = ''
197 197 for part in msg.walk():
198 198 content_type = part.get_content_type()
199 199 ui.debug('Content-Type: %s\n' % content_type)
200 200 if content_type not in ok_types:
201 201 continue
202 202 payload = part.get_payload(decode=True)
203 203 m = diffre.search(payload)
204 204 if m:
205 205 hgpatch = False
206 206 hgpatchheader = False
207 207 ignoretext = False
208 208
209 209 ui.debug('found patch at byte %d\n' % m.start(0))
210 210 diffs_seen += 1
211 211 cfp = cStringIO.StringIO()
212 212 for line in payload[:m.start(0)].splitlines():
213 213 if line.startswith('# HG changeset patch') and not hgpatch:
214 214 ui.debug('patch generated by hg export\n')
215 215 hgpatch = True
216 216 hgpatchheader = True
217 217 # drop earlier commit message content
218 218 cfp.seek(0)
219 219 cfp.truncate()
220 220 subject = None
221 221 elif hgpatchheader:
222 222 if line.startswith('# User '):
223 223 user = line[7:]
224 224 ui.debug('From: %s\n' % user)
225 225 elif line.startswith("# Date "):
226 226 date = line[7:]
227 227 elif line.startswith("# Branch "):
228 228 branch = line[9:]
229 229 elif line.startswith("# Node ID "):
230 230 nodeid = line[10:]
231 231 elif line.startswith("# Parent "):
232 232 parents.append(line[10:])
233 233 elif not line.startswith("# "):
234 234 hgpatchheader = False
235 235 elif line == '---' and gitsendmail:
236 236 ignoretext = True
237 237 if not hgpatchheader and not ignoretext:
238 238 cfp.write(line)
239 239 cfp.write('\n')
240 240 message = cfp.getvalue()
241 241 if tmpfp:
242 242 tmpfp.write(payload)
243 243 if not payload.endswith('\n'):
244 244 tmpfp.write('\n')
245 245 elif not diffs_seen and message and content_type == 'text/plain':
246 246 message += '\n' + payload
247 247 except:
248 248 tmpfp.close()
249 249 os.unlink(tmpname)
250 250 raise
251 251
252 252 if subject and not message.startswith(subject):
253 253 message = '%s\n%s' % (subject, message)
254 254 tmpfp.close()
255 255 if not diffs_seen:
256 256 os.unlink(tmpname)
257 257 return None, message, user, date, branch, None, None, None
258 258 p1 = parents and parents.pop(0) or None
259 259 p2 = parents and parents.pop(0) or None
260 260 return tmpname, message, user, date, branch, nodeid, p1, p2
261 261
262 262 class patchmeta(object):
263 263 """Patched file metadata
264 264
265 265 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
266 266 or COPY. 'path' is patched file path. 'oldpath' is set to the
267 267 origin file when 'op' is either COPY or RENAME, None otherwise. If
268 268 file mode is changed, 'mode' is a tuple (islink, isexec) where
269 269 'islink' is True if the file is a symlink and 'isexec' is True if
270 270 the file is executable. Otherwise, 'mode' is None.
271 271 """
272 272 def __init__(self, path):
273 273 self.path = path
274 274 self.oldpath = None
275 275 self.mode = None
276 276 self.op = 'MODIFY'
277 277 self.binary = False
278 278
279 279 def setmode(self, mode):
280 280 islink = mode & 020000
281 281 isexec = mode & 0100
282 282 self.mode = (islink, isexec)
283 283
284 284 def __repr__(self):
285 285 return "<patchmeta %s %r>" % (self.op, self.path)
286 286
287 287 def readgitpatch(lr):
288 288 """extract git-style metadata about patches from <patchname>"""
289 289
290 290 # Filter patch for git information
291 291 gp = None
292 292 gitpatches = []
293 293 for line in lr:
294 294 line = line.rstrip(' \r\n')
295 295 if line.startswith('diff --git'):
296 296 m = gitre.match(line)
297 297 if m:
298 298 if gp:
299 299 gitpatches.append(gp)
300 300 dst = m.group(2)
301 301 gp = patchmeta(dst)
302 302 elif gp:
303 303 if line.startswith('--- '):
304 304 gitpatches.append(gp)
305 305 gp = None
306 306 continue
307 307 if line.startswith('rename from '):
308 308 gp.op = 'RENAME'
309 309 gp.oldpath = line[12:]
310 310 elif line.startswith('rename to '):
311 311 gp.path = line[10:]
312 312 elif line.startswith('copy from '):
313 313 gp.op = 'COPY'
314 314 gp.oldpath = line[10:]
315 315 elif line.startswith('copy to '):
316 316 gp.path = line[8:]
317 317 elif line.startswith('deleted file'):
318 318 gp.op = 'DELETE'
319 319 elif line.startswith('new file mode '):
320 320 gp.op = 'ADD'
321 321 gp.setmode(int(line[-6:], 8))
322 322 elif line.startswith('new mode '):
323 323 gp.setmode(int(line[-6:], 8))
324 324 elif line.startswith('GIT binary patch'):
325 325 gp.binary = True
326 326 if gp:
327 327 gitpatches.append(gp)
328 328
329 329 return gitpatches
330 330
331 331 class linereader(object):
332 332 # simple class to allow pushing lines back into the input stream
333 333 def __init__(self, fp):
334 334 self.fp = fp
335 335 self.buf = []
336 336
337 337 def push(self, line):
338 338 if line is not None:
339 339 self.buf.append(line)
340 340
341 341 def readline(self):
342 342 if self.buf:
343 343 l = self.buf[0]
344 344 del self.buf[0]
345 345 return l
346 346 return self.fp.readline()
347 347
348 348 def __iter__(self):
349 349 while True:
350 350 l = self.readline()
351 351 if not l:
352 352 break
353 353 yield l
354 354
355 355 class abstractbackend(object):
356 356 def __init__(self, ui):
357 357 self.ui = ui
358 358
359 359 def getfile(self, fname):
360 360 """Return target file data and flags as a (data, (islink,
361 361 isexec)) tuple.
362 362 """
363 363 raise NotImplementedError
364 364
365 365 def setfile(self, fname, data, mode, copysource):
366 366 """Write data to target file fname and set its mode. mode is a
367 367 (islink, isexec) tuple. If data is None, the file content should
368 368 be left unchanged. If the file is modified after being copied,
369 369 copysource is set to the original file name.
370 370 """
371 371 raise NotImplementedError
372 372
373 373 def unlink(self, fname):
374 374 """Unlink target file."""
375 375 raise NotImplementedError
376 376
377 377 def writerej(self, fname, failed, total, lines):
378 378 """Write rejected lines for fname. total is the number of hunks
379 379 which failed to apply and total the total number of hunks for this
380 380 files.
381 381 """
382 382 pass
383 383
384 384 def exists(self, fname):
385 385 raise NotImplementedError
386 386
387 387 class fsbackend(abstractbackend):
388 388 def __init__(self, ui, basedir):
389 389 super(fsbackend, self).__init__(ui)
390 390 self.opener = scmutil.opener(basedir)
391 391
392 392 def _join(self, f):
393 393 return os.path.join(self.opener.base, f)
394 394
395 395 def getfile(self, fname):
396 396 path = self._join(fname)
397 397 if os.path.islink(path):
398 398 return (os.readlink(path), (True, False))
399 399 isexec = False
400 400 try:
401 401 isexec = os.lstat(path).st_mode & 0100 != 0
402 402 except OSError, e:
403 403 if e.errno != errno.ENOENT:
404 404 raise
405 405 return (self.opener.read(fname), (False, isexec))
406 406
407 407 def setfile(self, fname, data, mode, copysource):
408 408 islink, isexec = mode
409 409 if data is None:
410 410 util.setflags(self._join(fname), islink, isexec)
411 411 return
412 412 if islink:
413 413 self.opener.symlink(data, fname)
414 414 else:
415 415 self.opener.write(fname, data)
416 416 if isexec:
417 417 util.setflags(self._join(fname), False, True)
418 418
419 419 def unlink(self, fname):
420 420 try:
421 421 util.unlinkpath(self._join(fname))
422 422 except OSError, inst:
423 423 if inst.errno != errno.ENOENT:
424 424 raise
425 425
426 426 def writerej(self, fname, failed, total, lines):
427 427 fname = fname + ".rej"
428 428 self.ui.warn(
429 429 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
430 430 (failed, total, fname))
431 431 fp = self.opener(fname, 'w')
432 432 fp.writelines(lines)
433 433 fp.close()
434 434
435 435 def exists(self, fname):
436 436 return os.path.lexists(self._join(fname))
437 437
438 438 class workingbackend(fsbackend):
439 439 def __init__(self, ui, repo, similarity):
440 440 super(workingbackend, self).__init__(ui, repo.root)
441 441 self.repo = repo
442 442 self.similarity = similarity
443 443 self.removed = set()
444 444 self.changed = set()
445 445 self.copied = []
446 446
447 447 def _checkknown(self, fname):
448 448 if self.repo.dirstate[fname] == '?' and self.exists(fname):
449 449 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
450 450
451 451 def setfile(self, fname, data, mode, copysource):
452 452 self._checkknown(fname)
453 453 super(workingbackend, self).setfile(fname, data, mode, copysource)
454 454 if copysource is not None:
455 455 self.copied.append((copysource, fname))
456 456 self.changed.add(fname)
457 457
458 458 def unlink(self, fname):
459 459 self._checkknown(fname)
460 460 super(workingbackend, self).unlink(fname)
461 461 self.removed.add(fname)
462 462 self.changed.add(fname)
463 463
464 464 def close(self):
465 465 wctx = self.repo[None]
466 466 addremoved = set(self.changed)
467 467 for src, dst in self.copied:
468 468 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
469 469 addremoved.discard(src)
470 470 if (not self.similarity) and self.removed:
471 471 wctx.forget(sorted(self.removed))
472 472 if addremoved:
473 473 cwd = self.repo.getcwd()
474 474 if cwd:
475 475 addremoved = [util.pathto(self.repo.root, cwd, f)
476 476 for f in addremoved]
477 477 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
478 478 return sorted(self.changed)
479 479
480 480 class filestore(object):
481 481 def __init__(self):
482 482 self.opener = None
483 483 self.files = {}
484 484 self.created = 0
485 485
486 486 def setfile(self, fname, data, mode):
487 487 if self.opener is None:
488 488 root = tempfile.mkdtemp(prefix='hg-patch-')
489 489 self.opener = scmutil.opener(root)
490 490 # Avoid filename issues with these simple names
491 491 fn = str(self.created)
492 492 self.opener.write(fn, data)
493 493 self.created += 1
494 494 self.files[fname] = (fn, mode)
495 495
496 496 def getfile(self, fname):
497 497 if fname not in self.files:
498 498 raise IOError()
499 499 fn, mode = self.files[fname]
500 500 return self.opener.read(fn), mode
501 501
502 502 def close(self):
503 503 if self.opener:
504 504 shutil.rmtree(self.opener.base)
505 505
506 506 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
507 507 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
508 508 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
509 509 eolmodes = ['strict', 'crlf', 'lf', 'auto']
510 510
511 511 class patchfile(object):
512 512 def __init__(self, ui, fname, backend, store, mode, create, remove,
513 513 eolmode='strict', copysource=None):
514 514 self.fname = fname
515 515 self.eolmode = eolmode
516 516 self.eol = None
517 517 self.backend = backend
518 518 self.ui = ui
519 519 self.lines = []
520 520 self.exists = False
521 521 self.missing = True
522 522 self.mode = mode
523 523 self.copysource = copysource
524 524 self.create = create
525 525 self.remove = remove
526 526 try:
527 527 if copysource is None:
528 528 data, mode = backend.getfile(fname)
529 529 self.exists = True
530 530 else:
531 531 data, mode = store.getfile(copysource)
532 532 self.exists = backend.exists(fname)
533 533 self.missing = False
534 534 if data:
535 535 self.lines = data.splitlines(True)
536 536 if self.mode is None:
537 537 self.mode = mode
538 538 if self.lines:
539 539 # Normalize line endings
540 540 if self.lines[0].endswith('\r\n'):
541 541 self.eol = '\r\n'
542 542 elif self.lines[0].endswith('\n'):
543 543 self.eol = '\n'
544 544 if eolmode != 'strict':
545 545 nlines = []
546 546 for l in self.lines:
547 547 if l.endswith('\r\n'):
548 548 l = l[:-2] + '\n'
549 549 nlines.append(l)
550 550 self.lines = nlines
551 551 except IOError:
552 552 if create:
553 553 self.missing = False
554 554 if self.mode is None:
555 555 self.mode = (False, False)
556 556 if self.missing:
557 557 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
558 558
559 559 self.hash = {}
560 560 self.dirty = 0
561 561 self.offset = 0
562 562 self.skew = 0
563 563 self.rej = []
564 564 self.fileprinted = False
565 565 self.printfile(False)
566 566 self.hunks = 0
567 567
568 568 def writelines(self, fname, lines, mode):
569 569 if self.eolmode == 'auto':
570 570 eol = self.eol
571 571 elif self.eolmode == 'crlf':
572 572 eol = '\r\n'
573 573 else:
574 574 eol = '\n'
575 575
576 576 if self.eolmode != 'strict' and eol and eol != '\n':
577 577 rawlines = []
578 578 for l in lines:
579 579 if l and l[-1] == '\n':
580 580 l = l[:-1] + eol
581 581 rawlines.append(l)
582 582 lines = rawlines
583 583
584 584 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
585 585
586 586 def printfile(self, warn):
587 587 if self.fileprinted:
588 588 return
589 589 if warn or self.ui.verbose:
590 590 self.fileprinted = True
591 591 s = _("patching file %s\n") % self.fname
592 592 if warn:
593 593 self.ui.warn(s)
594 594 else:
595 595 self.ui.note(s)
596 596
597 597
598 598 def findlines(self, l, linenum):
599 599 # looks through the hash and finds candidate lines. The
600 600 # result is a list of line numbers sorted based on distance
601 601 # from linenum
602 602
603 603 cand = self.hash.get(l, [])
604 604 if len(cand) > 1:
605 605 # resort our list of potentials forward then back.
606 606 cand.sort(key=lambda x: abs(x - linenum))
607 607 return cand
608 608
609 609 def write_rej(self):
610 610 # our rejects are a little different from patch(1). This always
611 611 # creates rejects in the same form as the original patch. A file
612 612 # header is inserted so that you can run the reject through patch again
613 613 # without having to type the filename.
614 614 if not self.rej:
615 615 return
616 616 base = os.path.basename(self.fname)
617 617 lines = ["--- %s\n+++ %s\n" % (base, base)]
618 618 for x in self.rej:
619 619 for l in x.hunk:
620 620 lines.append(l)
621 621 if l[-1] != '\n':
622 622 lines.append("\n\ No newline at end of file\n")
623 623 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
624 624
625 625 def apply(self, h):
626 626 if not h.complete():
627 627 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
628 628 (h.number, h.desc, len(h.a), h.lena, len(h.b),
629 629 h.lenb))
630 630
631 631 self.hunks += 1
632 632
633 633 if self.missing:
634 634 self.rej.append(h)
635 635 return -1
636 636
637 637 if self.exists and self.create:
638 638 if self.copysource:
639 639 self.ui.warn(_("cannot create %s: destination already "
640 640 "exists\n" % self.fname))
641 641 else:
642 642 self.ui.warn(_("file %s already exists\n") % self.fname)
643 643 self.rej.append(h)
644 644 return -1
645 645
646 646 if isinstance(h, binhunk):
647 647 if self.remove:
648 648 self.backend.unlink(self.fname)
649 649 else:
650 650 self.lines[:] = h.new()
651 651 self.offset += len(h.new())
652 652 self.dirty = True
653 653 return 0
654 654
655 655 horig = h
656 656 if (self.eolmode in ('crlf', 'lf')
657 657 or self.eolmode == 'auto' and self.eol):
658 658 # If new eols are going to be normalized, then normalize
659 659 # hunk data before patching. Otherwise, preserve input
660 660 # line-endings.
661 661 h = h.getnormalized()
662 662
663 663 # fast case first, no offsets, no fuzz
664 664 old = h.old()
665 665 # patch starts counting at 1 unless we are adding the file
666 666 if h.starta == 0:
667 667 start = 0
668 668 else:
669 669 start = h.starta + self.offset - 1
670 670 orig_start = start
671 671 # if there's skew we want to emit the "(offset %d lines)" even
672 672 # when the hunk cleanly applies at start + skew, so skip the
673 673 # fast case code
674 674 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
675 675 if self.remove:
676 676 self.backend.unlink(self.fname)
677 677 else:
678 678 self.lines[start : start + h.lena] = h.new()
679 679 self.offset += h.lenb - h.lena
680 680 self.dirty = True
681 681 return 0
682 682
683 683 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
684 684 self.hash = {}
685 685 for x, s in enumerate(self.lines):
686 686 self.hash.setdefault(s, []).append(x)
687 687 if h.hunk[-1][0] != ' ':
688 688 # if the hunk tried to put something at the bottom of the file
689 689 # override the start line and use eof here
690 690 search_start = len(self.lines)
691 691 else:
692 692 search_start = orig_start + self.skew
693 693
694 694 for fuzzlen in xrange(3):
695 695 for toponly in [True, False]:
696 696 old = h.old(fuzzlen, toponly)
697 697
698 698 cand = self.findlines(old[0][1:], search_start)
699 699 for l in cand:
700 700 if diffhelpers.testhunk(old, self.lines, l) == 0:
701 701 newlines = h.new(fuzzlen, toponly)
702 702 self.lines[l : l + len(old)] = newlines
703 703 self.offset += len(newlines) - len(old)
704 704 self.skew = l - orig_start
705 705 self.dirty = True
706 706 offset = l - orig_start - fuzzlen
707 707 if fuzzlen:
708 708 msg = _("Hunk #%d succeeded at %d "
709 709 "with fuzz %d "
710 710 "(offset %d lines).\n")
711 711 self.printfile(True)
712 712 self.ui.warn(msg %
713 713 (h.number, l + 1, fuzzlen, offset))
714 714 else:
715 715 msg = _("Hunk #%d succeeded at %d "
716 716 "(offset %d lines).\n")
717 717 self.ui.note(msg % (h.number, l + 1, offset))
718 718 return fuzzlen
719 719 self.printfile(True)
720 720 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
721 721 self.rej.append(horig)
722 722 return -1
723 723
724 724 def close(self):
725 725 if self.dirty:
726 726 self.writelines(self.fname, self.lines, self.mode)
727 727 self.write_rej()
728 728 return len(self.rej)
729 729
730 730 class hunk(object):
731 731 def __init__(self, desc, num, lr, context):
732 732 self.number = num
733 733 self.desc = desc
734 734 self.hunk = [desc]
735 735 self.a = []
736 736 self.b = []
737 737 self.starta = self.lena = None
738 738 self.startb = self.lenb = None
739 739 if lr is not None:
740 740 if context:
741 741 self.read_context_hunk(lr)
742 742 else:
743 743 self.read_unified_hunk(lr)
744 744
745 745 def getnormalized(self):
746 746 """Return a copy with line endings normalized to LF."""
747 747
748 748 def normalize(lines):
749 749 nlines = []
750 750 for line in lines:
751 751 if line.endswith('\r\n'):
752 752 line = line[:-2] + '\n'
753 753 nlines.append(line)
754 754 return nlines
755 755
756 756 # Dummy object, it is rebuilt manually
757 757 nh = hunk(self.desc, self.number, None, None)
758 758 nh.number = self.number
759 759 nh.desc = self.desc
760 760 nh.hunk = self.hunk
761 761 nh.a = normalize(self.a)
762 762 nh.b = normalize(self.b)
763 763 nh.starta = self.starta
764 764 nh.startb = self.startb
765 765 nh.lena = self.lena
766 766 nh.lenb = self.lenb
767 767 return nh
768 768
769 769 def read_unified_hunk(self, lr):
770 770 m = unidesc.match(self.desc)
771 771 if not m:
772 772 raise PatchError(_("bad hunk #%d") % self.number)
773 773 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
774 774 if self.lena is None:
775 775 self.lena = 1
776 776 else:
777 777 self.lena = int(self.lena)
778 778 if self.lenb is None:
779 779 self.lenb = 1
780 780 else:
781 781 self.lenb = int(self.lenb)
782 782 self.starta = int(self.starta)
783 783 self.startb = int(self.startb)
784 784 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
785 785 # if we hit eof before finishing out the hunk, the last line will
786 786 # be zero length. Lets try to fix it up.
787 787 while len(self.hunk[-1]) == 0:
788 788 del self.hunk[-1]
789 789 del self.a[-1]
790 790 del self.b[-1]
791 791 self.lena -= 1
792 792 self.lenb -= 1
793 793 self._fixnewline(lr)
794 794
795 795 def read_context_hunk(self, lr):
796 796 self.desc = lr.readline()
797 797 m = contextdesc.match(self.desc)
798 798 if not m:
799 799 raise PatchError(_("bad hunk #%d") % self.number)
800 800 foo, self.starta, foo2, aend, foo3 = m.groups()
801 801 self.starta = int(self.starta)
802 802 if aend is None:
803 803 aend = self.starta
804 804 self.lena = int(aend) - self.starta
805 805 if self.starta:
806 806 self.lena += 1
807 807 for x in xrange(self.lena):
808 808 l = lr.readline()
809 809 if l.startswith('---'):
810 810 # lines addition, old block is empty
811 811 lr.push(l)
812 812 break
813 813 s = l[2:]
814 814 if l.startswith('- ') or l.startswith('! '):
815 815 u = '-' + s
816 816 elif l.startswith(' '):
817 817 u = ' ' + s
818 818 else:
819 819 raise PatchError(_("bad hunk #%d old text line %d") %
820 820 (self.number, x))
821 821 self.a.append(u)
822 822 self.hunk.append(u)
823 823
824 824 l = lr.readline()
825 825 if l.startswith('\ '):
826 826 s = self.a[-1][:-1]
827 827 self.a[-1] = s
828 828 self.hunk[-1] = s
829 829 l = lr.readline()
830 830 m = contextdesc.match(l)
831 831 if not m:
832 832 raise PatchError(_("bad hunk #%d") % self.number)
833 833 foo, self.startb, foo2, bend, foo3 = m.groups()
834 834 self.startb = int(self.startb)
835 835 if bend is None:
836 836 bend = self.startb
837 837 self.lenb = int(bend) - self.startb
838 838 if self.startb:
839 839 self.lenb += 1
840 840 hunki = 1
841 841 for x in xrange(self.lenb):
842 842 l = lr.readline()
843 843 if l.startswith('\ '):
844 844 # XXX: the only way to hit this is with an invalid line range.
845 845 # The no-eol marker is not counted in the line range, but I
846 846 # guess there are diff(1) out there which behave differently.
847 847 s = self.b[-1][:-1]
848 848 self.b[-1] = s
849 849 self.hunk[hunki - 1] = s
850 850 continue
851 851 if not l:
852 852 # line deletions, new block is empty and we hit EOF
853 853 lr.push(l)
854 854 break
855 855 s = l[2:]
856 856 if l.startswith('+ ') or l.startswith('! '):
857 857 u = '+' + s
858 858 elif l.startswith(' '):
859 859 u = ' ' + s
860 860 elif len(self.b) == 0:
861 861 # line deletions, new block is empty
862 862 lr.push(l)
863 863 break
864 864 else:
865 865 raise PatchError(_("bad hunk #%d old text line %d") %
866 866 (self.number, x))
867 867 self.b.append(s)
868 868 while True:
869 869 if hunki >= len(self.hunk):
870 870 h = ""
871 871 else:
872 872 h = self.hunk[hunki]
873 873 hunki += 1
874 874 if h == u:
875 875 break
876 876 elif h.startswith('-'):
877 877 continue
878 878 else:
879 879 self.hunk.insert(hunki - 1, u)
880 880 break
881 881
882 882 if not self.a:
883 883 # this happens when lines were only added to the hunk
884 884 for x in self.hunk:
885 885 if x.startswith('-') or x.startswith(' '):
886 886 self.a.append(x)
887 887 if not self.b:
888 888 # this happens when lines were only deleted from the hunk
889 889 for x in self.hunk:
890 890 if x.startswith('+') or x.startswith(' '):
891 891 self.b.append(x[1:])
892 892 # @@ -start,len +start,len @@
893 893 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
894 894 self.startb, self.lenb)
895 895 self.hunk[0] = self.desc
896 896 self._fixnewline(lr)
897 897
898 898 def _fixnewline(self, lr):
899 899 l = lr.readline()
900 900 if l.startswith('\ '):
901 901 diffhelpers.fix_newline(self.hunk, self.a, self.b)
902 902 else:
903 903 lr.push(l)
904 904
905 905 def complete(self):
906 906 return len(self.a) == self.lena and len(self.b) == self.lenb
907 907
908 908 def fuzzit(self, l, fuzz, toponly):
909 909 # this removes context lines from the top and bottom of list 'l'. It
910 910 # checks the hunk to make sure only context lines are removed, and then
911 911 # returns a new shortened list of lines.
912 912 fuzz = min(fuzz, len(l)-1)
913 913 if fuzz:
914 914 top = 0
915 915 bot = 0
916 916 hlen = len(self.hunk)
917 917 for x in xrange(hlen - 1):
918 918 # the hunk starts with the @@ line, so use x+1
919 919 if self.hunk[x + 1][0] == ' ':
920 920 top += 1
921 921 else:
922 922 break
923 923 if not toponly:
924 924 for x in xrange(hlen - 1):
925 925 if self.hunk[hlen - bot - 1][0] == ' ':
926 926 bot += 1
927 927 else:
928 928 break
929 929
930 930 # top and bot now count context in the hunk
931 931 # adjust them if either one is short
932 932 context = max(top, bot, 3)
933 933 if bot < context:
934 934 bot = max(0, fuzz - (context - bot))
935 935 else:
936 936 bot = min(fuzz, bot)
937 937 if top < context:
938 938 top = max(0, fuzz - (context - top))
939 939 else:
940 940 top = min(fuzz, top)
941 941
942 942 return l[top:len(l)-bot]
943 943 return l
944 944
945 945 def old(self, fuzz=0, toponly=False):
946 946 return self.fuzzit(self.a, fuzz, toponly)
947 947
948 948 def new(self, fuzz=0, toponly=False):
949 949 return self.fuzzit(self.b, fuzz, toponly)
950 950
951 951 class binhunk:
952 952 'A binary patch file. Only understands literals so far.'
953 953 def __init__(self, lr):
954 954 self.text = None
955 955 self.hunk = ['GIT binary patch\n']
956 956 self._read(lr)
957 957
958 958 def complete(self):
959 959 return self.text is not None
960 960
961 961 def new(self):
962 962 return [self.text]
963 963
964 964 def _read(self, lr):
965 965 line = lr.readline()
966 966 self.hunk.append(line)
967 967 while line and not line.startswith('literal '):
968 968 line = lr.readline()
969 969 self.hunk.append(line)
970 970 if not line:
971 971 raise PatchError(_('could not extract binary patch'))
972 972 size = int(line[8:].rstrip())
973 973 dec = []
974 974 line = lr.readline()
975 975 self.hunk.append(line)
976 976 while len(line) > 1:
977 977 l = line[0]
978 978 if l <= 'Z' and l >= 'A':
979 979 l = ord(l) - ord('A') + 1
980 980 else:
981 981 l = ord(l) - ord('a') + 27
982 982 dec.append(base85.b85decode(line[1:-1])[:l])
983 983 line = lr.readline()
984 984 self.hunk.append(line)
985 985 text = zlib.decompress(''.join(dec))
986 986 if len(text) != size:
987 987 raise PatchError(_('binary patch is %d bytes, not %d') %
988 988 len(text), size)
989 989 self.text = text
990 990
991 991 def parsefilename(str):
992 992 # --- filename \t|space stuff
993 993 s = str[4:].rstrip('\r\n')
994 994 i = s.find('\t')
995 995 if i < 0:
996 996 i = s.find(' ')
997 997 if i < 0:
998 998 return s
999 999 return s[:i]
1000 1000
1001 1001 def pathstrip(path, strip):
1002 1002 pathlen = len(path)
1003 1003 i = 0
1004 1004 if strip == 0:
1005 1005 return '', path.rstrip()
1006 1006 count = strip
1007 1007 while count > 0:
1008 1008 i = path.find('/', i)
1009 1009 if i == -1:
1010 1010 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1011 1011 (count, strip, path))
1012 1012 i += 1
1013 1013 # consume '//' in the path
1014 1014 while i < pathlen - 1 and path[i] == '/':
1015 1015 i += 1
1016 1016 count -= 1
1017 1017 return path[:i].lstrip(), path[i:].rstrip()
1018 1018
1019 1019 def selectfile(backend, afile_orig, bfile_orig, hunk, strip, gp):
1020 1020 if gp:
1021 1021 # Git patches do not play games. Excluding copies from the
1022 1022 # following heuristic avoids a lot of confusion
1023 1023 fname = pathstrip(gp.path, strip - 1)[1]
1024 1024 create = gp.op in ('ADD', 'COPY', 'RENAME')
1025 1025 remove = gp.op == 'DELETE'
1026 1026 return fname, create, remove
1027 1027 nulla = afile_orig == "/dev/null"
1028 1028 nullb = bfile_orig == "/dev/null"
1029 1029 create = nulla and hunk.starta == 0 and hunk.lena == 0
1030 1030 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1031 1031 abase, afile = pathstrip(afile_orig, strip)
1032 1032 gooda = not nulla and backend.exists(afile)
1033 1033 bbase, bfile = pathstrip(bfile_orig, strip)
1034 1034 if afile == bfile:
1035 1035 goodb = gooda
1036 1036 else:
1037 1037 goodb = not nullb and backend.exists(bfile)
1038 1038 missing = not goodb and not gooda and not create
1039 1039
1040 1040 # some diff programs apparently produce patches where the afile is
1041 1041 # not /dev/null, but afile starts with bfile
1042 1042 abasedir = afile[:afile.rfind('/') + 1]
1043 1043 bbasedir = bfile[:bfile.rfind('/') + 1]
1044 1044 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1045 1045 and hunk.starta == 0 and hunk.lena == 0):
1046 1046 create = True
1047 1047 missing = False
1048 1048
1049 1049 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1050 1050 # diff is between a file and its backup. In this case, the original
1051 1051 # file should be patched (see original mpatch code).
1052 1052 isbackup = (abase == bbase and bfile.startswith(afile))
1053 1053 fname = None
1054 1054 if not missing:
1055 1055 if gooda and goodb:
1056 1056 fname = isbackup and afile or bfile
1057 1057 elif gooda:
1058 1058 fname = afile
1059 1059
1060 1060 if not fname:
1061 1061 if not nullb:
1062 1062 fname = isbackup and afile or bfile
1063 1063 elif not nulla:
1064 1064 fname = afile
1065 1065 else:
1066 1066 raise PatchError(_("undefined source and destination files"))
1067 1067
1068 1068 return fname, create, remove
1069 1069
1070 1070 def scangitpatch(lr, firstline):
1071 1071 """
1072 1072 Git patches can emit:
1073 1073 - rename a to b
1074 1074 - change b
1075 1075 - copy a to c
1076 1076 - change c
1077 1077
1078 1078 We cannot apply this sequence as-is, the renamed 'a' could not be
1079 1079 found for it would have been renamed already. And we cannot copy
1080 1080 from 'b' instead because 'b' would have been changed already. So
1081 1081 we scan the git patch for copy and rename commands so we can
1082 1082 perform the copies ahead of time.
1083 1083 """
1084 1084 pos = 0
1085 1085 try:
1086 1086 pos = lr.fp.tell()
1087 1087 fp = lr.fp
1088 1088 except IOError:
1089 1089 fp = cStringIO.StringIO(lr.fp.read())
1090 1090 gitlr = linereader(fp)
1091 1091 gitlr.push(firstline)
1092 1092 gitpatches = readgitpatch(gitlr)
1093 1093 fp.seek(pos)
1094 1094 return gitpatches
1095 1095
1096 1096 def iterhunks(fp):
1097 1097 """Read a patch and yield the following events:
1098 1098 - ("file", afile, bfile, firsthunk): select a new target file.
1099 1099 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1100 1100 "file" event.
1101 1101 - ("git", gitchanges): current diff is in git format, gitchanges
1102 1102 maps filenames to gitpatch records. Unique event.
1103 1103 """
1104 1104 afile = ""
1105 1105 bfile = ""
1106 1106 state = None
1107 1107 hunknum = 0
1108 1108 emitfile = newfile = False
1109 1109 gitpatches = None
1110 1110
1111 1111 # our states
1112 1112 BFILE = 1
1113 1113 context = None
1114 1114 lr = linereader(fp)
1115 1115
1116 1116 while True:
1117 1117 x = lr.readline()
1118 1118 if not x:
1119 1119 break
1120 1120 if state == BFILE and (
1121 1121 (not context and x[0] == '@')
1122 1122 or (context is not False and x.startswith('***************'))
1123 1123 or x.startswith('GIT binary patch')):
1124 1124 gp = None
1125 if gitpatches and gitpatches[-1][0] == bfile:
1126 gp = gitpatches.pop()[1]
1125 if (gitpatches and
1126 (gitpatches[-1][0] == afile or gitpatches[-1][1] == bfile)):
1127 gp = gitpatches.pop()[2]
1127 1128 if x.startswith('GIT binary patch'):
1128 1129 h = binhunk(lr)
1129 1130 else:
1130 1131 if context is None and x.startswith('***************'):
1131 1132 context = True
1132 1133 h = hunk(x, hunknum + 1, lr, context)
1133 1134 hunknum += 1
1134 1135 if emitfile:
1135 1136 emitfile = False
1136 1137 yield 'file', (afile, bfile, h, gp)
1137 1138 yield 'hunk', h
1138 1139 elif x.startswith('diff --git'):
1139 1140 m = gitre.match(x)
1140 1141 if not m:
1141 1142 continue
1142 1143 if gitpatches is None:
1143 1144 # scan whole input for git metadata
1144 gitpatches = [('b/' + gp.path, gp) for gp
1145 gitpatches = [('a/' + gp.path, 'b/' + gp.path, gp) for gp
1145 1146 in scangitpatch(lr, x)]
1146 yield 'git', [g[1] for g in gitpatches
1147 if g[1].op in ('COPY', 'RENAME')]
1147 yield 'git', [g[2] for g in gitpatches
1148 if g[2].op in ('COPY', 'RENAME')]
1148 1149 gitpatches.reverse()
1149 1150 afile = 'a/' + m.group(1)
1150 1151 bfile = 'b/' + m.group(2)
1151 while bfile != gitpatches[-1][0]:
1152 gp = gitpatches.pop()[1]
1152 while afile != gitpatches[-1][0] and bfile != gitpatches[-1][1]:
1153 gp = gitpatches.pop()[2]
1153 1154 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1154 gp = gitpatches[-1][1]
1155 gp = gitpatches[-1][2]
1155 1156 # copy/rename + modify should modify target, not source
1156 1157 if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
1157 1158 afile = bfile
1158 1159 newfile = True
1159 1160 elif x.startswith('---'):
1160 1161 # check for a unified diff
1161 1162 l2 = lr.readline()
1162 1163 if not l2.startswith('+++'):
1163 1164 lr.push(l2)
1164 1165 continue
1165 1166 newfile = True
1166 1167 context = False
1167 1168 afile = parsefilename(x)
1168 1169 bfile = parsefilename(l2)
1169 1170 elif x.startswith('***'):
1170 1171 # check for a context diff
1171 1172 l2 = lr.readline()
1172 1173 if not l2.startswith('---'):
1173 1174 lr.push(l2)
1174 1175 continue
1175 1176 l3 = lr.readline()
1176 1177 lr.push(l3)
1177 1178 if not l3.startswith("***************"):
1178 1179 lr.push(l2)
1179 1180 continue
1180 1181 newfile = True
1181 1182 context = True
1182 1183 afile = parsefilename(x)
1183 1184 bfile = parsefilename(l2)
1184 1185
1185 1186 if newfile:
1186 1187 newfile = False
1187 1188 emitfile = True
1188 1189 state = BFILE
1189 1190 hunknum = 0
1190 1191
1191 1192 while gitpatches:
1192 gp = gitpatches.pop()[1]
1193 gp = gitpatches.pop()[2]
1193 1194 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1194 1195
1195 1196 def applydiff(ui, fp, changed, backend, store, strip=1, eolmode='strict'):
1196 1197 """Reads a patch from fp and tries to apply it.
1197 1198
1198 1199 The dict 'changed' is filled in with all of the filenames changed
1199 1200 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1200 1201 found and 1 if there was any fuzz.
1201 1202
1202 1203 If 'eolmode' is 'strict', the patch content and patched file are
1203 1204 read in binary mode. Otherwise, line endings are ignored when
1204 1205 patching then normalized according to 'eolmode'.
1205 1206 """
1206 1207 return _applydiff(ui, fp, patchfile, backend, store, changed, strip=strip,
1207 1208 eolmode=eolmode)
1208 1209
1209 1210 def _applydiff(ui, fp, patcher, backend, store, changed, strip=1,
1210 1211 eolmode='strict'):
1211 1212
1212 1213 def pstrip(p):
1213 1214 return pathstrip(p, strip - 1)[1]
1214 1215
1215 1216 rejects = 0
1216 1217 err = 0
1217 1218 current_file = None
1218 1219
1219 1220 for state, values in iterhunks(fp):
1220 1221 if state == 'hunk':
1221 1222 if not current_file:
1222 1223 continue
1223 1224 ret = current_file.apply(values)
1224 1225 if ret >= 0:
1225 1226 changed.setdefault(current_file.fname, None)
1226 1227 if ret > 0:
1227 1228 err = 1
1228 1229 elif state == 'file':
1229 1230 if current_file:
1230 1231 rejects += current_file.close()
1231 1232 current_file = None
1232 1233 afile, bfile, first_hunk, gp = values
1233 1234 copysource = None
1234 1235 if gp:
1235 1236 path = pstrip(gp.path)
1236 1237 if gp.oldpath:
1237 1238 copysource = pstrip(gp.oldpath)
1238 1239 changed[path] = gp
1239 1240 if gp.op == 'DELETE':
1240 1241 backend.unlink(path)
1241 1242 continue
1242 1243 if gp.op == 'RENAME':
1243 1244 backend.unlink(copysource)
1244 1245 if not first_hunk:
1245 1246 data, mode = None, None
1246 1247 if gp.op in ('RENAME', 'COPY'):
1247 1248 data, mode = store.getfile(copysource)
1248 1249 if gp.mode:
1249 1250 mode = gp.mode
1250 1251 if gp.op == 'ADD':
1251 1252 # Added files without content have no hunk and
1252 1253 # must be created
1253 1254 data = ''
1254 1255 if data or mode:
1255 1256 if (gp.op in ('ADD', 'RENAME', 'COPY')
1256 1257 and backend.exists(path)):
1257 1258 raise PatchError(_("cannot create %s: destination "
1258 1259 "already exists") % path)
1259 1260 backend.setfile(path, data, mode, copysource)
1260 1261 if not first_hunk:
1261 1262 continue
1262 1263 try:
1263 1264 mode = gp and gp.mode or None
1264 1265 current_file, create, remove = selectfile(
1265 1266 backend, afile, bfile, first_hunk, strip, gp)
1266 1267 current_file = patcher(ui, current_file, backend, store, mode,
1267 1268 create, remove, eolmode=eolmode,
1268 1269 copysource=copysource)
1269 1270 except PatchError, inst:
1270 1271 ui.warn(str(inst) + '\n')
1271 1272 current_file = None
1272 1273 rejects += 1
1273 1274 continue
1274 1275 elif state == 'git':
1275 1276 for gp in values:
1276 1277 path = pstrip(gp.oldpath)
1277 1278 data, mode = backend.getfile(path)
1278 1279 store.setfile(path, data, mode)
1279 1280 else:
1280 1281 raise util.Abort(_('unsupported parser state: %s') % state)
1281 1282
1282 1283 if current_file:
1283 1284 rejects += current_file.close()
1284 1285
1285 1286 if rejects:
1286 1287 return -1
1287 1288 return err
1288 1289
1289 1290 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1290 1291 similarity):
1291 1292 """use <patcher> to apply <patchname> to the working directory.
1292 1293 returns whether patch was applied with fuzz factor."""
1293 1294
1294 1295 fuzz = False
1295 1296 args = []
1296 1297 cwd = repo.root
1297 1298 if cwd:
1298 1299 args.append('-d %s' % util.shellquote(cwd))
1299 1300 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1300 1301 util.shellquote(patchname)))
1301 1302 try:
1302 1303 for line in fp:
1303 1304 line = line.rstrip()
1304 1305 ui.note(line + '\n')
1305 1306 if line.startswith('patching file '):
1306 1307 pf = util.parsepatchoutput(line)
1307 1308 printed_file = False
1308 1309 files.setdefault(pf, None)
1309 1310 elif line.find('with fuzz') >= 0:
1310 1311 fuzz = True
1311 1312 if not printed_file:
1312 1313 ui.warn(pf + '\n')
1313 1314 printed_file = True
1314 1315 ui.warn(line + '\n')
1315 1316 elif line.find('saving rejects to file') >= 0:
1316 1317 ui.warn(line + '\n')
1317 1318 elif line.find('FAILED') >= 0:
1318 1319 if not printed_file:
1319 1320 ui.warn(pf + '\n')
1320 1321 printed_file = True
1321 1322 ui.warn(line + '\n')
1322 1323 finally:
1323 1324 if files:
1324 1325 cfiles = list(files)
1325 1326 cwd = repo.getcwd()
1326 1327 if cwd:
1327 1328 cfiles = [util.pathto(repo.root, cwd, f)
1328 1329 for f in cfile]
1329 1330 scmutil.addremove(repo, cfiles, similarity=similarity)
1330 1331 code = fp.close()
1331 1332 if code:
1332 1333 raise PatchError(_("patch command failed: %s") %
1333 1334 util.explainexit(code)[0])
1334 1335 return fuzz
1335 1336
1336 1337 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1337 1338 similarity=0):
1338 1339 """use builtin patch to apply <patchobj> to the working directory.
1339 1340 returns whether patch was applied with fuzz factor."""
1340 1341
1341 1342 if files is None:
1342 1343 files = {}
1343 1344 if eolmode is None:
1344 1345 eolmode = ui.config('patch', 'eol', 'strict')
1345 1346 if eolmode.lower() not in eolmodes:
1346 1347 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1347 1348 eolmode = eolmode.lower()
1348 1349
1349 1350 store = filestore()
1350 1351 backend = workingbackend(ui, repo, similarity)
1351 1352 try:
1352 1353 fp = open(patchobj, 'rb')
1353 1354 except TypeError:
1354 1355 fp = patchobj
1355 1356 try:
1356 1357 ret = applydiff(ui, fp, files, backend, store, strip=strip,
1357 1358 eolmode=eolmode)
1358 1359 finally:
1359 1360 if fp != patchobj:
1360 1361 fp.close()
1361 1362 files.update(dict.fromkeys(backend.close()))
1362 1363 store.close()
1363 1364 if ret < 0:
1364 1365 raise PatchError(_('patch failed to apply'))
1365 1366 return ret > 0
1366 1367
1367 1368 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1368 1369 similarity=0):
1369 1370 """Apply <patchname> to the working directory.
1370 1371
1371 1372 'eolmode' specifies how end of lines should be handled. It can be:
1372 1373 - 'strict': inputs are read in binary mode, EOLs are preserved
1373 1374 - 'crlf': EOLs are ignored when patching and reset to CRLF
1374 1375 - 'lf': EOLs are ignored when patching and reset to LF
1375 1376 - None: get it from user settings, default to 'strict'
1376 1377 'eolmode' is ignored when using an external patcher program.
1377 1378
1378 1379 Returns whether patch was applied with fuzz factor.
1379 1380 """
1380 1381 patcher = ui.config('ui', 'patch')
1381 1382 if files is None:
1382 1383 files = {}
1383 1384 try:
1384 1385 if patcher:
1385 1386 return _externalpatch(ui, repo, patcher, patchname, strip,
1386 1387 files, similarity)
1387 1388 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1388 1389 similarity)
1389 1390 except PatchError, err:
1390 1391 raise util.Abort(str(err))
1391 1392
1392 1393 def changedfiles(ui, repo, patchpath, strip=1):
1393 1394 backend = fsbackend(ui, repo.root)
1394 1395 fp = open(patchpath, 'rb')
1395 1396 try:
1396 1397 changed = set()
1397 1398 for state, values in iterhunks(fp):
1398 1399 if state == 'file':
1399 1400 afile, bfile, first_hunk, gp = values
1400 1401 if gp:
1401 1402 changed.add(pathstrip(gp.path, strip - 1)[1])
1402 1403 if gp.op == 'RENAME':
1403 1404 changed.add(pathstrip(gp.oldpath, strip - 1)[1])
1404 1405 if not first_hunk:
1405 1406 continue
1406 1407 current_file, create, remove = selectfile(
1407 1408 backend, afile, bfile, first_hunk, strip, gp)
1408 1409 changed.add(current_file)
1409 1410 elif state not in ('hunk', 'git'):
1410 1411 raise util.Abort(_('unsupported parser state: %s') % state)
1411 1412 return changed
1412 1413 finally:
1413 1414 fp.close()
1414 1415
1415 1416 def b85diff(to, tn):
1416 1417 '''print base85-encoded binary diff'''
1417 1418 def gitindex(text):
1418 1419 if not text:
1419 1420 return hex(nullid)
1420 1421 l = len(text)
1421 1422 s = util.sha1('blob %d\0' % l)
1422 1423 s.update(text)
1423 1424 return s.hexdigest()
1424 1425
1425 1426 def fmtline(line):
1426 1427 l = len(line)
1427 1428 if l <= 26:
1428 1429 l = chr(ord('A') + l - 1)
1429 1430 else:
1430 1431 l = chr(l - 26 + ord('a') - 1)
1431 1432 return '%c%s\n' % (l, base85.b85encode(line, True))
1432 1433
1433 1434 def chunk(text, csize=52):
1434 1435 l = len(text)
1435 1436 i = 0
1436 1437 while i < l:
1437 1438 yield text[i:i + csize]
1438 1439 i += csize
1439 1440
1440 1441 tohash = gitindex(to)
1441 1442 tnhash = gitindex(tn)
1442 1443 if tohash == tnhash:
1443 1444 return ""
1444 1445
1445 1446 # TODO: deltas
1446 1447 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1447 1448 (tohash, tnhash, len(tn))]
1448 1449 for l in chunk(zlib.compress(tn)):
1449 1450 ret.append(fmtline(l))
1450 1451 ret.append('\n')
1451 1452 return ''.join(ret)
1452 1453
1453 1454 class GitDiffRequired(Exception):
1454 1455 pass
1455 1456
1456 1457 def diffopts(ui, opts=None, untrusted=False):
1457 1458 def get(key, name=None, getter=ui.configbool):
1458 1459 return ((opts and opts.get(key)) or
1459 1460 getter('diff', name or key, None, untrusted=untrusted))
1460 1461 return mdiff.diffopts(
1461 1462 text=opts and opts.get('text'),
1462 1463 git=get('git'),
1463 1464 nodates=get('nodates'),
1464 1465 showfunc=get('show_function', 'showfunc'),
1465 1466 ignorews=get('ignore_all_space', 'ignorews'),
1466 1467 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1467 1468 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1468 1469 context=get('unified', getter=ui.config))
1469 1470
1470 1471 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1471 1472 losedatafn=None, prefix=''):
1472 1473 '''yields diff of changes to files between two nodes, or node and
1473 1474 working directory.
1474 1475
1475 1476 if node1 is None, use first dirstate parent instead.
1476 1477 if node2 is None, compare node1 with working directory.
1477 1478
1478 1479 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1479 1480 every time some change cannot be represented with the current
1480 1481 patch format. Return False to upgrade to git patch format, True to
1481 1482 accept the loss or raise an exception to abort the diff. It is
1482 1483 called with the name of current file being diffed as 'fn'. If set
1483 1484 to None, patches will always be upgraded to git format when
1484 1485 necessary.
1485 1486
1486 1487 prefix is a filename prefix that is prepended to all filenames on
1487 1488 display (used for subrepos).
1488 1489 '''
1489 1490
1490 1491 if opts is None:
1491 1492 opts = mdiff.defaultopts
1492 1493
1493 1494 if not node1 and not node2:
1494 1495 node1 = repo.dirstate.p1()
1495 1496
1496 1497 def lrugetfilectx():
1497 1498 cache = {}
1498 1499 order = []
1499 1500 def getfilectx(f, ctx):
1500 1501 fctx = ctx.filectx(f, filelog=cache.get(f))
1501 1502 if f not in cache:
1502 1503 if len(cache) > 20:
1503 1504 del cache[order.pop(0)]
1504 1505 cache[f] = fctx.filelog()
1505 1506 else:
1506 1507 order.remove(f)
1507 1508 order.append(f)
1508 1509 return fctx
1509 1510 return getfilectx
1510 1511 getfilectx = lrugetfilectx()
1511 1512
1512 1513 ctx1 = repo[node1]
1513 1514 ctx2 = repo[node2]
1514 1515
1515 1516 if not changes:
1516 1517 changes = repo.status(ctx1, ctx2, match=match)
1517 1518 modified, added, removed = changes[:3]
1518 1519
1519 1520 if not modified and not added and not removed:
1520 1521 return []
1521 1522
1522 1523 revs = None
1523 1524 if not repo.ui.quiet:
1524 1525 hexfunc = repo.ui.debugflag and hex or short
1525 1526 revs = [hexfunc(node) for node in [node1, node2] if node]
1526 1527
1527 1528 copy = {}
1528 1529 if opts.git or opts.upgrade:
1529 1530 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1530 1531
1531 1532 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1532 1533 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1533 1534 if opts.upgrade and not opts.git:
1534 1535 try:
1535 1536 def losedata(fn):
1536 1537 if not losedatafn or not losedatafn(fn=fn):
1537 1538 raise GitDiffRequired()
1538 1539 # Buffer the whole output until we are sure it can be generated
1539 1540 return list(difffn(opts.copy(git=False), losedata))
1540 1541 except GitDiffRequired:
1541 1542 return difffn(opts.copy(git=True), None)
1542 1543 else:
1543 1544 return difffn(opts, None)
1544 1545
1545 1546 def difflabel(func, *args, **kw):
1546 1547 '''yields 2-tuples of (output, label) based on the output of func()'''
1547 1548 prefixes = [('diff', 'diff.diffline'),
1548 1549 ('copy', 'diff.extended'),
1549 1550 ('rename', 'diff.extended'),
1550 1551 ('old', 'diff.extended'),
1551 1552 ('new', 'diff.extended'),
1552 1553 ('deleted', 'diff.extended'),
1553 1554 ('---', 'diff.file_a'),
1554 1555 ('+++', 'diff.file_b'),
1555 1556 ('@@', 'diff.hunk'),
1556 1557 ('-', 'diff.deleted'),
1557 1558 ('+', 'diff.inserted')]
1558 1559
1559 1560 for chunk in func(*args, **kw):
1560 1561 lines = chunk.split('\n')
1561 1562 for i, line in enumerate(lines):
1562 1563 if i != 0:
1563 1564 yield ('\n', '')
1564 1565 stripline = line
1565 1566 if line and line[0] in '+-':
1566 1567 # highlight trailing whitespace, but only in changed lines
1567 1568 stripline = line.rstrip()
1568 1569 for prefix, label in prefixes:
1569 1570 if stripline.startswith(prefix):
1570 1571 yield (stripline, label)
1571 1572 break
1572 1573 else:
1573 1574 yield (line, '')
1574 1575 if line != stripline:
1575 1576 yield (line[len(stripline):], 'diff.trailingwhitespace')
1576 1577
1577 1578 def diffui(*args, **kw):
1578 1579 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1579 1580 return difflabel(diff, *args, **kw)
1580 1581
1581 1582
1582 1583 def _addmodehdr(header, omode, nmode):
1583 1584 if omode != nmode:
1584 1585 header.append('old mode %s\n' % omode)
1585 1586 header.append('new mode %s\n' % nmode)
1586 1587
1587 1588 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1588 1589 copy, getfilectx, opts, losedatafn, prefix):
1589 1590
1590 1591 def join(f):
1591 1592 return os.path.join(prefix, f)
1592 1593
1593 1594 date1 = util.datestr(ctx1.date())
1594 1595 man1 = ctx1.manifest()
1595 1596
1596 1597 gone = set()
1597 1598 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1598 1599
1599 1600 copyto = dict([(v, k) for k, v in copy.items()])
1600 1601
1601 1602 if opts.git:
1602 1603 revs = None
1603 1604
1604 1605 for f in sorted(modified + added + removed):
1605 1606 to = None
1606 1607 tn = None
1607 1608 dodiff = True
1608 1609 header = []
1609 1610 if f in man1:
1610 1611 to = getfilectx(f, ctx1).data()
1611 1612 if f not in removed:
1612 1613 tn = getfilectx(f, ctx2).data()
1613 1614 a, b = f, f
1614 1615 if opts.git or losedatafn:
1615 1616 if f in added:
1616 1617 mode = gitmode[ctx2.flags(f)]
1617 1618 if f in copy or f in copyto:
1618 1619 if opts.git:
1619 1620 if f in copy:
1620 1621 a = copy[f]
1621 1622 else:
1622 1623 a = copyto[f]
1623 1624 omode = gitmode[man1.flags(a)]
1624 1625 _addmodehdr(header, omode, mode)
1625 1626 if a in removed and a not in gone:
1626 1627 op = 'rename'
1627 1628 gone.add(a)
1628 1629 else:
1629 1630 op = 'copy'
1630 1631 header.append('%s from %s\n' % (op, join(a)))
1631 1632 header.append('%s to %s\n' % (op, join(f)))
1632 1633 to = getfilectx(a, ctx1).data()
1633 1634 else:
1634 1635 losedatafn(f)
1635 1636 else:
1636 1637 if opts.git:
1637 1638 header.append('new file mode %s\n' % mode)
1638 1639 elif ctx2.flags(f):
1639 1640 losedatafn(f)
1640 1641 # In theory, if tn was copied or renamed we should check
1641 1642 # if the source is binary too but the copy record already
1642 1643 # forces git mode.
1643 1644 if util.binary(tn):
1644 1645 if opts.git:
1645 1646 dodiff = 'binary'
1646 1647 else:
1647 1648 losedatafn(f)
1648 1649 if not opts.git and not tn:
1649 1650 # regular diffs cannot represent new empty file
1650 1651 losedatafn(f)
1651 1652 elif f in removed:
1652 1653 if opts.git:
1653 1654 # have we already reported a copy above?
1654 1655 if ((f in copy and copy[f] in added
1655 1656 and copyto[copy[f]] == f) or
1656 1657 (f in copyto and copyto[f] in added
1657 1658 and copy[copyto[f]] == f)):
1658 1659 dodiff = False
1659 1660 else:
1660 1661 header.append('deleted file mode %s\n' %
1661 1662 gitmode[man1.flags(f)])
1662 1663 elif not to or util.binary(to):
1663 1664 # regular diffs cannot represent empty file deletion
1664 1665 losedatafn(f)
1665 1666 else:
1666 1667 oflag = man1.flags(f)
1667 1668 nflag = ctx2.flags(f)
1668 1669 binary = util.binary(to) or util.binary(tn)
1669 1670 if opts.git:
1670 1671 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1671 1672 if binary:
1672 1673 dodiff = 'binary'
1673 1674 elif binary or nflag != oflag:
1674 1675 losedatafn(f)
1675 1676 if opts.git:
1676 1677 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1677 1678
1678 1679 if dodiff:
1679 1680 if dodiff == 'binary':
1680 1681 text = b85diff(to, tn)
1681 1682 else:
1682 1683 text = mdiff.unidiff(to, date1,
1683 1684 # ctx2 date may be dynamic
1684 1685 tn, util.datestr(ctx2.date()),
1685 1686 join(a), join(b), revs, opts=opts)
1686 1687 if header and (text or len(header) > 1):
1687 1688 yield ''.join(header)
1688 1689 if text:
1689 1690 yield text
1690 1691
1691 1692 def diffstatsum(stats):
1692 1693 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1693 1694 for f, a, r, b in stats:
1694 1695 maxfile = max(maxfile, encoding.colwidth(f))
1695 1696 maxtotal = max(maxtotal, a + r)
1696 1697 addtotal += a
1697 1698 removetotal += r
1698 1699 binary = binary or b
1699 1700
1700 1701 return maxfile, maxtotal, addtotal, removetotal, binary
1701 1702
1702 1703 def diffstatdata(lines):
1703 1704 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1704 1705
1705 1706 results = []
1706 1707 filename, adds, removes = None, 0, 0
1707 1708
1708 1709 def addresult():
1709 1710 if filename:
1710 1711 isbinary = adds == 0 and removes == 0
1711 1712 results.append((filename, adds, removes, isbinary))
1712 1713
1713 1714 for line in lines:
1714 1715 if line.startswith('diff'):
1715 1716 addresult()
1716 1717 # set numbers to 0 anyway when starting new file
1717 1718 adds, removes = 0, 0
1718 1719 if line.startswith('diff --git'):
1719 1720 filename = gitre.search(line).group(1)
1720 1721 elif line.startswith('diff -r'):
1721 1722 # format: "diff -r ... -r ... filename"
1722 1723 filename = diffre.search(line).group(1)
1723 1724 elif line.startswith('+') and not line.startswith('+++'):
1724 1725 adds += 1
1725 1726 elif line.startswith('-') and not line.startswith('---'):
1726 1727 removes += 1
1727 1728 addresult()
1728 1729 return results
1729 1730
1730 1731 def diffstat(lines, width=80, git=False):
1731 1732 output = []
1732 1733 stats = diffstatdata(lines)
1733 1734 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1734 1735
1735 1736 countwidth = len(str(maxtotal))
1736 1737 if hasbinary and countwidth < 3:
1737 1738 countwidth = 3
1738 1739 graphwidth = width - countwidth - maxname - 6
1739 1740 if graphwidth < 10:
1740 1741 graphwidth = 10
1741 1742
1742 1743 def scale(i):
1743 1744 if maxtotal <= graphwidth:
1744 1745 return i
1745 1746 # If diffstat runs out of room it doesn't print anything,
1746 1747 # which isn't very useful, so always print at least one + or -
1747 1748 # if there were at least some changes.
1748 1749 return max(i * graphwidth // maxtotal, int(bool(i)))
1749 1750
1750 1751 for filename, adds, removes, isbinary in stats:
1751 1752 if git and isbinary:
1752 1753 count = 'Bin'
1753 1754 else:
1754 1755 count = adds + removes
1755 1756 pluses = '+' * scale(adds)
1756 1757 minuses = '-' * scale(removes)
1757 1758 output.append(' %s%s | %*s %s%s\n' %
1758 1759 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1759 1760 countwidth, count, pluses, minuses))
1760 1761
1761 1762 if stats:
1762 1763 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1763 1764 % (len(stats), totaladds, totalremoves))
1764 1765
1765 1766 return ''.join(output)
1766 1767
1767 1768 def diffstatui(*args, **kw):
1768 1769 '''like diffstat(), but yields 2-tuples of (output, label) for
1769 1770 ui.write()
1770 1771 '''
1771 1772
1772 1773 for line in diffstat(*args, **kw).splitlines():
1773 1774 if line and line[-1] in '+-':
1774 1775 name, graph = line.rsplit(' ', 1)
1775 1776 yield (name + ' ', '')
1776 1777 m = re.search(r'\++', graph)
1777 1778 if m:
1778 1779 yield (m.group(0), 'diffstat.inserted')
1779 1780 m = re.search(r'-+', graph)
1780 1781 if m:
1781 1782 yield (m.group(0), 'diffstat.deleted')
1782 1783 else:
1783 1784 yield (line, '')
1784 1785 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now