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