##// END OF EJS Templates
patch: catch only IOError from makedirs()
Patrick Mezard -
r7507:8e76e9f6 default
parent child Browse files
Show More
@@ -1,1334 +1,1334 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 except:
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 777 l = self.buf[0]
778 778 del self.buf[0]
779 779 return l
780 780 return self.fp.readline()
781 781
782 782 def __iter__(self):
783 783 while 1:
784 784 l = self.readline()
785 785 if not l:
786 786 break
787 787 yield l
788 788
789 789 def scangitpatch(lr, firstline):
790 790 """
791 791 Git patches can emit:
792 792 - rename a to b
793 793 - change b
794 794 - copy a to c
795 795 - change c
796 796
797 797 We cannot apply this sequence as-is, the renamed 'a' could not be
798 798 found for it would have been renamed already. And we cannot copy
799 799 from 'b' instead because 'b' would have been changed already. So
800 800 we scan the git patch for copy and rename commands so we can
801 801 perform the copies ahead of time.
802 802 """
803 803 pos = 0
804 804 try:
805 805 pos = lr.fp.tell()
806 806 fp = lr.fp
807 807 except IOError:
808 808 fp = cStringIO.StringIO(lr.fp.read())
809 809 gitlr = linereader(fp)
810 810 gitlr.push(firstline)
811 811 (dopatch, gitpatches) = readgitpatch(gitlr)
812 812 fp.seek(pos)
813 813 return dopatch, gitpatches
814 814
815 815 def iterhunks(ui, fp, sourcefile=None):
816 816 """Read a patch and yield the following events:
817 817 - ("file", afile, bfile, firsthunk): select a new target file.
818 818 - ("hunk", hunk): a new hunk is ready to be applied, follows a
819 819 "file" event.
820 820 - ("git", gitchanges): current diff is in git format, gitchanges
821 821 maps filenames to gitpatch records. Unique event.
822 822 """
823 823 changed = {}
824 824 current_hunk = None
825 825 afile = ""
826 826 bfile = ""
827 827 state = None
828 828 hunknum = 0
829 829 emitfile = False
830 830 git = False
831 831
832 832 # our states
833 833 BFILE = 1
834 834 context = None
835 835 lr = linereader(fp)
836 836 dopatch = True
837 837 # gitworkdone is True if a git operation (copy, rename, ...) was
838 838 # performed already for the current file. Useful when the file
839 839 # section may have no hunk.
840 840 gitworkdone = False
841 841
842 842 while True:
843 843 newfile = False
844 844 x = lr.readline()
845 845 if not x:
846 846 break
847 847 if current_hunk:
848 848 if x.startswith('\ '):
849 849 current_hunk.fix_newline()
850 850 yield 'hunk', current_hunk
851 851 current_hunk = None
852 852 gitworkdone = False
853 853 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
854 854 ((context or context == None) and x.startswith('***************')))):
855 855 try:
856 856 if context == None and x.startswith('***************'):
857 857 context = True
858 858 gpatch = changed.get(bfile)
859 859 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
860 860 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
861 861 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
862 862 except PatchError, err:
863 863 ui.debug(err)
864 864 current_hunk = None
865 865 continue
866 866 hunknum += 1
867 867 if emitfile:
868 868 emitfile = False
869 869 yield 'file', (afile, bfile, current_hunk)
870 870 elif state == BFILE and x.startswith('GIT binary patch'):
871 871 current_hunk = binhunk(changed[bfile])
872 872 hunknum += 1
873 873 if emitfile:
874 874 emitfile = False
875 875 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
876 876 current_hunk.extract(lr)
877 877 elif x.startswith('diff --git'):
878 878 # check for git diff, scanning the whole patch file if needed
879 879 m = gitre.match(x)
880 880 if m:
881 881 afile, bfile = m.group(1, 2)
882 882 if not git:
883 883 git = True
884 884 dopatch, gitpatches = scangitpatch(lr, x)
885 885 yield 'git', gitpatches
886 886 for gp in gitpatches:
887 887 changed[gp.path] = gp
888 888 # else error?
889 889 # copy/rename + modify should modify target, not source
890 890 gp = changed.get(bfile)
891 891 if gp and gp.op in ('COPY', 'DELETE', 'RENAME'):
892 892 afile = bfile
893 893 gitworkdone = True
894 894 newfile = True
895 895 elif x.startswith('---'):
896 896 # check for a unified diff
897 897 l2 = lr.readline()
898 898 if not l2.startswith('+++'):
899 899 lr.push(l2)
900 900 continue
901 901 newfile = True
902 902 context = False
903 903 afile = parsefilename(x)
904 904 bfile = parsefilename(l2)
905 905 elif x.startswith('***'):
906 906 # check for a context diff
907 907 l2 = lr.readline()
908 908 if not l2.startswith('---'):
909 909 lr.push(l2)
910 910 continue
911 911 l3 = lr.readline()
912 912 lr.push(l3)
913 913 if not l3.startswith("***************"):
914 914 lr.push(l2)
915 915 continue
916 916 newfile = True
917 917 context = True
918 918 afile = parsefilename(x)
919 919 bfile = parsefilename(l2)
920 920
921 921 if newfile:
922 922 emitfile = True
923 923 state = BFILE
924 924 hunknum = 0
925 925 if current_hunk:
926 926 if current_hunk.complete():
927 927 yield 'hunk', current_hunk
928 928 else:
929 929 raise PatchError(_("malformed patch %s %s") % (afile,
930 930 current_hunk.desc))
931 931
932 932 if hunknum == 0 and dopatch and not gitworkdone:
933 933 raise NoHunks
934 934
935 935 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False):
936 936 """reads a patch from fp and tries to apply it. The dict 'changed' is
937 937 filled in with all of the filenames changed by the patch. Returns 0
938 938 for a clean patch, -1 if any rejects were found and 1 if there was
939 939 any fuzz."""
940 940
941 941 rejects = 0
942 942 err = 0
943 943 current_file = None
944 944 gitpatches = None
945 945 opener = util.opener(os.getcwd())
946 946
947 947 def closefile():
948 948 if not current_file:
949 949 return 0
950 950 current_file.close()
951 951 return len(current_file.rej)
952 952
953 953 for state, values in iterhunks(ui, fp, sourcefile):
954 954 if state == 'hunk':
955 955 if not current_file:
956 956 continue
957 957 current_hunk = values
958 958 ret = current_file.apply(current_hunk, reverse)
959 959 if ret >= 0:
960 960 changed.setdefault(current_file.fname, None)
961 961 if ret > 0:
962 962 err = 1
963 963 elif state == 'file':
964 964 rejects += closefile()
965 965 afile, bfile, first_hunk = values
966 966 try:
967 967 if sourcefile:
968 968 current_file = patchfile(ui, sourcefile, opener)
969 969 else:
970 970 current_file, missing = selectfile(afile, bfile, first_hunk,
971 971 strip, reverse)
972 972 current_file = patchfile(ui, current_file, opener, missing)
973 973 except PatchError, err:
974 974 ui.warn(str(err) + '\n')
975 975 current_file, current_hunk = None, None
976 976 rejects += 1
977 977 continue
978 978 elif state == 'git':
979 979 gitpatches = values
980 980 cwd = os.getcwd()
981 981 for gp in gitpatches:
982 982 if gp.op in ('COPY', 'RENAME'):
983 983 copyfile(gp.oldpath, gp.path, cwd)
984 984 changed[gp.path] = gp
985 985 else:
986 986 raise util.Abort(_('unsupported parser state: %s') % state)
987 987
988 988 rejects += closefile()
989 989
990 990 if rejects:
991 991 return -1
992 992 return err
993 993
994 994 def diffopts(ui, opts={}, untrusted=False):
995 995 def get(key, name=None, getter=ui.configbool):
996 996 return (opts.get(key) or
997 997 getter('diff', name or key, None, untrusted=untrusted))
998 998 return mdiff.diffopts(
999 999 text=opts.get('text'),
1000 1000 git=get('git'),
1001 1001 nodates=get('nodates'),
1002 1002 showfunc=get('show_function', 'showfunc'),
1003 1003 ignorews=get('ignore_all_space', 'ignorews'),
1004 1004 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1005 1005 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1006 1006 context=get('unified', getter=ui.config))
1007 1007
1008 1008 def updatedir(ui, repo, patches, similarity=0):
1009 1009 '''Update dirstate after patch application according to metadata'''
1010 1010 if not patches:
1011 1011 return
1012 1012 copies = []
1013 1013 removes = {}
1014 1014 cfiles = patches.keys()
1015 1015 cwd = repo.getcwd()
1016 1016 if cwd:
1017 1017 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1018 1018 for f in patches:
1019 1019 gp = patches[f]
1020 1020 if not gp:
1021 1021 continue
1022 1022 if gp.op == 'RENAME':
1023 1023 copies.append((gp.oldpath, gp.path))
1024 1024 removes[gp.oldpath] = 1
1025 1025 elif gp.op == 'COPY':
1026 1026 copies.append((gp.oldpath, gp.path))
1027 1027 elif gp.op == 'DELETE':
1028 1028 removes[gp.path] = 1
1029 1029 for src, dst in copies:
1030 1030 repo.copy(src, dst)
1031 1031 removes = removes.keys()
1032 1032 if (not similarity) and removes:
1033 1033 repo.remove(util.sort(removes), True)
1034 1034 for f in patches:
1035 1035 gp = patches[f]
1036 1036 if gp and gp.mode:
1037 1037 islink, isexec = gp.mode
1038 1038 dst = os.path.join(repo.root, gp.path)
1039 1039 # patch won't create empty files
1040 1040 if gp.op == 'ADD' and not os.path.exists(dst):
1041 1041 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1042 1042 repo.wwrite(gp.path, '', flags)
1043 1043 else:
1044 1044 util.set_flags(dst, islink, isexec)
1045 1045 cmdutil.addremove(repo, cfiles, similarity=similarity)
1046 1046 files = patches.keys()
1047 1047 files.extend([r for r in removes if r not in files])
1048 1048 return util.sort(files)
1049 1049
1050 1050 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1051 1051 """use <patcher> to apply <patchname> to the working directory.
1052 1052 returns whether patch was applied with fuzz factor."""
1053 1053
1054 1054 fuzz = False
1055 1055 if cwd:
1056 1056 args.append('-d %s' % util.shellquote(cwd))
1057 1057 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1058 1058 util.shellquote(patchname)))
1059 1059
1060 1060 for line in fp:
1061 1061 line = line.rstrip()
1062 1062 ui.note(line + '\n')
1063 1063 if line.startswith('patching file '):
1064 1064 pf = util.parse_patch_output(line)
1065 1065 printed_file = False
1066 1066 files.setdefault(pf, None)
1067 1067 elif line.find('with fuzz') >= 0:
1068 1068 fuzz = True
1069 1069 if not printed_file:
1070 1070 ui.warn(pf + '\n')
1071 1071 printed_file = True
1072 1072 ui.warn(line + '\n')
1073 1073 elif line.find('saving rejects to file') >= 0:
1074 1074 ui.warn(line + '\n')
1075 1075 elif line.find('FAILED') >= 0:
1076 1076 if not printed_file:
1077 1077 ui.warn(pf + '\n')
1078 1078 printed_file = True
1079 1079 ui.warn(line + '\n')
1080 1080 code = fp.close()
1081 1081 if code:
1082 1082 raise PatchError(_("patch command failed: %s") %
1083 1083 util.explain_exit(code)[0])
1084 1084 return fuzz
1085 1085
1086 1086 def internalpatch(patchobj, ui, strip, cwd, files={}):
1087 1087 """use builtin patch to apply <patchobj> to the working directory.
1088 1088 returns whether patch was applied with fuzz factor."""
1089 1089 try:
1090 1090 fp = file(patchobj, 'rb')
1091 1091 except TypeError:
1092 1092 fp = patchobj
1093 1093 if cwd:
1094 1094 curdir = os.getcwd()
1095 1095 os.chdir(cwd)
1096 1096 try:
1097 1097 ret = applydiff(ui, fp, files, strip=strip)
1098 1098 finally:
1099 1099 if cwd:
1100 1100 os.chdir(curdir)
1101 1101 if ret < 0:
1102 1102 raise PatchError
1103 1103 return ret > 0
1104 1104
1105 1105 def patch(patchname, ui, strip=1, cwd=None, files={}):
1106 1106 """apply <patchname> to the working directory.
1107 1107 returns whether patch was applied with fuzz factor."""
1108 1108 patcher = ui.config('ui', 'patch')
1109 1109 args = []
1110 1110 try:
1111 1111 if patcher:
1112 1112 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1113 1113 files)
1114 1114 else:
1115 1115 try:
1116 1116 return internalpatch(patchname, ui, strip, cwd, files)
1117 1117 except NoHunks:
1118 1118 patcher = util.find_exe('gpatch') or util.find_exe('patch')
1119 1119 ui.debug(_('no valid hunks found; trying with %r instead\n') %
1120 1120 patcher)
1121 1121 if util.needbinarypatch():
1122 1122 args.append('--binary')
1123 1123 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1124 1124 files)
1125 1125 except PatchError, err:
1126 1126 s = str(err)
1127 1127 if s:
1128 1128 raise util.Abort(s)
1129 1129 else:
1130 1130 raise util.Abort(_('patch failed to apply'))
1131 1131
1132 1132 def b85diff(to, tn):
1133 1133 '''print base85-encoded binary diff'''
1134 1134 def gitindex(text):
1135 1135 if not text:
1136 1136 return '0' * 40
1137 1137 l = len(text)
1138 1138 s = util.sha1('blob %d\0' % l)
1139 1139 s.update(text)
1140 1140 return s.hexdigest()
1141 1141
1142 1142 def fmtline(line):
1143 1143 l = len(line)
1144 1144 if l <= 26:
1145 1145 l = chr(ord('A') + l - 1)
1146 1146 else:
1147 1147 l = chr(l - 26 + ord('a') - 1)
1148 1148 return '%c%s\n' % (l, base85.b85encode(line, True))
1149 1149
1150 1150 def chunk(text, csize=52):
1151 1151 l = len(text)
1152 1152 i = 0
1153 1153 while i < l:
1154 1154 yield text[i:i+csize]
1155 1155 i += csize
1156 1156
1157 1157 tohash = gitindex(to)
1158 1158 tnhash = gitindex(tn)
1159 1159 if tohash == tnhash:
1160 1160 return ""
1161 1161
1162 1162 # TODO: deltas
1163 1163 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1164 1164 (tohash, tnhash, len(tn))]
1165 1165 for l in chunk(zlib.compress(tn)):
1166 1166 ret.append(fmtline(l))
1167 1167 ret.append('\n')
1168 1168 return ''.join(ret)
1169 1169
1170 1170 def _addmodehdr(header, omode, nmode):
1171 1171 if omode != nmode:
1172 1172 header.append('old mode %s\n' % omode)
1173 1173 header.append('new mode %s\n' % nmode)
1174 1174
1175 1175 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None):
1176 1176 '''yields diff of changes to files between two nodes, or node and
1177 1177 working directory.
1178 1178
1179 1179 if node1 is None, use first dirstate parent instead.
1180 1180 if node2 is None, compare node1 with working directory.'''
1181 1181
1182 1182 if not match:
1183 1183 match = cmdutil.matchall(repo)
1184 1184
1185 1185 if opts is None:
1186 1186 opts = mdiff.defaultopts
1187 1187
1188 1188 if not node1:
1189 1189 node1 = repo.dirstate.parents()[0]
1190 1190
1191 1191 flcache = {}
1192 1192 def getfilectx(f, ctx):
1193 1193 flctx = ctx.filectx(f, filelog=flcache.get(f))
1194 1194 if f not in flcache:
1195 1195 flcache[f] = flctx._filelog
1196 1196 return flctx
1197 1197
1198 1198 ctx1 = repo[node1]
1199 1199 ctx2 = repo[node2]
1200 1200
1201 1201 if not changes:
1202 1202 changes = repo.status(ctx1, ctx2, match=match)
1203 1203 modified, added, removed = changes[:3]
1204 1204
1205 1205 if not modified and not added and not removed:
1206 1206 return
1207 1207
1208 1208 date1 = util.datestr(ctx1.date())
1209 1209 man1 = ctx1.manifest()
1210 1210
1211 1211 if repo.ui.quiet:
1212 1212 r = None
1213 1213 else:
1214 1214 hexfunc = repo.ui.debugflag and hex or short
1215 1215 r = [hexfunc(node) for node in [node1, node2] if node]
1216 1216
1217 1217 if opts.git:
1218 1218 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
1219 1219 for k, v in copy.items():
1220 1220 copy[v] = k
1221 1221
1222 1222 gone = {}
1223 1223 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1224 1224
1225 1225 for f in util.sort(modified + added + removed):
1226 1226 to = None
1227 1227 tn = None
1228 1228 dodiff = True
1229 1229 header = []
1230 1230 if f in man1:
1231 1231 to = getfilectx(f, ctx1).data()
1232 1232 if f not in removed:
1233 1233 tn = getfilectx(f, ctx2).data()
1234 1234 a, b = f, f
1235 1235 if opts.git:
1236 1236 if f in added:
1237 1237 mode = gitmode[ctx2.flags(f)]
1238 1238 if f in copy:
1239 1239 a = copy[f]
1240 1240 omode = gitmode[man1.flags(a)]
1241 1241 _addmodehdr(header, omode, mode)
1242 1242 if a in removed and a not in gone:
1243 1243 op = 'rename'
1244 1244 gone[a] = 1
1245 1245 else:
1246 1246 op = 'copy'
1247 1247 header.append('%s from %s\n' % (op, a))
1248 1248 header.append('%s to %s\n' % (op, f))
1249 1249 to = getfilectx(a, ctx1).data()
1250 1250 else:
1251 1251 header.append('new file mode %s\n' % mode)
1252 1252 if util.binary(tn):
1253 1253 dodiff = 'binary'
1254 1254 elif f in removed:
1255 1255 # have we already reported a copy above?
1256 1256 if f in copy and copy[f] in added and copy[copy[f]] == f:
1257 1257 dodiff = False
1258 1258 else:
1259 1259 header.append('deleted file mode %s\n' %
1260 1260 gitmode[man1.flags(f)])
1261 1261 else:
1262 1262 omode = gitmode[man1.flags(f)]
1263 1263 nmode = gitmode[ctx2.flags(f)]
1264 1264 _addmodehdr(header, omode, nmode)
1265 1265 if util.binary(to) or util.binary(tn):
1266 1266 dodiff = 'binary'
1267 1267 r = None
1268 1268 header.insert(0, mdiff.diffline(r, a, b, opts))
1269 1269 if dodiff:
1270 1270 if dodiff == 'binary':
1271 1271 text = b85diff(to, tn)
1272 1272 else:
1273 1273 text = mdiff.unidiff(to, date1,
1274 1274 # ctx2 date may be dynamic
1275 1275 tn, util.datestr(ctx2.date()),
1276 1276 a, b, r, opts=opts)
1277 1277 if header and (text or len(header) > 1):
1278 1278 yield ''.join(header)
1279 1279 if text:
1280 1280 yield text
1281 1281
1282 1282 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1283 1283 opts=None):
1284 1284 '''export changesets as hg patches.'''
1285 1285
1286 1286 total = len(revs)
1287 1287 revwidth = max([len(str(rev)) for rev in revs])
1288 1288
1289 1289 def single(rev, seqno, fp):
1290 1290 ctx = repo[rev]
1291 1291 node = ctx.node()
1292 1292 parents = [p.node() for p in ctx.parents() if p]
1293 1293 branch = ctx.branch()
1294 1294 if switch_parent:
1295 1295 parents.reverse()
1296 1296 prev = (parents and parents[0]) or nullid
1297 1297
1298 1298 if not fp:
1299 1299 fp = cmdutil.make_file(repo, template, node, total=total,
1300 1300 seqno=seqno, revwidth=revwidth,
1301 1301 mode='ab')
1302 1302 if fp != sys.stdout and hasattr(fp, 'name'):
1303 1303 repo.ui.note("%s\n" % fp.name)
1304 1304
1305 1305 fp.write("# HG changeset patch\n")
1306 1306 fp.write("# User %s\n" % ctx.user())
1307 1307 fp.write("# Date %d %d\n" % ctx.date())
1308 1308 if branch and (branch != 'default'):
1309 1309 fp.write("# Branch %s\n" % branch)
1310 1310 fp.write("# Node ID %s\n" % hex(node))
1311 1311 fp.write("# Parent %s\n" % hex(prev))
1312 1312 if len(parents) > 1:
1313 1313 fp.write("# Parent %s\n" % hex(parents[1]))
1314 1314 fp.write(ctx.description().rstrip())
1315 1315 fp.write("\n\n")
1316 1316
1317 1317 for chunk in diff(repo, prev, node, opts=opts):
1318 1318 fp.write(chunk)
1319 1319 if fp not in (sys.stdout, repo.ui):
1320 1320 fp.close()
1321 1321
1322 1322 for seqno, rev in enumerate(revs):
1323 1323 single(rev, seqno+1, fp)
1324 1324
1325 1325 def diffstat(patchlines):
1326 1326 if not util.find_exe('diffstat'):
1327 1327 return
1328 1328 output = util.filter('\n'.join(patchlines),
1329 1329 'diffstat -p1 -w79 2>%s' % util.nulldev)
1330 1330 stat = [l.lstrip() for l in output.splitlines(True)]
1331 1331 last = stat.pop()
1332 1332 stat.insert(0, last)
1333 1333 stat = ''.join(stat)
1334 1334 return stat
General Comments 0
You need to be logged in to leave comments. Login now