##// END OF EJS Templates
patch: correctly handle non-tabular Subject: line...
Steffen Daode Nurpmeso -
r15158:7ce7177e stable
parent child Browse files
Show More
@@ -1,1859 +1,1859 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 hasattr(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 subject = subject.replace('\n\t', ' ')
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 # patch starts counting at 1 unless we are adding the file
727 727 if h.starta == 0:
728 728 start = 0
729 729 else:
730 730 start = h.starta + self.offset - 1
731 731 orig_start = start
732 732 # if there's skew we want to emit the "(offset %d lines)" even
733 733 # when the hunk cleanly applies at start + skew, so skip the
734 734 # fast case code
735 735 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
736 736 if self.remove:
737 737 self.backend.unlink(self.fname)
738 738 else:
739 739 self.lines[start : start + h.lena] = h.new()
740 740 self.offset += h.lenb - h.lena
741 741 self.dirty = True
742 742 return 0
743 743
744 744 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
745 745 self.hash = {}
746 746 for x, s in enumerate(self.lines):
747 747 self.hash.setdefault(s, []).append(x)
748 748 if h.hunk[-1][0] != ' ':
749 749 # if the hunk tried to put something at the bottom of the file
750 750 # override the start line and use eof here
751 751 search_start = len(self.lines)
752 752 else:
753 753 search_start = orig_start + self.skew
754 754
755 755 for fuzzlen in xrange(3):
756 756 for toponly in [True, False]:
757 757 old = h.old(fuzzlen, toponly)
758 758
759 759 cand = self.findlines(old[0][1:], search_start)
760 760 for l in cand:
761 761 if diffhelpers.testhunk(old, self.lines, l) == 0:
762 762 newlines = h.new(fuzzlen, toponly)
763 763 self.lines[l : l + len(old)] = newlines
764 764 self.offset += len(newlines) - len(old)
765 765 self.skew = l - orig_start
766 766 self.dirty = True
767 767 offset = l - orig_start - fuzzlen
768 768 if fuzzlen:
769 769 msg = _("Hunk #%d succeeded at %d "
770 770 "with fuzz %d "
771 771 "(offset %d lines).\n")
772 772 self.printfile(True)
773 773 self.ui.warn(msg %
774 774 (h.number, l + 1, fuzzlen, offset))
775 775 else:
776 776 msg = _("Hunk #%d succeeded at %d "
777 777 "(offset %d lines).\n")
778 778 self.ui.note(msg % (h.number, l + 1, offset))
779 779 return fuzzlen
780 780 self.printfile(True)
781 781 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
782 782 self.rej.append(horig)
783 783 return -1
784 784
785 785 def close(self):
786 786 if self.dirty:
787 787 self.writelines(self.fname, self.lines, self.mode)
788 788 self.write_rej()
789 789 return len(self.rej)
790 790
791 791 class hunk(object):
792 792 def __init__(self, desc, num, lr, context):
793 793 self.number = num
794 794 self.desc = desc
795 795 self.hunk = [desc]
796 796 self.a = []
797 797 self.b = []
798 798 self.starta = self.lena = None
799 799 self.startb = self.lenb = None
800 800 if lr is not None:
801 801 if context:
802 802 self.read_context_hunk(lr)
803 803 else:
804 804 self.read_unified_hunk(lr)
805 805
806 806 def getnormalized(self):
807 807 """Return a copy with line endings normalized to LF."""
808 808
809 809 def normalize(lines):
810 810 nlines = []
811 811 for line in lines:
812 812 if line.endswith('\r\n'):
813 813 line = line[:-2] + '\n'
814 814 nlines.append(line)
815 815 return nlines
816 816
817 817 # Dummy object, it is rebuilt manually
818 818 nh = hunk(self.desc, self.number, None, None)
819 819 nh.number = self.number
820 820 nh.desc = self.desc
821 821 nh.hunk = self.hunk
822 822 nh.a = normalize(self.a)
823 823 nh.b = normalize(self.b)
824 824 nh.starta = self.starta
825 825 nh.startb = self.startb
826 826 nh.lena = self.lena
827 827 nh.lenb = self.lenb
828 828 return nh
829 829
830 830 def read_unified_hunk(self, lr):
831 831 m = unidesc.match(self.desc)
832 832 if not m:
833 833 raise PatchError(_("bad hunk #%d") % self.number)
834 834 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
835 835 if self.lena is None:
836 836 self.lena = 1
837 837 else:
838 838 self.lena = int(self.lena)
839 839 if self.lenb is None:
840 840 self.lenb = 1
841 841 else:
842 842 self.lenb = int(self.lenb)
843 843 self.starta = int(self.starta)
844 844 self.startb = int(self.startb)
845 845 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
846 846 # if we hit eof before finishing out the hunk, the last line will
847 847 # be zero length. Lets try to fix it up.
848 848 while len(self.hunk[-1]) == 0:
849 849 del self.hunk[-1]
850 850 del self.a[-1]
851 851 del self.b[-1]
852 852 self.lena -= 1
853 853 self.lenb -= 1
854 854 self._fixnewline(lr)
855 855
856 856 def read_context_hunk(self, lr):
857 857 self.desc = lr.readline()
858 858 m = contextdesc.match(self.desc)
859 859 if not m:
860 860 raise PatchError(_("bad hunk #%d") % self.number)
861 861 foo, self.starta, foo2, aend, foo3 = m.groups()
862 862 self.starta = int(self.starta)
863 863 if aend is None:
864 864 aend = self.starta
865 865 self.lena = int(aend) - self.starta
866 866 if self.starta:
867 867 self.lena += 1
868 868 for x in xrange(self.lena):
869 869 l = lr.readline()
870 870 if l.startswith('---'):
871 871 # lines addition, old block is empty
872 872 lr.push(l)
873 873 break
874 874 s = l[2:]
875 875 if l.startswith('- ') or l.startswith('! '):
876 876 u = '-' + s
877 877 elif l.startswith(' '):
878 878 u = ' ' + s
879 879 else:
880 880 raise PatchError(_("bad hunk #%d old text line %d") %
881 881 (self.number, x))
882 882 self.a.append(u)
883 883 self.hunk.append(u)
884 884
885 885 l = lr.readline()
886 886 if l.startswith('\ '):
887 887 s = self.a[-1][:-1]
888 888 self.a[-1] = s
889 889 self.hunk[-1] = s
890 890 l = lr.readline()
891 891 m = contextdesc.match(l)
892 892 if not m:
893 893 raise PatchError(_("bad hunk #%d") % self.number)
894 894 foo, self.startb, foo2, bend, foo3 = m.groups()
895 895 self.startb = int(self.startb)
896 896 if bend is None:
897 897 bend = self.startb
898 898 self.lenb = int(bend) - self.startb
899 899 if self.startb:
900 900 self.lenb += 1
901 901 hunki = 1
902 902 for x in xrange(self.lenb):
903 903 l = lr.readline()
904 904 if l.startswith('\ '):
905 905 # XXX: the only way to hit this is with an invalid line range.
906 906 # The no-eol marker is not counted in the line range, but I
907 907 # guess there are diff(1) out there which behave differently.
908 908 s = self.b[-1][:-1]
909 909 self.b[-1] = s
910 910 self.hunk[hunki - 1] = s
911 911 continue
912 912 if not l:
913 913 # line deletions, new block is empty and we hit EOF
914 914 lr.push(l)
915 915 break
916 916 s = l[2:]
917 917 if l.startswith('+ ') or l.startswith('! '):
918 918 u = '+' + s
919 919 elif l.startswith(' '):
920 920 u = ' ' + s
921 921 elif len(self.b) == 0:
922 922 # line deletions, new block is empty
923 923 lr.push(l)
924 924 break
925 925 else:
926 926 raise PatchError(_("bad hunk #%d old text line %d") %
927 927 (self.number, x))
928 928 self.b.append(s)
929 929 while True:
930 930 if hunki >= len(self.hunk):
931 931 h = ""
932 932 else:
933 933 h = self.hunk[hunki]
934 934 hunki += 1
935 935 if h == u:
936 936 break
937 937 elif h.startswith('-'):
938 938 continue
939 939 else:
940 940 self.hunk.insert(hunki - 1, u)
941 941 break
942 942
943 943 if not self.a:
944 944 # this happens when lines were only added to the hunk
945 945 for x in self.hunk:
946 946 if x.startswith('-') or x.startswith(' '):
947 947 self.a.append(x)
948 948 if not self.b:
949 949 # this happens when lines were only deleted from the hunk
950 950 for x in self.hunk:
951 951 if x.startswith('+') or x.startswith(' '):
952 952 self.b.append(x[1:])
953 953 # @@ -start,len +start,len @@
954 954 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
955 955 self.startb, self.lenb)
956 956 self.hunk[0] = self.desc
957 957 self._fixnewline(lr)
958 958
959 959 def _fixnewline(self, lr):
960 960 l = lr.readline()
961 961 if l.startswith('\ '):
962 962 diffhelpers.fix_newline(self.hunk, self.a, self.b)
963 963 else:
964 964 lr.push(l)
965 965
966 966 def complete(self):
967 967 return len(self.a) == self.lena and len(self.b) == self.lenb
968 968
969 969 def fuzzit(self, l, fuzz, toponly):
970 970 # this removes context lines from the top and bottom of list 'l'. It
971 971 # checks the hunk to make sure only context lines are removed, and then
972 972 # returns a new shortened list of lines.
973 973 fuzz = min(fuzz, len(l)-1)
974 974 if fuzz:
975 975 top = 0
976 976 bot = 0
977 977 hlen = len(self.hunk)
978 978 for x in xrange(hlen - 1):
979 979 # the hunk starts with the @@ line, so use x+1
980 980 if self.hunk[x + 1][0] == ' ':
981 981 top += 1
982 982 else:
983 983 break
984 984 if not toponly:
985 985 for x in xrange(hlen - 1):
986 986 if self.hunk[hlen - bot - 1][0] == ' ':
987 987 bot += 1
988 988 else:
989 989 break
990 990
991 991 # top and bot now count context in the hunk
992 992 # adjust them if either one is short
993 993 context = max(top, bot, 3)
994 994 if bot < context:
995 995 bot = max(0, fuzz - (context - bot))
996 996 else:
997 997 bot = min(fuzz, bot)
998 998 if top < context:
999 999 top = max(0, fuzz - (context - top))
1000 1000 else:
1001 1001 top = min(fuzz, top)
1002 1002
1003 1003 return l[top:len(l)-bot]
1004 1004 return l
1005 1005
1006 1006 def old(self, fuzz=0, toponly=False):
1007 1007 return self.fuzzit(self.a, fuzz, toponly)
1008 1008
1009 1009 def new(self, fuzz=0, toponly=False):
1010 1010 return self.fuzzit(self.b, fuzz, toponly)
1011 1011
1012 1012 class binhunk(object):
1013 1013 'A binary patch file. Only understands literals so far.'
1014 1014 def __init__(self, lr):
1015 1015 self.text = None
1016 1016 self.hunk = ['GIT binary patch\n']
1017 1017 self._read(lr)
1018 1018
1019 1019 def complete(self):
1020 1020 return self.text is not None
1021 1021
1022 1022 def new(self):
1023 1023 return [self.text]
1024 1024
1025 1025 def _read(self, lr):
1026 1026 line = lr.readline()
1027 1027 self.hunk.append(line)
1028 1028 while line and not line.startswith('literal '):
1029 1029 line = lr.readline()
1030 1030 self.hunk.append(line)
1031 1031 if not line:
1032 1032 raise PatchError(_('could not extract binary patch'))
1033 1033 size = int(line[8:].rstrip())
1034 1034 dec = []
1035 1035 line = lr.readline()
1036 1036 self.hunk.append(line)
1037 1037 while len(line) > 1:
1038 1038 l = line[0]
1039 1039 if l <= 'Z' and l >= 'A':
1040 1040 l = ord(l) - ord('A') + 1
1041 1041 else:
1042 1042 l = ord(l) - ord('a') + 27
1043 1043 dec.append(base85.b85decode(line[1:-1])[:l])
1044 1044 line = lr.readline()
1045 1045 self.hunk.append(line)
1046 1046 text = zlib.decompress(''.join(dec))
1047 1047 if len(text) != size:
1048 1048 raise PatchError(_('binary patch is %d bytes, not %d') %
1049 1049 len(text), size)
1050 1050 self.text = text
1051 1051
1052 1052 def parsefilename(str):
1053 1053 # --- filename \t|space stuff
1054 1054 s = str[4:].rstrip('\r\n')
1055 1055 i = s.find('\t')
1056 1056 if i < 0:
1057 1057 i = s.find(' ')
1058 1058 if i < 0:
1059 1059 return s
1060 1060 return s[:i]
1061 1061
1062 1062 def pathstrip(path, strip):
1063 1063 pathlen = len(path)
1064 1064 i = 0
1065 1065 if strip == 0:
1066 1066 return '', path.rstrip()
1067 1067 count = strip
1068 1068 while count > 0:
1069 1069 i = path.find('/', i)
1070 1070 if i == -1:
1071 1071 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1072 1072 (count, strip, path))
1073 1073 i += 1
1074 1074 # consume '//' in the path
1075 1075 while i < pathlen - 1 and path[i] == '/':
1076 1076 i += 1
1077 1077 count -= 1
1078 1078 return path[:i].lstrip(), path[i:].rstrip()
1079 1079
1080 1080 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1081 1081 nulla = afile_orig == "/dev/null"
1082 1082 nullb = bfile_orig == "/dev/null"
1083 1083 create = nulla and hunk.starta == 0 and hunk.lena == 0
1084 1084 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1085 1085 abase, afile = pathstrip(afile_orig, strip)
1086 1086 gooda = not nulla and backend.exists(afile)
1087 1087 bbase, bfile = pathstrip(bfile_orig, strip)
1088 1088 if afile == bfile:
1089 1089 goodb = gooda
1090 1090 else:
1091 1091 goodb = not nullb and backend.exists(bfile)
1092 1092 missing = not goodb and not gooda and not create
1093 1093
1094 1094 # some diff programs apparently produce patches where the afile is
1095 1095 # not /dev/null, but afile starts with bfile
1096 1096 abasedir = afile[:afile.rfind('/') + 1]
1097 1097 bbasedir = bfile[:bfile.rfind('/') + 1]
1098 1098 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1099 1099 and hunk.starta == 0 and hunk.lena == 0):
1100 1100 create = True
1101 1101 missing = False
1102 1102
1103 1103 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1104 1104 # diff is between a file and its backup. In this case, the original
1105 1105 # file should be patched (see original mpatch code).
1106 1106 isbackup = (abase == bbase and bfile.startswith(afile))
1107 1107 fname = None
1108 1108 if not missing:
1109 1109 if gooda and goodb:
1110 1110 fname = isbackup and afile or bfile
1111 1111 elif gooda:
1112 1112 fname = afile
1113 1113
1114 1114 if not fname:
1115 1115 if not nullb:
1116 1116 fname = isbackup and afile or bfile
1117 1117 elif not nulla:
1118 1118 fname = afile
1119 1119 else:
1120 1120 raise PatchError(_("undefined source and destination files"))
1121 1121
1122 1122 gp = patchmeta(fname)
1123 1123 if create:
1124 1124 gp.op = 'ADD'
1125 1125 elif remove:
1126 1126 gp.op = 'DELETE'
1127 1127 return gp
1128 1128
1129 1129 def scangitpatch(lr, firstline):
1130 1130 """
1131 1131 Git patches can emit:
1132 1132 - rename a to b
1133 1133 - change b
1134 1134 - copy a to c
1135 1135 - change c
1136 1136
1137 1137 We cannot apply this sequence as-is, the renamed 'a' could not be
1138 1138 found for it would have been renamed already. And we cannot copy
1139 1139 from 'b' instead because 'b' would have been changed already. So
1140 1140 we scan the git patch for copy and rename commands so we can
1141 1141 perform the copies ahead of time.
1142 1142 """
1143 1143 pos = 0
1144 1144 try:
1145 1145 pos = lr.fp.tell()
1146 1146 fp = lr.fp
1147 1147 except IOError:
1148 1148 fp = cStringIO.StringIO(lr.fp.read())
1149 1149 gitlr = linereader(fp)
1150 1150 gitlr.push(firstline)
1151 1151 gitpatches = readgitpatch(gitlr)
1152 1152 fp.seek(pos)
1153 1153 return gitpatches
1154 1154
1155 1155 def iterhunks(fp):
1156 1156 """Read a patch and yield the following events:
1157 1157 - ("file", afile, bfile, firsthunk): select a new target file.
1158 1158 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1159 1159 "file" event.
1160 1160 - ("git", gitchanges): current diff is in git format, gitchanges
1161 1161 maps filenames to gitpatch records. Unique event.
1162 1162 """
1163 1163 afile = ""
1164 1164 bfile = ""
1165 1165 state = None
1166 1166 hunknum = 0
1167 1167 emitfile = newfile = False
1168 1168 gitpatches = None
1169 1169
1170 1170 # our states
1171 1171 BFILE = 1
1172 1172 context = None
1173 1173 lr = linereader(fp)
1174 1174
1175 1175 while True:
1176 1176 x = lr.readline()
1177 1177 if not x:
1178 1178 break
1179 1179 if state == BFILE and (
1180 1180 (not context and x[0] == '@')
1181 1181 or (context is not False and x.startswith('***************'))
1182 1182 or x.startswith('GIT binary patch')):
1183 1183 gp = None
1184 1184 if (gitpatches and
1185 1185 (gitpatches[-1][0] == afile or gitpatches[-1][1] == bfile)):
1186 1186 gp = gitpatches.pop()[2]
1187 1187 if x.startswith('GIT binary patch'):
1188 1188 h = binhunk(lr)
1189 1189 else:
1190 1190 if context is None and x.startswith('***************'):
1191 1191 context = True
1192 1192 h = hunk(x, hunknum + 1, lr, context)
1193 1193 hunknum += 1
1194 1194 if emitfile:
1195 1195 emitfile = False
1196 1196 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1197 1197 yield 'hunk', h
1198 1198 elif x.startswith('diff --git'):
1199 1199 m = gitre.match(x)
1200 1200 if not m:
1201 1201 continue
1202 1202 if not gitpatches:
1203 1203 # scan whole input for git metadata
1204 1204 gitpatches = [('a/' + gp.path, 'b/' + gp.path, gp) for gp
1205 1205 in scangitpatch(lr, x)]
1206 1206 yield 'git', [g[2].copy() for g in gitpatches
1207 1207 if g[2].op in ('COPY', 'RENAME')]
1208 1208 gitpatches.reverse()
1209 1209 afile = 'a/' + m.group(1)
1210 1210 bfile = 'b/' + m.group(2)
1211 1211 while afile != gitpatches[-1][0] and bfile != gitpatches[-1][1]:
1212 1212 gp = gitpatches.pop()[2]
1213 1213 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1214 1214 gp = gitpatches[-1][2]
1215 1215 # copy/rename + modify should modify target, not source
1216 1216 if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
1217 1217 afile = bfile
1218 1218 newfile = True
1219 1219 elif x.startswith('---'):
1220 1220 # check for a unified diff
1221 1221 l2 = lr.readline()
1222 1222 if not l2.startswith('+++'):
1223 1223 lr.push(l2)
1224 1224 continue
1225 1225 newfile = True
1226 1226 context = False
1227 1227 afile = parsefilename(x)
1228 1228 bfile = parsefilename(l2)
1229 1229 elif x.startswith('***'):
1230 1230 # check for a context diff
1231 1231 l2 = lr.readline()
1232 1232 if not l2.startswith('---'):
1233 1233 lr.push(l2)
1234 1234 continue
1235 1235 l3 = lr.readline()
1236 1236 lr.push(l3)
1237 1237 if not l3.startswith("***************"):
1238 1238 lr.push(l2)
1239 1239 continue
1240 1240 newfile = True
1241 1241 context = True
1242 1242 afile = parsefilename(x)
1243 1243 bfile = parsefilename(l2)
1244 1244
1245 1245 if newfile:
1246 1246 newfile = False
1247 1247 emitfile = True
1248 1248 state = BFILE
1249 1249 hunknum = 0
1250 1250
1251 1251 while gitpatches:
1252 1252 gp = gitpatches.pop()[2]
1253 1253 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1254 1254
1255 1255 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1256 1256 """Reads a patch from fp and tries to apply it.
1257 1257
1258 1258 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1259 1259 there was any fuzz.
1260 1260
1261 1261 If 'eolmode' is 'strict', the patch content and patched file are
1262 1262 read in binary mode. Otherwise, line endings are ignored when
1263 1263 patching then normalized according to 'eolmode'.
1264 1264 """
1265 1265 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1266 1266 eolmode=eolmode)
1267 1267
1268 1268 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1269 1269 eolmode='strict'):
1270 1270
1271 1271 def pstrip(p):
1272 1272 return pathstrip(p, strip - 1)[1]
1273 1273
1274 1274 rejects = 0
1275 1275 err = 0
1276 1276 current_file = None
1277 1277
1278 1278 for state, values in iterhunks(fp):
1279 1279 if state == 'hunk':
1280 1280 if not current_file:
1281 1281 continue
1282 1282 ret = current_file.apply(values)
1283 1283 if ret > 0:
1284 1284 err = 1
1285 1285 elif state == 'file':
1286 1286 if current_file:
1287 1287 rejects += current_file.close()
1288 1288 current_file = None
1289 1289 afile, bfile, first_hunk, gp = values
1290 1290 if gp:
1291 1291 path = pstrip(gp.path)
1292 1292 gp.path = pstrip(gp.path)
1293 1293 if gp.oldpath:
1294 1294 gp.oldpath = pstrip(gp.oldpath)
1295 1295 else:
1296 1296 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1297 1297 if gp.op == 'RENAME':
1298 1298 backend.unlink(gp.oldpath)
1299 1299 if not first_hunk:
1300 1300 if gp.op == 'DELETE':
1301 1301 backend.unlink(gp.path)
1302 1302 continue
1303 1303 data, mode = None, None
1304 1304 if gp.op in ('RENAME', 'COPY'):
1305 1305 data, mode = store.getfile(gp.oldpath)[:2]
1306 1306 if gp.mode:
1307 1307 mode = gp.mode
1308 1308 if gp.op == 'ADD':
1309 1309 # Added files without content have no hunk and
1310 1310 # must be created
1311 1311 data = ''
1312 1312 if data or mode:
1313 1313 if (gp.op in ('ADD', 'RENAME', 'COPY')
1314 1314 and backend.exists(gp.path)):
1315 1315 raise PatchError(_("cannot create %s: destination "
1316 1316 "already exists") % gp.path)
1317 1317 backend.setfile(gp.path, data, mode, gp.oldpath)
1318 1318 continue
1319 1319 try:
1320 1320 current_file = patcher(ui, gp, backend, store,
1321 1321 eolmode=eolmode)
1322 1322 except PatchError, inst:
1323 1323 ui.warn(str(inst) + '\n')
1324 1324 current_file = None
1325 1325 rejects += 1
1326 1326 continue
1327 1327 elif state == 'git':
1328 1328 for gp in values:
1329 1329 path = pstrip(gp.oldpath)
1330 1330 data, mode = backend.getfile(path)
1331 1331 store.setfile(path, data, mode)
1332 1332 else:
1333 1333 raise util.Abort(_('unsupported parser state: %s') % state)
1334 1334
1335 1335 if current_file:
1336 1336 rejects += current_file.close()
1337 1337
1338 1338 if rejects:
1339 1339 return -1
1340 1340 return err
1341 1341
1342 1342 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1343 1343 similarity):
1344 1344 """use <patcher> to apply <patchname> to the working directory.
1345 1345 returns whether patch was applied with fuzz factor."""
1346 1346
1347 1347 fuzz = False
1348 1348 args = []
1349 1349 cwd = repo.root
1350 1350 if cwd:
1351 1351 args.append('-d %s' % util.shellquote(cwd))
1352 1352 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1353 1353 util.shellquote(patchname)))
1354 1354 try:
1355 1355 for line in fp:
1356 1356 line = line.rstrip()
1357 1357 ui.note(line + '\n')
1358 1358 if line.startswith('patching file '):
1359 1359 pf = util.parsepatchoutput(line)
1360 1360 printed_file = False
1361 1361 files.add(pf)
1362 1362 elif line.find('with fuzz') >= 0:
1363 1363 fuzz = True
1364 1364 if not printed_file:
1365 1365 ui.warn(pf + '\n')
1366 1366 printed_file = True
1367 1367 ui.warn(line + '\n')
1368 1368 elif line.find('saving rejects to file') >= 0:
1369 1369 ui.warn(line + '\n')
1370 1370 elif line.find('FAILED') >= 0:
1371 1371 if not printed_file:
1372 1372 ui.warn(pf + '\n')
1373 1373 printed_file = True
1374 1374 ui.warn(line + '\n')
1375 1375 finally:
1376 1376 if files:
1377 1377 cfiles = list(files)
1378 1378 cwd = repo.getcwd()
1379 1379 if cwd:
1380 1380 cfiles = [util.pathto(repo.root, cwd, f)
1381 1381 for f in cfiles]
1382 1382 scmutil.addremove(repo, cfiles, similarity=similarity)
1383 1383 code = fp.close()
1384 1384 if code:
1385 1385 raise PatchError(_("patch command failed: %s") %
1386 1386 util.explainexit(code)[0])
1387 1387 return fuzz
1388 1388
1389 1389 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1390 1390 if files is None:
1391 1391 files = set()
1392 1392 if eolmode is None:
1393 1393 eolmode = ui.config('patch', 'eol', 'strict')
1394 1394 if eolmode.lower() not in eolmodes:
1395 1395 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1396 1396 eolmode = eolmode.lower()
1397 1397
1398 1398 store = filestore()
1399 1399 try:
1400 1400 fp = open(patchobj, 'rb')
1401 1401 except TypeError:
1402 1402 fp = patchobj
1403 1403 try:
1404 1404 ret = applydiff(ui, fp, backend, store, strip=strip,
1405 1405 eolmode=eolmode)
1406 1406 finally:
1407 1407 if fp != patchobj:
1408 1408 fp.close()
1409 1409 files.update(backend.close())
1410 1410 store.close()
1411 1411 if ret < 0:
1412 1412 raise PatchError(_('patch failed to apply'))
1413 1413 return ret > 0
1414 1414
1415 1415 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1416 1416 similarity=0):
1417 1417 """use builtin patch to apply <patchobj> to the working directory.
1418 1418 returns whether patch was applied with fuzz factor."""
1419 1419 backend = workingbackend(ui, repo, similarity)
1420 1420 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1421 1421
1422 1422 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1423 1423 eolmode='strict'):
1424 1424 backend = repobackend(ui, repo, ctx, store)
1425 1425 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1426 1426
1427 1427 def makememctx(repo, parents, text, user, date, branch, files, store,
1428 1428 editor=None):
1429 1429 def getfilectx(repo, memctx, path):
1430 1430 data, (islink, isexec), copied = store.getfile(path)
1431 1431 return context.memfilectx(path, data, islink=islink, isexec=isexec,
1432 1432 copied=copied)
1433 1433 extra = {}
1434 1434 if branch:
1435 1435 extra['branch'] = encoding.fromlocal(branch)
1436 1436 ctx = context.memctx(repo, parents, text, files, getfilectx, user,
1437 1437 date, extra)
1438 1438 if editor:
1439 1439 ctx._text = editor(repo, ctx, [])
1440 1440 return ctx
1441 1441
1442 1442 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1443 1443 similarity=0):
1444 1444 """Apply <patchname> to the working directory.
1445 1445
1446 1446 'eolmode' specifies how end of lines should be handled. It can be:
1447 1447 - 'strict': inputs are read in binary mode, EOLs are preserved
1448 1448 - 'crlf': EOLs are ignored when patching and reset to CRLF
1449 1449 - 'lf': EOLs are ignored when patching and reset to LF
1450 1450 - None: get it from user settings, default to 'strict'
1451 1451 'eolmode' is ignored when using an external patcher program.
1452 1452
1453 1453 Returns whether patch was applied with fuzz factor.
1454 1454 """
1455 1455 patcher = ui.config('ui', 'patch')
1456 1456 if files is None:
1457 1457 files = set()
1458 1458 try:
1459 1459 if patcher:
1460 1460 return _externalpatch(ui, repo, patcher, patchname, strip,
1461 1461 files, similarity)
1462 1462 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1463 1463 similarity)
1464 1464 except PatchError, err:
1465 1465 raise util.Abort(str(err))
1466 1466
1467 1467 def changedfiles(ui, repo, patchpath, strip=1):
1468 1468 backend = fsbackend(ui, repo.root)
1469 1469 fp = open(patchpath, 'rb')
1470 1470 try:
1471 1471 changed = set()
1472 1472 for state, values in iterhunks(fp):
1473 1473 if state == 'file':
1474 1474 afile, bfile, first_hunk, gp = values
1475 1475 if gp:
1476 1476 gp.path = pathstrip(gp.path, strip - 1)[1]
1477 1477 if gp.oldpath:
1478 1478 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1479 1479 else:
1480 1480 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1481 1481 changed.add(gp.path)
1482 1482 if gp.op == 'RENAME':
1483 1483 changed.add(gp.oldpath)
1484 1484 elif state not in ('hunk', 'git'):
1485 1485 raise util.Abort(_('unsupported parser state: %s') % state)
1486 1486 return changed
1487 1487 finally:
1488 1488 fp.close()
1489 1489
1490 1490 def b85diff(to, tn):
1491 1491 '''print base85-encoded binary diff'''
1492 1492 def gitindex(text):
1493 1493 if not text:
1494 1494 return hex(nullid)
1495 1495 l = len(text)
1496 1496 s = util.sha1('blob %d\0' % l)
1497 1497 s.update(text)
1498 1498 return s.hexdigest()
1499 1499
1500 1500 def fmtline(line):
1501 1501 l = len(line)
1502 1502 if l <= 26:
1503 1503 l = chr(ord('A') + l - 1)
1504 1504 else:
1505 1505 l = chr(l - 26 + ord('a') - 1)
1506 1506 return '%c%s\n' % (l, base85.b85encode(line, True))
1507 1507
1508 1508 def chunk(text, csize=52):
1509 1509 l = len(text)
1510 1510 i = 0
1511 1511 while i < l:
1512 1512 yield text[i:i + csize]
1513 1513 i += csize
1514 1514
1515 1515 tohash = gitindex(to)
1516 1516 tnhash = gitindex(tn)
1517 1517 if tohash == tnhash:
1518 1518 return ""
1519 1519
1520 1520 # TODO: deltas
1521 1521 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1522 1522 (tohash, tnhash, len(tn))]
1523 1523 for l in chunk(zlib.compress(tn)):
1524 1524 ret.append(fmtline(l))
1525 1525 ret.append('\n')
1526 1526 return ''.join(ret)
1527 1527
1528 1528 class GitDiffRequired(Exception):
1529 1529 pass
1530 1530
1531 1531 def diffopts(ui, opts=None, untrusted=False):
1532 1532 def get(key, name=None, getter=ui.configbool):
1533 1533 return ((opts and opts.get(key)) or
1534 1534 getter('diff', name or key, None, untrusted=untrusted))
1535 1535 return mdiff.diffopts(
1536 1536 text=opts and opts.get('text'),
1537 1537 git=get('git'),
1538 1538 nodates=get('nodates'),
1539 1539 showfunc=get('show_function', 'showfunc'),
1540 1540 ignorews=get('ignore_all_space', 'ignorews'),
1541 1541 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1542 1542 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1543 1543 context=get('unified', getter=ui.config))
1544 1544
1545 1545 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1546 1546 losedatafn=None, prefix=''):
1547 1547 '''yields diff of changes to files between two nodes, or node and
1548 1548 working directory.
1549 1549
1550 1550 if node1 is None, use first dirstate parent instead.
1551 1551 if node2 is None, compare node1 with working directory.
1552 1552
1553 1553 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1554 1554 every time some change cannot be represented with the current
1555 1555 patch format. Return False to upgrade to git patch format, True to
1556 1556 accept the loss or raise an exception to abort the diff. It is
1557 1557 called with the name of current file being diffed as 'fn'. If set
1558 1558 to None, patches will always be upgraded to git format when
1559 1559 necessary.
1560 1560
1561 1561 prefix is a filename prefix that is prepended to all filenames on
1562 1562 display (used for subrepos).
1563 1563 '''
1564 1564
1565 1565 if opts is None:
1566 1566 opts = mdiff.defaultopts
1567 1567
1568 1568 if not node1 and not node2:
1569 1569 node1 = repo.dirstate.p1()
1570 1570
1571 1571 def lrugetfilectx():
1572 1572 cache = {}
1573 1573 order = []
1574 1574 def getfilectx(f, ctx):
1575 1575 fctx = ctx.filectx(f, filelog=cache.get(f))
1576 1576 if f not in cache:
1577 1577 if len(cache) > 20:
1578 1578 del cache[order.pop(0)]
1579 1579 cache[f] = fctx.filelog()
1580 1580 else:
1581 1581 order.remove(f)
1582 1582 order.append(f)
1583 1583 return fctx
1584 1584 return getfilectx
1585 1585 getfilectx = lrugetfilectx()
1586 1586
1587 1587 ctx1 = repo[node1]
1588 1588 ctx2 = repo[node2]
1589 1589
1590 1590 if not changes:
1591 1591 changes = repo.status(ctx1, ctx2, match=match)
1592 1592 modified, added, removed = changes[:3]
1593 1593
1594 1594 if not modified and not added and not removed:
1595 1595 return []
1596 1596
1597 1597 revs = None
1598 1598 if not repo.ui.quiet:
1599 1599 hexfunc = repo.ui.debugflag and hex or short
1600 1600 revs = [hexfunc(node) for node in [node1, node2] if node]
1601 1601
1602 1602 copy = {}
1603 1603 if opts.git or opts.upgrade:
1604 1604 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1605 1605
1606 1606 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1607 1607 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1608 1608 if opts.upgrade and not opts.git:
1609 1609 try:
1610 1610 def losedata(fn):
1611 1611 if not losedatafn or not losedatafn(fn=fn):
1612 1612 raise GitDiffRequired()
1613 1613 # Buffer the whole output until we are sure it can be generated
1614 1614 return list(difffn(opts.copy(git=False), losedata))
1615 1615 except GitDiffRequired:
1616 1616 return difffn(opts.copy(git=True), None)
1617 1617 else:
1618 1618 return difffn(opts, None)
1619 1619
1620 1620 def difflabel(func, *args, **kw):
1621 1621 '''yields 2-tuples of (output, label) based on the output of func()'''
1622 1622 prefixes = [('diff', 'diff.diffline'),
1623 1623 ('copy', 'diff.extended'),
1624 1624 ('rename', 'diff.extended'),
1625 1625 ('old', 'diff.extended'),
1626 1626 ('new', 'diff.extended'),
1627 1627 ('deleted', 'diff.extended'),
1628 1628 ('---', 'diff.file_a'),
1629 1629 ('+++', 'diff.file_b'),
1630 1630 ('@@', 'diff.hunk'),
1631 1631 ('-', 'diff.deleted'),
1632 1632 ('+', 'diff.inserted')]
1633 1633
1634 1634 for chunk in func(*args, **kw):
1635 1635 lines = chunk.split('\n')
1636 1636 for i, line in enumerate(lines):
1637 1637 if i != 0:
1638 1638 yield ('\n', '')
1639 1639 stripline = line
1640 1640 if line and line[0] in '+-':
1641 1641 # highlight trailing whitespace, but only in changed lines
1642 1642 stripline = line.rstrip()
1643 1643 for prefix, label in prefixes:
1644 1644 if stripline.startswith(prefix):
1645 1645 yield (stripline, label)
1646 1646 break
1647 1647 else:
1648 1648 yield (line, '')
1649 1649 if line != stripline:
1650 1650 yield (line[len(stripline):], 'diff.trailingwhitespace')
1651 1651
1652 1652 def diffui(*args, **kw):
1653 1653 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1654 1654 return difflabel(diff, *args, **kw)
1655 1655
1656 1656
1657 1657 def _addmodehdr(header, omode, nmode):
1658 1658 if omode != nmode:
1659 1659 header.append('old mode %s\n' % omode)
1660 1660 header.append('new mode %s\n' % nmode)
1661 1661
1662 1662 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1663 1663 copy, getfilectx, opts, losedatafn, prefix):
1664 1664
1665 1665 def join(f):
1666 1666 return os.path.join(prefix, f)
1667 1667
1668 1668 date1 = util.datestr(ctx1.date())
1669 1669 man1 = ctx1.manifest()
1670 1670
1671 1671 gone = set()
1672 1672 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1673 1673
1674 1674 copyto = dict([(v, k) for k, v in copy.items()])
1675 1675
1676 1676 if opts.git:
1677 1677 revs = None
1678 1678
1679 1679 for f in sorted(modified + added + removed):
1680 1680 to = None
1681 1681 tn = None
1682 1682 dodiff = True
1683 1683 header = []
1684 1684 if f in man1:
1685 1685 to = getfilectx(f, ctx1).data()
1686 1686 if f not in removed:
1687 1687 tn = getfilectx(f, ctx2).data()
1688 1688 a, b = f, f
1689 1689 if opts.git or losedatafn:
1690 1690 if f in added:
1691 1691 mode = gitmode[ctx2.flags(f)]
1692 1692 if f in copy or f in copyto:
1693 1693 if opts.git:
1694 1694 if f in copy:
1695 1695 a = copy[f]
1696 1696 else:
1697 1697 a = copyto[f]
1698 1698 omode = gitmode[man1.flags(a)]
1699 1699 _addmodehdr(header, omode, mode)
1700 1700 if a in removed and a not in gone:
1701 1701 op = 'rename'
1702 1702 gone.add(a)
1703 1703 else:
1704 1704 op = 'copy'
1705 1705 header.append('%s from %s\n' % (op, join(a)))
1706 1706 header.append('%s to %s\n' % (op, join(f)))
1707 1707 to = getfilectx(a, ctx1).data()
1708 1708 else:
1709 1709 losedatafn(f)
1710 1710 else:
1711 1711 if opts.git:
1712 1712 header.append('new file mode %s\n' % mode)
1713 1713 elif ctx2.flags(f):
1714 1714 losedatafn(f)
1715 1715 # In theory, if tn was copied or renamed we should check
1716 1716 # if the source is binary too but the copy record already
1717 1717 # forces git mode.
1718 1718 if util.binary(tn):
1719 1719 if opts.git:
1720 1720 dodiff = 'binary'
1721 1721 else:
1722 1722 losedatafn(f)
1723 1723 if not opts.git and not tn:
1724 1724 # regular diffs cannot represent new empty file
1725 1725 losedatafn(f)
1726 1726 elif f in removed:
1727 1727 if opts.git:
1728 1728 # have we already reported a copy above?
1729 1729 if ((f in copy and copy[f] in added
1730 1730 and copyto[copy[f]] == f) or
1731 1731 (f in copyto and copyto[f] in added
1732 1732 and copy[copyto[f]] == f)):
1733 1733 dodiff = False
1734 1734 else:
1735 1735 header.append('deleted file mode %s\n' %
1736 1736 gitmode[man1.flags(f)])
1737 1737 elif not to or util.binary(to):
1738 1738 # regular diffs cannot represent empty file deletion
1739 1739 losedatafn(f)
1740 1740 else:
1741 1741 oflag = man1.flags(f)
1742 1742 nflag = ctx2.flags(f)
1743 1743 binary = util.binary(to) or util.binary(tn)
1744 1744 if opts.git:
1745 1745 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1746 1746 if binary:
1747 1747 dodiff = 'binary'
1748 1748 elif binary or nflag != oflag:
1749 1749 losedatafn(f)
1750 1750 if opts.git:
1751 1751 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1752 1752
1753 1753 if dodiff:
1754 1754 if dodiff == 'binary':
1755 1755 text = b85diff(to, tn)
1756 1756 else:
1757 1757 text = mdiff.unidiff(to, date1,
1758 1758 # ctx2 date may be dynamic
1759 1759 tn, util.datestr(ctx2.date()),
1760 1760 join(a), join(b), revs, opts=opts)
1761 1761 if header and (text or len(header) > 1):
1762 1762 yield ''.join(header)
1763 1763 if text:
1764 1764 yield text
1765 1765
1766 1766 def diffstatsum(stats):
1767 1767 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1768 1768 for f, a, r, b in stats:
1769 1769 maxfile = max(maxfile, encoding.colwidth(f))
1770 1770 maxtotal = max(maxtotal, a + r)
1771 1771 addtotal += a
1772 1772 removetotal += r
1773 1773 binary = binary or b
1774 1774
1775 1775 return maxfile, maxtotal, addtotal, removetotal, binary
1776 1776
1777 1777 def diffstatdata(lines):
1778 1778 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1779 1779
1780 1780 results = []
1781 1781 filename, adds, removes = None, 0, 0
1782 1782
1783 1783 def addresult():
1784 1784 if filename:
1785 1785 isbinary = adds == 0 and removes == 0
1786 1786 results.append((filename, adds, removes, isbinary))
1787 1787
1788 1788 for line in lines:
1789 1789 if line.startswith('diff'):
1790 1790 addresult()
1791 1791 # set numbers to 0 anyway when starting new file
1792 1792 adds, removes = 0, 0
1793 1793 if line.startswith('diff --git'):
1794 1794 filename = gitre.search(line).group(1)
1795 1795 elif line.startswith('diff -r'):
1796 1796 # format: "diff -r ... -r ... filename"
1797 1797 filename = diffre.search(line).group(1)
1798 1798 elif line.startswith('+') and not line.startswith('+++'):
1799 1799 adds += 1
1800 1800 elif line.startswith('-') and not line.startswith('---'):
1801 1801 removes += 1
1802 1802 addresult()
1803 1803 return results
1804 1804
1805 1805 def diffstat(lines, width=80, git=False):
1806 1806 output = []
1807 1807 stats = diffstatdata(lines)
1808 1808 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1809 1809
1810 1810 countwidth = len(str(maxtotal))
1811 1811 if hasbinary and countwidth < 3:
1812 1812 countwidth = 3
1813 1813 graphwidth = width - countwidth - maxname - 6
1814 1814 if graphwidth < 10:
1815 1815 graphwidth = 10
1816 1816
1817 1817 def scale(i):
1818 1818 if maxtotal <= graphwidth:
1819 1819 return i
1820 1820 # If diffstat runs out of room it doesn't print anything,
1821 1821 # which isn't very useful, so always print at least one + or -
1822 1822 # if there were at least some changes.
1823 1823 return max(i * graphwidth // maxtotal, int(bool(i)))
1824 1824
1825 1825 for filename, adds, removes, isbinary in stats:
1826 1826 if git and isbinary:
1827 1827 count = 'Bin'
1828 1828 else:
1829 1829 count = adds + removes
1830 1830 pluses = '+' * scale(adds)
1831 1831 minuses = '-' * scale(removes)
1832 1832 output.append(' %s%s | %*s %s%s\n' %
1833 1833 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1834 1834 countwidth, count, pluses, minuses))
1835 1835
1836 1836 if stats:
1837 1837 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1838 1838 % (len(stats), totaladds, totalremoves))
1839 1839
1840 1840 return ''.join(output)
1841 1841
1842 1842 def diffstatui(*args, **kw):
1843 1843 '''like diffstat(), but yields 2-tuples of (output, label) for
1844 1844 ui.write()
1845 1845 '''
1846 1846
1847 1847 for line in diffstat(*args, **kw).splitlines():
1848 1848 if line and line[-1] in '+-':
1849 1849 name, graph = line.rsplit(' ', 1)
1850 1850 yield (name + ' ', '')
1851 1851 m = re.search(r'\++', graph)
1852 1852 if m:
1853 1853 yield (m.group(0), 'diffstat.inserted')
1854 1854 m = re.search(r'-+', graph)
1855 1855 if m:
1856 1856 yield (m.group(0), 'diffstat.deleted')
1857 1857 else:
1858 1858 yield (line, '')
1859 1859 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now