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