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