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