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