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