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