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