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