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