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