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