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