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