##// END OF EJS Templates
patch: initialize all attributes of the hunk class
Benoit Boissinot -
r9682:bd70f645 default
parent child Browse files
Show More
@@ -1,1424 +1,1426 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 self.starta = self.lena = None
484 self.startb = self.lenb = None
483 485 if context:
484 486 self.read_context_hunk(lr)
485 487 else:
486 488 self.read_unified_hunk(lr)
487 489 self.create = create
488 490 self.remove = remove and not create
489 491
490 492 def read_unified_hunk(self, lr):
491 493 m = unidesc.match(self.desc)
492 494 if not m:
493 495 raise PatchError(_("bad hunk #%d") % self.number)
494 496 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
495 497 if self.lena is None:
496 498 self.lena = 1
497 499 else:
498 500 self.lena = int(self.lena)
499 501 if self.lenb is None:
500 502 self.lenb = 1
501 503 else:
502 504 self.lenb = int(self.lenb)
503 505 self.starta = int(self.starta)
504 506 self.startb = int(self.startb)
505 507 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
506 508 # if we hit eof before finishing out the hunk, the last line will
507 509 # be zero length. Lets try to fix it up.
508 510 while len(self.hunk[-1]) == 0:
509 511 del self.hunk[-1]
510 512 del self.a[-1]
511 513 del self.b[-1]
512 514 self.lena -= 1
513 515 self.lenb -= 1
514 516
515 517 def read_context_hunk(self, lr):
516 518 self.desc = lr.readline()
517 519 m = contextdesc.match(self.desc)
518 520 if not m:
519 521 raise PatchError(_("bad hunk #%d") % self.number)
520 522 foo, self.starta, foo2, aend, foo3 = m.groups()
521 523 self.starta = int(self.starta)
522 524 if aend is None:
523 525 aend = self.starta
524 526 self.lena = int(aend) - self.starta
525 527 if self.starta:
526 528 self.lena += 1
527 529 for x in xrange(self.lena):
528 530 l = lr.readline()
529 531 if l.startswith('---'):
530 532 lr.push(l)
531 533 break
532 534 s = l[2:]
533 535 if l.startswith('- ') or l.startswith('! '):
534 536 u = '-' + s
535 537 elif l.startswith(' '):
536 538 u = ' ' + s
537 539 else:
538 540 raise PatchError(_("bad hunk #%d old text line %d") %
539 541 (self.number, x))
540 542 self.a.append(u)
541 543 self.hunk.append(u)
542 544
543 545 l = lr.readline()
544 546 if l.startswith('\ '):
545 547 s = self.a[-1][:-1]
546 548 self.a[-1] = s
547 549 self.hunk[-1] = s
548 550 l = lr.readline()
549 551 m = contextdesc.match(l)
550 552 if not m:
551 553 raise PatchError(_("bad hunk #%d") % self.number)
552 554 foo, self.startb, foo2, bend, foo3 = m.groups()
553 555 self.startb = int(self.startb)
554 556 if bend is None:
555 557 bend = self.startb
556 558 self.lenb = int(bend) - self.startb
557 559 if self.startb:
558 560 self.lenb += 1
559 561 hunki = 1
560 562 for x in xrange(self.lenb):
561 563 l = lr.readline()
562 564 if l.startswith('\ '):
563 565 s = self.b[-1][:-1]
564 566 self.b[-1] = s
565 567 self.hunk[hunki-1] = s
566 568 continue
567 569 if not l:
568 570 lr.push(l)
569 571 break
570 572 s = l[2:]
571 573 if l.startswith('+ ') or l.startswith('! '):
572 574 u = '+' + s
573 575 elif l.startswith(' '):
574 576 u = ' ' + s
575 577 elif len(self.b) == 0:
576 578 # this can happen when the hunk does not add any lines
577 579 lr.push(l)
578 580 break
579 581 else:
580 582 raise PatchError(_("bad hunk #%d old text line %d") %
581 583 (self.number, x))
582 584 self.b.append(s)
583 585 while True:
584 586 if hunki >= len(self.hunk):
585 587 h = ""
586 588 else:
587 589 h = self.hunk[hunki]
588 590 hunki += 1
589 591 if h == u:
590 592 break
591 593 elif h.startswith('-'):
592 594 continue
593 595 else:
594 596 self.hunk.insert(hunki-1, u)
595 597 break
596 598
597 599 if not self.a:
598 600 # this happens when lines were only added to the hunk
599 601 for x in self.hunk:
600 602 if x.startswith('-') or x.startswith(' '):
601 603 self.a.append(x)
602 604 if not self.b:
603 605 # this happens when lines were only deleted from the hunk
604 606 for x in self.hunk:
605 607 if x.startswith('+') or x.startswith(' '):
606 608 self.b.append(x[1:])
607 609 # @@ -start,len +start,len @@
608 610 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
609 611 self.startb, self.lenb)
610 612 self.hunk[0] = self.desc
611 613
612 614 def fix_newline(self):
613 615 diffhelpers.fix_newline(self.hunk, self.a, self.b)
614 616
615 617 def complete(self):
616 618 return len(self.a) == self.lena and len(self.b) == self.lenb
617 619
618 620 def createfile(self):
619 621 return self.starta == 0 and self.lena == 0 and self.create
620 622
621 623 def rmfile(self):
622 624 return self.startb == 0 and self.lenb == 0 and self.remove
623 625
624 626 def fuzzit(self, l, fuzz, toponly):
625 627 # this removes context lines from the top and bottom of list 'l'. It
626 628 # checks the hunk to make sure only context lines are removed, and then
627 629 # returns a new shortened list of lines.
628 630 fuzz = min(fuzz, len(l)-1)
629 631 if fuzz:
630 632 top = 0
631 633 bot = 0
632 634 hlen = len(self.hunk)
633 635 for x in xrange(hlen-1):
634 636 # the hunk starts with the @@ line, so use x+1
635 637 if self.hunk[x+1][0] == ' ':
636 638 top += 1
637 639 else:
638 640 break
639 641 if not toponly:
640 642 for x in xrange(hlen-1):
641 643 if self.hunk[hlen-bot-1][0] == ' ':
642 644 bot += 1
643 645 else:
644 646 break
645 647
646 648 # top and bot now count context in the hunk
647 649 # adjust them if either one is short
648 650 context = max(top, bot, 3)
649 651 if bot < context:
650 652 bot = max(0, fuzz - (context - bot))
651 653 else:
652 654 bot = min(fuzz, bot)
653 655 if top < context:
654 656 top = max(0, fuzz - (context - top))
655 657 else:
656 658 top = min(fuzz, top)
657 659
658 660 return l[top:len(l)-bot]
659 661 return l
660 662
661 663 def old(self, fuzz=0, toponly=False):
662 664 return self.fuzzit(self.a, fuzz, toponly)
663 665
664 666 def newctrl(self):
665 667 res = []
666 668 for x in self.hunk:
667 669 c = x[0]
668 670 if c == ' ' or c == '+':
669 671 res.append(x)
670 672 return res
671 673
672 674 def new(self, fuzz=0, toponly=False):
673 675 return self.fuzzit(self.b, fuzz, toponly)
674 676
675 677 class binhunk:
676 678 'A binary patch file. Only understands literals so far.'
677 679 def __init__(self, gitpatch):
678 680 self.gitpatch = gitpatch
679 681 self.text = None
680 682 self.hunk = ['GIT binary patch\n']
681 683
682 684 def createfile(self):
683 685 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
684 686
685 687 def rmfile(self):
686 688 return self.gitpatch.op == 'DELETE'
687 689
688 690 def complete(self):
689 691 return self.text is not None
690 692
691 693 def new(self):
692 694 return [self.text]
693 695
694 696 def extract(self, lr):
695 697 line = lr.readline()
696 698 self.hunk.append(line)
697 699 while line and not line.startswith('literal '):
698 700 line = lr.readline()
699 701 self.hunk.append(line)
700 702 if not line:
701 703 raise PatchError(_('could not extract binary patch'))
702 704 size = int(line[8:].rstrip())
703 705 dec = []
704 706 line = lr.readline()
705 707 self.hunk.append(line)
706 708 while len(line) > 1:
707 709 l = line[0]
708 710 if l <= 'Z' and l >= 'A':
709 711 l = ord(l) - ord('A') + 1
710 712 else:
711 713 l = ord(l) - ord('a') + 27
712 714 dec.append(base85.b85decode(line[1:-1])[:l])
713 715 line = lr.readline()
714 716 self.hunk.append(line)
715 717 text = zlib.decompress(''.join(dec))
716 718 if len(text) != size:
717 719 raise PatchError(_('binary patch is %d bytes, not %d') %
718 720 len(text), size)
719 721 self.text = text
720 722
721 723 def parsefilename(str):
722 724 # --- filename \t|space stuff
723 725 s = str[4:].rstrip('\r\n')
724 726 i = s.find('\t')
725 727 if i < 0:
726 728 i = s.find(' ')
727 729 if i < 0:
728 730 return s
729 731 return s[:i]
730 732
731 733 def selectfile(afile_orig, bfile_orig, hunk, strip):
732 734 def pathstrip(path, count=1):
733 735 pathlen = len(path)
734 736 i = 0
735 737 if count == 0:
736 738 return '', path.rstrip()
737 739 while count > 0:
738 740 i = path.find('/', i)
739 741 if i == -1:
740 742 raise PatchError(_("unable to strip away %d dirs from %s") %
741 743 (count, path))
742 744 i += 1
743 745 # consume '//' in the path
744 746 while i < pathlen - 1 and path[i] == '/':
745 747 i += 1
746 748 count -= 1
747 749 return path[:i].lstrip(), path[i:].rstrip()
748 750
749 751 nulla = afile_orig == "/dev/null"
750 752 nullb = bfile_orig == "/dev/null"
751 753 abase, afile = pathstrip(afile_orig, strip)
752 754 gooda = not nulla and util.lexists(afile)
753 755 bbase, bfile = pathstrip(bfile_orig, strip)
754 756 if afile == bfile:
755 757 goodb = gooda
756 758 else:
757 759 goodb = not nullb and os.path.exists(bfile)
758 760 createfunc = hunk.createfile
759 761 missing = not goodb and not gooda and not createfunc()
760 762
761 763 # some diff programs apparently produce create patches where the
762 764 # afile is not /dev/null, but rather the same name as the bfile
763 765 if missing and afile == bfile:
764 766 # this isn't very pretty
765 767 hunk.create = True
766 768 if createfunc():
767 769 missing = False
768 770 else:
769 771 hunk.create = False
770 772
771 773 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
772 774 # diff is between a file and its backup. In this case, the original
773 775 # file should be patched (see original mpatch code).
774 776 isbackup = (abase == bbase and bfile.startswith(afile))
775 777 fname = None
776 778 if not missing:
777 779 if gooda and goodb:
778 780 fname = isbackup and afile or bfile
779 781 elif gooda:
780 782 fname = afile
781 783
782 784 if not fname:
783 785 if not nullb:
784 786 fname = isbackup and afile or bfile
785 787 elif not nulla:
786 788 fname = afile
787 789 else:
788 790 raise PatchError(_("undefined source and destination files"))
789 791
790 792 return fname, missing
791 793
792 794 def scangitpatch(lr, firstline):
793 795 """
794 796 Git patches can emit:
795 797 - rename a to b
796 798 - change b
797 799 - copy a to c
798 800 - change c
799 801
800 802 We cannot apply this sequence as-is, the renamed 'a' could not be
801 803 found for it would have been renamed already. And we cannot copy
802 804 from 'b' instead because 'b' would have been changed already. So
803 805 we scan the git patch for copy and rename commands so we can
804 806 perform the copies ahead of time.
805 807 """
806 808 pos = 0
807 809 try:
808 810 pos = lr.fp.tell()
809 811 fp = lr.fp
810 812 except IOError:
811 813 fp = cStringIO.StringIO(lr.fp.read())
812 814 gitlr = linereader(fp, lr.textmode)
813 815 gitlr.push(firstline)
814 816 (dopatch, gitpatches) = readgitpatch(gitlr)
815 817 fp.seek(pos)
816 818 return dopatch, gitpatches
817 819
818 820 def iterhunks(ui, fp, sourcefile=None, textmode=False):
819 821 """Read a patch and yield the following events:
820 822 - ("file", afile, bfile, firsthunk): select a new target file.
821 823 - ("hunk", hunk): a new hunk is ready to be applied, follows a
822 824 "file" event.
823 825 - ("git", gitchanges): current diff is in git format, gitchanges
824 826 maps filenames to gitpatch records. Unique event.
825 827
826 828 If textmode is True, input line-endings are normalized to LF.
827 829 """
828 830 changed = {}
829 831 current_hunk = None
830 832 afile = ""
831 833 bfile = ""
832 834 state = None
833 835 hunknum = 0
834 836 emitfile = False
835 837 git = False
836 838
837 839 # our states
838 840 BFILE = 1
839 841 context = None
840 842 lr = linereader(fp, textmode)
841 843 dopatch = True
842 844 # gitworkdone is True if a git operation (copy, rename, ...) was
843 845 # performed already for the current file. Useful when the file
844 846 # section may have no hunk.
845 847 gitworkdone = False
846 848
847 849 while True:
848 850 newfile = False
849 851 x = lr.readline()
850 852 if not x:
851 853 break
852 854 if current_hunk:
853 855 if x.startswith('\ '):
854 856 current_hunk.fix_newline()
855 857 yield 'hunk', current_hunk
856 858 current_hunk = None
857 859 gitworkdone = False
858 860 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
859 861 ((context is not False) and x.startswith('***************')))):
860 862 try:
861 863 if context is None and x.startswith('***************'):
862 864 context = True
863 865 gpatch = changed.get(bfile)
864 866 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
865 867 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
866 868 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
867 869 except PatchError, err:
868 870 ui.debug(err)
869 871 current_hunk = None
870 872 continue
871 873 hunknum += 1
872 874 if emitfile:
873 875 emitfile = False
874 876 yield 'file', (afile, bfile, current_hunk)
875 877 elif state == BFILE and x.startswith('GIT binary patch'):
876 878 current_hunk = binhunk(changed[bfile])
877 879 hunknum += 1
878 880 if emitfile:
879 881 emitfile = False
880 882 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
881 883 current_hunk.extract(lr)
882 884 elif x.startswith('diff --git'):
883 885 # check for git diff, scanning the whole patch file if needed
884 886 m = gitre.match(x)
885 887 if m:
886 888 afile, bfile = m.group(1, 2)
887 889 if not git:
888 890 git = True
889 891 dopatch, gitpatches = scangitpatch(lr, x)
890 892 yield 'git', gitpatches
891 893 for gp in gitpatches:
892 894 changed[gp.path] = gp
893 895 # else error?
894 896 # copy/rename + modify should modify target, not source
895 897 gp = changed.get(bfile)
896 898 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
897 899 afile = bfile
898 900 gitworkdone = True
899 901 newfile = True
900 902 elif x.startswith('---'):
901 903 # check for a unified diff
902 904 l2 = lr.readline()
903 905 if not l2.startswith('+++'):
904 906 lr.push(l2)
905 907 continue
906 908 newfile = True
907 909 context = False
908 910 afile = parsefilename(x)
909 911 bfile = parsefilename(l2)
910 912 elif x.startswith('***'):
911 913 # check for a context diff
912 914 l2 = lr.readline()
913 915 if not l2.startswith('---'):
914 916 lr.push(l2)
915 917 continue
916 918 l3 = lr.readline()
917 919 lr.push(l3)
918 920 if not l3.startswith("***************"):
919 921 lr.push(l2)
920 922 continue
921 923 newfile = True
922 924 context = True
923 925 afile = parsefilename(x)
924 926 bfile = parsefilename(l2)
925 927
926 928 if newfile:
927 929 emitfile = True
928 930 state = BFILE
929 931 hunknum = 0
930 932 if current_hunk:
931 933 if current_hunk.complete():
932 934 yield 'hunk', current_hunk
933 935 else:
934 936 raise PatchError(_("malformed patch %s %s") % (afile,
935 937 current_hunk.desc))
936 938
937 939 if hunknum == 0 and dopatch and not gitworkdone:
938 940 raise NoHunks
939 941
940 942 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eol=None):
941 943 """
942 944 Reads a patch from fp and tries to apply it.
943 945
944 946 The dict 'changed' is filled in with all of the filenames changed
945 947 by the patch. Returns 0 for a clean patch, -1 if any rejects were
946 948 found and 1 if there was any fuzz.
947 949
948 950 If 'eol' is None, the patch content and patched file are read in
949 951 binary mode. Otherwise, line endings are ignored when patching then
950 952 normalized to 'eol' (usually '\n' or \r\n').
951 953 """
952 954 rejects = 0
953 955 err = 0
954 956 current_file = None
955 957 gitpatches = None
956 958 opener = util.opener(os.getcwd())
957 959 textmode = eol is not None
958 960
959 961 def closefile():
960 962 if not current_file:
961 963 return 0
962 964 current_file.close()
963 965 return len(current_file.rej)
964 966
965 967 for state, values in iterhunks(ui, fp, sourcefile, textmode):
966 968 if state == 'hunk':
967 969 if not current_file:
968 970 continue
969 971 current_hunk = values
970 972 ret = current_file.apply(current_hunk)
971 973 if ret >= 0:
972 974 changed.setdefault(current_file.fname, None)
973 975 if ret > 0:
974 976 err = 1
975 977 elif state == 'file':
976 978 rejects += closefile()
977 979 afile, bfile, first_hunk = values
978 980 try:
979 981 if sourcefile:
980 982 current_file = patchfile(ui, sourcefile, opener, eol=eol)
981 983 else:
982 984 current_file, missing = selectfile(afile, bfile, first_hunk,
983 985 strip)
984 986 current_file = patchfile(ui, current_file, opener, missing, eol)
985 987 except PatchError, err:
986 988 ui.warn(str(err) + '\n')
987 989 current_file, current_hunk = None, None
988 990 rejects += 1
989 991 continue
990 992 elif state == 'git':
991 993 gitpatches = values
992 994 cwd = os.getcwd()
993 995 for gp in gitpatches:
994 996 if gp.op in ('COPY', 'RENAME'):
995 997 copyfile(gp.oldpath, gp.path, cwd)
996 998 changed[gp.path] = gp
997 999 else:
998 1000 raise util.Abort(_('unsupported parser state: %s') % state)
999 1001
1000 1002 rejects += closefile()
1001 1003
1002 1004 if rejects:
1003 1005 return -1
1004 1006 return err
1005 1007
1006 1008 def diffopts(ui, opts={}, untrusted=False):
1007 1009 def get(key, name=None, getter=ui.configbool):
1008 1010 return (opts.get(key) or
1009 1011 getter('diff', name or key, None, untrusted=untrusted))
1010 1012 return mdiff.diffopts(
1011 1013 text=opts.get('text'),
1012 1014 git=get('git'),
1013 1015 nodates=get('nodates'),
1014 1016 showfunc=get('show_function', 'showfunc'),
1015 1017 ignorews=get('ignore_all_space', 'ignorews'),
1016 1018 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1017 1019 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1018 1020 context=get('unified', getter=ui.config))
1019 1021
1020 1022 def updatedir(ui, repo, patches, similarity=0):
1021 1023 '''Update dirstate after patch application according to metadata'''
1022 1024 if not patches:
1023 1025 return
1024 1026 copies = []
1025 1027 removes = set()
1026 1028 cfiles = patches.keys()
1027 1029 cwd = repo.getcwd()
1028 1030 if cwd:
1029 1031 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1030 1032 for f in patches:
1031 1033 gp = patches[f]
1032 1034 if not gp:
1033 1035 continue
1034 1036 if gp.op == 'RENAME':
1035 1037 copies.append((gp.oldpath, gp.path))
1036 1038 removes.add(gp.oldpath)
1037 1039 elif gp.op == 'COPY':
1038 1040 copies.append((gp.oldpath, gp.path))
1039 1041 elif gp.op == 'DELETE':
1040 1042 removes.add(gp.path)
1041 1043 for src, dst in copies:
1042 1044 repo.copy(src, dst)
1043 1045 if (not similarity) and removes:
1044 1046 repo.remove(sorted(removes), True)
1045 1047 for f in patches:
1046 1048 gp = patches[f]
1047 1049 if gp and gp.mode:
1048 1050 islink, isexec = gp.mode
1049 1051 dst = repo.wjoin(gp.path)
1050 1052 # patch won't create empty files
1051 1053 if gp.op == 'ADD' and not os.path.exists(dst):
1052 1054 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1053 1055 repo.wwrite(gp.path, '', flags)
1054 1056 elif gp.op != 'DELETE':
1055 1057 util.set_flags(dst, islink, isexec)
1056 1058 cmdutil.addremove(repo, cfiles, similarity=similarity)
1057 1059 files = patches.keys()
1058 1060 files.extend([r for r in removes if r not in files])
1059 1061 return sorted(files)
1060 1062
1061 1063 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1062 1064 """use <patcher> to apply <patchname> to the working directory.
1063 1065 returns whether patch was applied with fuzz factor."""
1064 1066
1065 1067 fuzz = False
1066 1068 if cwd:
1067 1069 args.append('-d %s' % util.shellquote(cwd))
1068 1070 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1069 1071 util.shellquote(patchname)))
1070 1072
1071 1073 for line in fp:
1072 1074 line = line.rstrip()
1073 1075 ui.note(line + '\n')
1074 1076 if line.startswith('patching file '):
1075 1077 pf = util.parse_patch_output(line)
1076 1078 printed_file = False
1077 1079 files.setdefault(pf, None)
1078 1080 elif line.find('with fuzz') >= 0:
1079 1081 fuzz = True
1080 1082 if not printed_file:
1081 1083 ui.warn(pf + '\n')
1082 1084 printed_file = True
1083 1085 ui.warn(line + '\n')
1084 1086 elif line.find('saving rejects to file') >= 0:
1085 1087 ui.warn(line + '\n')
1086 1088 elif line.find('FAILED') >= 0:
1087 1089 if not printed_file:
1088 1090 ui.warn(pf + '\n')
1089 1091 printed_file = True
1090 1092 ui.warn(line + '\n')
1091 1093 code = fp.close()
1092 1094 if code:
1093 1095 raise PatchError(_("patch command failed: %s") %
1094 1096 util.explain_exit(code)[0])
1095 1097 return fuzz
1096 1098
1097 1099 def internalpatch(patchobj, ui, strip, cwd, files={}, eolmode='strict'):
1098 1100 """use builtin patch to apply <patchobj> to the working directory.
1099 1101 returns whether patch was applied with fuzz factor."""
1100 1102
1101 1103 if eolmode is None:
1102 1104 eolmode = ui.config('patch', 'eol', 'strict')
1103 1105 try:
1104 1106 eol = {'strict': None, 'crlf': '\r\n', 'lf': '\n'}[eolmode.lower()]
1105 1107 except KeyError:
1106 1108 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1107 1109
1108 1110 try:
1109 1111 fp = open(patchobj, 'rb')
1110 1112 except TypeError:
1111 1113 fp = patchobj
1112 1114 if cwd:
1113 1115 curdir = os.getcwd()
1114 1116 os.chdir(cwd)
1115 1117 try:
1116 1118 ret = applydiff(ui, fp, files, strip=strip, eol=eol)
1117 1119 finally:
1118 1120 if cwd:
1119 1121 os.chdir(curdir)
1120 1122 if ret < 0:
1121 1123 raise PatchError
1122 1124 return ret > 0
1123 1125
1124 1126 def patch(patchname, ui, strip=1, cwd=None, files={}, eolmode='strict'):
1125 1127 """Apply <patchname> to the working directory.
1126 1128
1127 1129 'eolmode' specifies how end of lines should be handled. It can be:
1128 1130 - 'strict': inputs are read in binary mode, EOLs are preserved
1129 1131 - 'crlf': EOLs are ignored when patching and reset to CRLF
1130 1132 - 'lf': EOLs are ignored when patching and reset to LF
1131 1133 - None: get it from user settings, default to 'strict'
1132 1134 'eolmode' is ignored when using an external patcher program.
1133 1135
1134 1136 Returns whether patch was applied with fuzz factor.
1135 1137 """
1136 1138 patcher = ui.config('ui', 'patch')
1137 1139 args = []
1138 1140 try:
1139 1141 if patcher:
1140 1142 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1141 1143 files)
1142 1144 else:
1143 1145 try:
1144 1146 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1145 1147 except NoHunks:
1146 1148 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch'
1147 1149 ui.debug('no valid hunks found; trying with %r instead\n' %
1148 1150 patcher)
1149 1151 if util.needbinarypatch():
1150 1152 args.append('--binary')
1151 1153 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1152 1154 files)
1153 1155 except PatchError, err:
1154 1156 s = str(err)
1155 1157 if s:
1156 1158 raise util.Abort(s)
1157 1159 else:
1158 1160 raise util.Abort(_('patch failed to apply'))
1159 1161
1160 1162 def b85diff(to, tn):
1161 1163 '''print base85-encoded binary diff'''
1162 1164 def gitindex(text):
1163 1165 if not text:
1164 1166 return '0' * 40
1165 1167 l = len(text)
1166 1168 s = util.sha1('blob %d\0' % l)
1167 1169 s.update(text)
1168 1170 return s.hexdigest()
1169 1171
1170 1172 def fmtline(line):
1171 1173 l = len(line)
1172 1174 if l <= 26:
1173 1175 l = chr(ord('A') + l - 1)
1174 1176 else:
1175 1177 l = chr(l - 26 + ord('a') - 1)
1176 1178 return '%c%s\n' % (l, base85.b85encode(line, True))
1177 1179
1178 1180 def chunk(text, csize=52):
1179 1181 l = len(text)
1180 1182 i = 0
1181 1183 while i < l:
1182 1184 yield text[i:i+csize]
1183 1185 i += csize
1184 1186
1185 1187 tohash = gitindex(to)
1186 1188 tnhash = gitindex(tn)
1187 1189 if tohash == tnhash:
1188 1190 return ""
1189 1191
1190 1192 # TODO: deltas
1191 1193 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1192 1194 (tohash, tnhash, len(tn))]
1193 1195 for l in chunk(zlib.compress(tn)):
1194 1196 ret.append(fmtline(l))
1195 1197 ret.append('\n')
1196 1198 return ''.join(ret)
1197 1199
1198 1200 def _addmodehdr(header, omode, nmode):
1199 1201 if omode != nmode:
1200 1202 header.append('old mode %s\n' % omode)
1201 1203 header.append('new mode %s\n' % nmode)
1202 1204
1203 1205 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None):
1204 1206 '''yields diff of changes to files between two nodes, or node and
1205 1207 working directory.
1206 1208
1207 1209 if node1 is None, use first dirstate parent instead.
1208 1210 if node2 is None, compare node1 with working directory.'''
1209 1211
1210 1212 if opts is None:
1211 1213 opts = mdiff.defaultopts
1212 1214
1213 1215 if not node1:
1214 1216 node1 = repo.dirstate.parents()[0]
1215 1217
1216 1218 def lrugetfilectx():
1217 1219 cache = {}
1218 1220 order = []
1219 1221 def getfilectx(f, ctx):
1220 1222 fctx = ctx.filectx(f, filelog=cache.get(f))
1221 1223 if f not in cache:
1222 1224 if len(cache) > 20:
1223 1225 del cache[order.pop(0)]
1224 1226 cache[f] = fctx._filelog
1225 1227 else:
1226 1228 order.remove(f)
1227 1229 order.append(f)
1228 1230 return fctx
1229 1231 return getfilectx
1230 1232 getfilectx = lrugetfilectx()
1231 1233
1232 1234 ctx1 = repo[node1]
1233 1235 ctx2 = repo[node2]
1234 1236
1235 1237 if not changes:
1236 1238 changes = repo.status(ctx1, ctx2, match=match)
1237 1239 modified, added, removed = changes[:3]
1238 1240
1239 1241 if not modified and not added and not removed:
1240 1242 return
1241 1243
1242 1244 date1 = util.datestr(ctx1.date())
1243 1245 man1 = ctx1.manifest()
1244 1246
1245 1247 if repo.ui.quiet:
1246 1248 r = None
1247 1249 else:
1248 1250 hexfunc = repo.ui.debugflag and hex or short
1249 1251 r = [hexfunc(node) for node in [node1, node2] if node]
1250 1252
1251 1253 if opts.git:
1252 1254 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
1253 1255 copy = copy.copy()
1254 1256 for k, v in copy.items():
1255 1257 copy[v] = k
1256 1258
1257 1259 gone = set()
1258 1260 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1259 1261
1260 1262 for f in sorted(modified + added + removed):
1261 1263 to = None
1262 1264 tn = None
1263 1265 dodiff = True
1264 1266 header = []
1265 1267 if f in man1:
1266 1268 to = getfilectx(f, ctx1).data()
1267 1269 if f not in removed:
1268 1270 tn = getfilectx(f, ctx2).data()
1269 1271 a, b = f, f
1270 1272 if opts.git:
1271 1273 if f in added:
1272 1274 mode = gitmode[ctx2.flags(f)]
1273 1275 if f in copy:
1274 1276 a = copy[f]
1275 1277 omode = gitmode[man1.flags(a)]
1276 1278 _addmodehdr(header, omode, mode)
1277 1279 if a in removed and a not in gone:
1278 1280 op = 'rename'
1279 1281 gone.add(a)
1280 1282 else:
1281 1283 op = 'copy'
1282 1284 header.append('%s from %s\n' % (op, a))
1283 1285 header.append('%s to %s\n' % (op, f))
1284 1286 to = getfilectx(a, ctx1).data()
1285 1287 else:
1286 1288 header.append('new file mode %s\n' % mode)
1287 1289 if util.binary(tn):
1288 1290 dodiff = 'binary'
1289 1291 elif f in removed:
1290 1292 # have we already reported a copy above?
1291 1293 if f in copy and copy[f] in added and copy[copy[f]] == f:
1292 1294 dodiff = False
1293 1295 else:
1294 1296 header.append('deleted file mode %s\n' %
1295 1297 gitmode[man1.flags(f)])
1296 1298 else:
1297 1299 omode = gitmode[man1.flags(f)]
1298 1300 nmode = gitmode[ctx2.flags(f)]
1299 1301 _addmodehdr(header, omode, nmode)
1300 1302 if util.binary(to) or util.binary(tn):
1301 1303 dodiff = 'binary'
1302 1304 r = None
1303 1305 header.insert(0, mdiff.diffline(r, a, b, opts))
1304 1306 if dodiff:
1305 1307 if dodiff == 'binary':
1306 1308 text = b85diff(to, tn)
1307 1309 else:
1308 1310 text = mdiff.unidiff(to, date1,
1309 1311 # ctx2 date may be dynamic
1310 1312 tn, util.datestr(ctx2.date()),
1311 1313 a, b, r, opts=opts)
1312 1314 if header and (text or len(header) > 1):
1313 1315 yield ''.join(header)
1314 1316 if text:
1315 1317 yield text
1316 1318
1317 1319 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1318 1320 opts=None):
1319 1321 '''export changesets as hg patches.'''
1320 1322
1321 1323 total = len(revs)
1322 1324 revwidth = max([len(str(rev)) for rev in revs])
1323 1325
1324 1326 def single(rev, seqno, fp):
1325 1327 ctx = repo[rev]
1326 1328 node = ctx.node()
1327 1329 parents = [p.node() for p in ctx.parents() if p]
1328 1330 branch = ctx.branch()
1329 1331 if switch_parent:
1330 1332 parents.reverse()
1331 1333 prev = (parents and parents[0]) or nullid
1332 1334
1333 1335 if not fp:
1334 1336 fp = cmdutil.make_file(repo, template, node, total=total,
1335 1337 seqno=seqno, revwidth=revwidth,
1336 1338 mode='ab')
1337 1339 if fp != sys.stdout and hasattr(fp, 'name'):
1338 1340 repo.ui.note("%s\n" % fp.name)
1339 1341
1340 1342 fp.write("# HG changeset patch\n")
1341 1343 fp.write("# User %s\n" % ctx.user())
1342 1344 fp.write("# Date %d %d\n" % ctx.date())
1343 1345 if branch and (branch != 'default'):
1344 1346 fp.write("# Branch %s\n" % branch)
1345 1347 fp.write("# Node ID %s\n" % hex(node))
1346 1348 fp.write("# Parent %s\n" % hex(prev))
1347 1349 if len(parents) > 1:
1348 1350 fp.write("# Parent %s\n" % hex(parents[1]))
1349 1351 fp.write(ctx.description().rstrip())
1350 1352 fp.write("\n\n")
1351 1353
1352 1354 for chunk in diff(repo, prev, node, opts=opts):
1353 1355 fp.write(chunk)
1354 1356
1355 1357 for seqno, rev in enumerate(revs):
1356 1358 single(rev, seqno+1, fp)
1357 1359
1358 1360 def diffstatdata(lines):
1359 1361 filename, adds, removes = None, 0, 0
1360 1362 for line in lines:
1361 1363 if line.startswith('diff'):
1362 1364 if filename:
1363 1365 isbinary = adds == 0 and removes == 0
1364 1366 yield (filename, adds, removes, isbinary)
1365 1367 # set numbers to 0 anyway when starting new file
1366 1368 adds, removes = 0, 0
1367 1369 if line.startswith('diff --git'):
1368 1370 filename = gitre.search(line).group(1)
1369 1371 else:
1370 1372 # format: "diff -r ... -r ... filename"
1371 1373 filename = line.split(None, 5)[-1]
1372 1374 elif line.startswith('+') and not line.startswith('+++'):
1373 1375 adds += 1
1374 1376 elif line.startswith('-') and not line.startswith('---'):
1375 1377 removes += 1
1376 1378 if filename:
1377 1379 isbinary = adds == 0 and removes == 0
1378 1380 yield (filename, adds, removes, isbinary)
1379 1381
1380 1382 def diffstat(lines, width=80, git=False):
1381 1383 output = []
1382 1384 stats = list(diffstatdata(lines))
1383 1385
1384 1386 maxtotal, maxname = 0, 0
1385 1387 totaladds, totalremoves = 0, 0
1386 1388 hasbinary = False
1387 1389 for filename, adds, removes, isbinary in stats:
1388 1390 totaladds += adds
1389 1391 totalremoves += removes
1390 1392 maxname = max(maxname, len(filename))
1391 1393 maxtotal = max(maxtotal, adds+removes)
1392 1394 if isbinary:
1393 1395 hasbinary = True
1394 1396
1395 1397 countwidth = len(str(maxtotal))
1396 1398 if hasbinary and countwidth < 3:
1397 1399 countwidth = 3
1398 1400 graphwidth = width - countwidth - maxname - 6
1399 1401 if graphwidth < 10:
1400 1402 graphwidth = 10
1401 1403
1402 1404 def scale(i):
1403 1405 if maxtotal <= graphwidth:
1404 1406 return i
1405 1407 # If diffstat runs out of room it doesn't print anything,
1406 1408 # which isn't very useful, so always print at least one + or -
1407 1409 # if there were at least some changes.
1408 1410 return max(i * graphwidth // maxtotal, int(bool(i)))
1409 1411
1410 1412 for filename, adds, removes, isbinary in stats:
1411 1413 if git and isbinary:
1412 1414 count = 'Bin'
1413 1415 else:
1414 1416 count = adds + removes
1415 1417 pluses = '+' * scale(adds)
1416 1418 minuses = '-' * scale(removes)
1417 1419 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1418 1420 count, pluses, minuses))
1419 1421
1420 1422 if stats:
1421 1423 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1422 1424 % (len(stats), totaladds, totalremoves))
1423 1425
1424 1426 return ''.join(output)
General Comments 0
You need to be logged in to leave comments. Login now