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