##// END OF EJS Templates
patch.extract: do not prepend subject if the description already starts with it
Brendan Cully -
r4777:5ee5cbfc default
parent child Browse files
Show More
@@ -1,675 +1,674 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from i18n import _
9 9 from node import *
10 10 import base85, cmdutil, mdiff, util, context, revlog
11 11 import cStringIO, email.Parser, os, popen2, re, sha
12 12 import sys, tempfile, zlib
13 13
14 14 # helper functions
15 15
16 16 def copyfile(src, dst, basedir=None):
17 17 if not basedir:
18 18 basedir = os.getcwd()
19 19
20 20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 21 if os.path.exists(absdst):
22 22 raise util.Abort(_("cannot create %s: destination already exists") %
23 23 dst)
24 24
25 25 targetdir = os.path.dirname(absdst)
26 26 if not os.path.isdir(targetdir):
27 27 os.makedirs(targetdir)
28 28
29 29 util.copyfile(abssrc, absdst)
30 30
31 31 # public functions
32 32
33 33 def extract(ui, fileobj):
34 34 '''extract patch from data read from fileobj.
35 35
36 36 patch can be a normal patch or contained in an email message.
37 37
38 38 return tuple (filename, message, user, date, node, p1, p2).
39 39 Any item in the returned tuple can be None. If filename is None,
40 40 fileobj did not contain a patch. Caller must unlink filename when done.'''
41 41
42 42 # attempt to detect the start of a patch
43 43 # (this heuristic is borrowed from quilt)
44 44 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
45 45 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
46 46 '(---|\*\*\*)[ \t])', re.MULTILINE)
47 47
48 48 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
49 49 tmpfp = os.fdopen(fd, 'w')
50 50 try:
51 51 msg = email.Parser.Parser().parse(fileobj)
52 52
53 message = msg['Subject']
53 subject = msg['Subject']
54 54 user = msg['From']
55 55 # should try to parse msg['Date']
56 56 date = None
57 57 nodeid = None
58 58 branch = None
59 59 parents = []
60 60
61 if message:
62 if message.startswith('[PATCH'):
63 pend = message.find(']')
61 if subject:
62 if subject.startswith('[PATCH'):
63 pend = subject.find(']')
64 64 if pend >= 0:
65 message = message[pend+1:].lstrip()
66 message = message.replace('\n\t', ' ')
67 ui.debug('Subject: %s\n' % message)
65 subject = subject[pend+1:].lstrip()
66 subject = subject.replace('\n\t', ' ')
67 ui.debug('Subject: %s\n' % subject)
68 68 if user:
69 69 ui.debug('From: %s\n' % user)
70 70 diffs_seen = 0
71 71 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
72 72
73 73 for part in msg.walk():
74 74 content_type = part.get_content_type()
75 75 ui.debug('Content-Type: %s\n' % content_type)
76 76 if content_type not in ok_types:
77 77 continue
78 78 payload = part.get_payload(decode=True)
79 79 m = diffre.search(payload)
80 80 if m:
81 81 hgpatch = False
82 82 ignoretext = False
83 83
84 84 ui.debug(_('found patch at byte %d\n') % m.start(0))
85 85 diffs_seen += 1
86 86 cfp = cStringIO.StringIO()
87 if message:
88 cfp.write(message)
89 cfp.write('\n')
90 87 for line in payload[:m.start(0)].splitlines():
91 88 if line.startswith('# HG changeset patch'):
92 89 ui.debug(_('patch generated by hg export\n'))
93 90 hgpatch = True
94 91 # drop earlier commit message content
95 92 cfp.seek(0)
96 93 cfp.truncate()
97 94 elif hgpatch:
98 95 if line.startswith('# User '):
99 96 user = line[7:]
100 97 ui.debug('From: %s\n' % user)
101 98 elif line.startswith("# Date "):
102 99 date = line[7:]
103 100 elif line.startswith("# Branch "):
104 101 branch = line[9:]
105 102 elif line.startswith("# Node ID "):
106 103 nodeid = line[10:]
107 104 elif line.startswith("# Parent "):
108 105 parents.append(line[10:])
109 106 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
110 107 ignoretext = True
111 108 if not line.startswith('# ') and not ignoretext:
112 109 cfp.write(line)
113 110 cfp.write('\n')
114 111 message = cfp.getvalue()
115 112 if tmpfp:
116 113 tmpfp.write(payload)
117 114 if not payload.endswith('\n'):
118 115 tmpfp.write('\n')
119 116 elif not diffs_seen and message and content_type == 'text/plain':
120 117 message += '\n' + payload
121 118 except:
122 119 tmpfp.close()
123 120 os.unlink(tmpname)
124 121 raise
125 122
123 if subject and not message.startswith(subject):
124 message = '%s\n%s' % (subject, message)
126 125 tmpfp.close()
127 126 if not diffs_seen:
128 127 os.unlink(tmpname)
129 128 return None, message, user, date, branch, None, None, None
130 129 p1 = parents and parents.pop(0) or None
131 130 p2 = parents and parents.pop(0) or None
132 131 return tmpname, message, user, date, branch, nodeid, p1, p2
133 132
134 133 GP_PATCH = 1 << 0 # we have to run patch
135 134 GP_FILTER = 1 << 1 # there's some copy/rename operation
136 135 GP_BINARY = 1 << 2 # there's a binary patch
137 136
138 137 def readgitpatch(patchname):
139 138 """extract git-style metadata about patches from <patchname>"""
140 139 class gitpatch:
141 140 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
142 141 def __init__(self, path):
143 142 self.path = path
144 143 self.oldpath = None
145 144 self.mode = None
146 145 self.op = 'MODIFY'
147 146 self.copymod = False
148 147 self.lineno = 0
149 148 self.binary = False
150 149
151 150 # Filter patch for git information
152 151 gitre = re.compile('diff --git a/(.*) b/(.*)')
153 152 pf = file(patchname)
154 153 gp = None
155 154 gitpatches = []
156 155 # Can have a git patch with only metadata, causing patch to complain
157 156 dopatch = 0
158 157
159 158 lineno = 0
160 159 for line in pf:
161 160 lineno += 1
162 161 if line.startswith('diff --git'):
163 162 m = gitre.match(line)
164 163 if m:
165 164 if gp:
166 165 gitpatches.append(gp)
167 166 src, dst = m.group(1, 2)
168 167 gp = gitpatch(dst)
169 168 gp.lineno = lineno
170 169 elif gp:
171 170 if line.startswith('--- '):
172 171 if gp.op in ('COPY', 'RENAME'):
173 172 gp.copymod = True
174 173 dopatch |= GP_FILTER
175 174 gitpatches.append(gp)
176 175 gp = None
177 176 dopatch |= GP_PATCH
178 177 continue
179 178 if line.startswith('rename from '):
180 179 gp.op = 'RENAME'
181 180 gp.oldpath = line[12:].rstrip()
182 181 elif line.startswith('rename to '):
183 182 gp.path = line[10:].rstrip()
184 183 elif line.startswith('copy from '):
185 184 gp.op = 'COPY'
186 185 gp.oldpath = line[10:].rstrip()
187 186 elif line.startswith('copy to '):
188 187 gp.path = line[8:].rstrip()
189 188 elif line.startswith('deleted file'):
190 189 gp.op = 'DELETE'
191 190 elif line.startswith('new file mode '):
192 191 gp.op = 'ADD'
193 192 gp.mode = int(line.rstrip()[-3:], 8)
194 193 elif line.startswith('new mode '):
195 194 gp.mode = int(line.rstrip()[-3:], 8)
196 195 elif line.startswith('GIT binary patch'):
197 196 dopatch |= GP_BINARY
198 197 gp.binary = True
199 198 if gp:
200 199 gitpatches.append(gp)
201 200
202 201 if not gitpatches:
203 202 dopatch = GP_PATCH
204 203
205 204 return (dopatch, gitpatches)
206 205
207 206 def dogitpatch(patchname, gitpatches, cwd=None):
208 207 """Preprocess git patch so that vanilla patch can handle it"""
209 208 def extractbin(fp):
210 209 i = [0] # yuck
211 210 def readline():
212 211 i[0] += 1
213 212 return fp.readline().rstrip()
214 213 line = readline()
215 214 while line and not line.startswith('literal '):
216 215 line = readline()
217 216 if not line:
218 217 return None, i[0]
219 218 size = int(line[8:])
220 219 dec = []
221 220 line = readline()
222 221 while line:
223 222 l = line[0]
224 223 if l <= 'Z' and l >= 'A':
225 224 l = ord(l) - ord('A') + 1
226 225 else:
227 226 l = ord(l) - ord('a') + 27
228 227 dec.append(base85.b85decode(line[1:])[:l])
229 228 line = readline()
230 229 text = zlib.decompress(''.join(dec))
231 230 if len(text) != size:
232 231 raise util.Abort(_('binary patch is %d bytes, not %d') %
233 232 (len(text), size))
234 233 return text, i[0]
235 234
236 235 pf = file(patchname)
237 236 pfline = 1
238 237
239 238 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
240 239 tmpfp = os.fdopen(fd, 'w')
241 240
242 241 try:
243 242 for i in xrange(len(gitpatches)):
244 243 p = gitpatches[i]
245 244 if not p.copymod and not p.binary:
246 245 continue
247 246
248 247 # rewrite patch hunk
249 248 while pfline < p.lineno:
250 249 tmpfp.write(pf.readline())
251 250 pfline += 1
252 251
253 252 if p.binary:
254 253 text, delta = extractbin(pf)
255 254 if not text:
256 255 raise util.Abort(_('binary patch extraction failed'))
257 256 pfline += delta
258 257 if not cwd:
259 258 cwd = os.getcwd()
260 259 absdst = os.path.join(cwd, p.path)
261 260 basedir = os.path.dirname(absdst)
262 261 if not os.path.isdir(basedir):
263 262 os.makedirs(basedir)
264 263 out = file(absdst, 'wb')
265 264 out.write(text)
266 265 out.close()
267 266 elif p.copymod:
268 267 copyfile(p.oldpath, p.path, basedir=cwd)
269 268 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
270 269 line = pf.readline()
271 270 pfline += 1
272 271 while not line.startswith('--- a/'):
273 272 tmpfp.write(line)
274 273 line = pf.readline()
275 274 pfline += 1
276 275 tmpfp.write('--- a/%s\n' % p.path)
277 276
278 277 line = pf.readline()
279 278 while line:
280 279 tmpfp.write(line)
281 280 line = pf.readline()
282 281 except:
283 282 tmpfp.close()
284 283 os.unlink(patchname)
285 284 raise
286 285
287 286 tmpfp.close()
288 287 return patchname
289 288
290 289 def patch(patchname, ui, strip=1, cwd=None, files={}):
291 290 """apply the patch <patchname> to the working directory.
292 291 a list of patched files is returned"""
293 292
294 293 # helper function
295 294 def __patch(patchname):
296 295 """patch and updates the files and fuzz variables"""
297 296 fuzz = False
298 297
299 298 args = []
300 299 patcher = ui.config('ui', 'patch')
301 300 if not patcher:
302 301 patcher = util.find_exe('gpatch') or util.find_exe('patch')
303 302 # Try to be smart only if patch call was not supplied
304 303 if util.needbinarypatch():
305 304 args.append('--binary')
306 305
307 306 if not patcher:
308 307 raise util.Abort(_('no patch command found in hgrc or PATH'))
309 308
310 309 if cwd:
311 310 args.append('-d %s' % util.shellquote(cwd))
312 311 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
313 312 util.shellquote(patchname)))
314 313
315 314 for line in fp:
316 315 line = line.rstrip()
317 316 ui.note(line + '\n')
318 317 if line.startswith('patching file '):
319 318 pf = util.parse_patch_output(line)
320 319 printed_file = False
321 320 files.setdefault(pf, (None, None))
322 321 elif line.find('with fuzz') >= 0:
323 322 fuzz = True
324 323 if not printed_file:
325 324 ui.warn(pf + '\n')
326 325 printed_file = True
327 326 ui.warn(line + '\n')
328 327 elif line.find('saving rejects to file') >= 0:
329 328 ui.warn(line + '\n')
330 329 elif line.find('FAILED') >= 0:
331 330 if not printed_file:
332 331 ui.warn(pf + '\n')
333 332 printed_file = True
334 333 ui.warn(line + '\n')
335 334 code = fp.close()
336 335 if code:
337 336 raise util.Abort(_("patch command failed: %s") %
338 337 util.explain_exit(code)[0])
339 338 return fuzz
340 339
341 340 (dopatch, gitpatches) = readgitpatch(patchname)
342 341 for gp in gitpatches:
343 342 files[gp.path] = (gp.op, gp)
344 343
345 344 fuzz = False
346 345 if dopatch:
347 346 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
348 347 if filterpatch:
349 348 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
350 349 try:
351 350 if dopatch & GP_PATCH:
352 351 fuzz = __patch(patchname)
353 352 finally:
354 353 if filterpatch:
355 354 os.unlink(patchname)
356 355
357 356 return fuzz
358 357
359 358 def diffopts(ui, opts={}, untrusted=False):
360 359 def get(key, name=None):
361 360 return (opts.get(key) or
362 361 ui.configbool('diff', name or key, None, untrusted=untrusted))
363 362 return mdiff.diffopts(
364 363 text=opts.get('text'),
365 364 git=get('git'),
366 365 nodates=get('nodates'),
367 366 showfunc=get('show_function', 'showfunc'),
368 367 ignorews=get('ignore_all_space', 'ignorews'),
369 368 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
370 369 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
371 370
372 371 def updatedir(ui, repo, patches, wlock=None):
373 372 '''Update dirstate after patch application according to metadata'''
374 373 if not patches:
375 374 return
376 375 copies = []
377 376 removes = {}
378 377 cfiles = patches.keys()
379 378 cwd = repo.getcwd()
380 379 if cwd:
381 380 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
382 381 for f in patches:
383 382 ctype, gp = patches[f]
384 383 if ctype == 'RENAME':
385 384 copies.append((gp.oldpath, gp.path, gp.copymod))
386 385 removes[gp.oldpath] = 1
387 386 elif ctype == 'COPY':
388 387 copies.append((gp.oldpath, gp.path, gp.copymod))
389 388 elif ctype == 'DELETE':
390 389 removes[gp.path] = 1
391 390 for src, dst, after in copies:
392 391 if not after:
393 392 copyfile(src, dst, repo.root)
394 393 repo.copy(src, dst, wlock=wlock)
395 394 removes = removes.keys()
396 395 if removes:
397 396 removes.sort()
398 397 repo.remove(removes, True, wlock=wlock)
399 398 for f in patches:
400 399 ctype, gp = patches[f]
401 400 if gp and gp.mode:
402 401 x = gp.mode & 0100 != 0
403 402 dst = os.path.join(repo.root, gp.path)
404 403 # patch won't create empty files
405 404 if ctype == 'ADD' and not os.path.exists(dst):
406 405 repo.wwrite(gp.path, '', x and 'x' or '')
407 406 else:
408 407 util.set_exec(dst, x)
409 408 cmdutil.addremove(repo, cfiles, wlock=wlock)
410 409 files = patches.keys()
411 410 files.extend([r for r in removes if r not in files])
412 411 files.sort()
413 412
414 413 return files
415 414
416 415 def b85diff(fp, to, tn):
417 416 '''print base85-encoded binary diff'''
418 417 def gitindex(text):
419 418 if not text:
420 419 return '0' * 40
421 420 l = len(text)
422 421 s = sha.new('blob %d\0' % l)
423 422 s.update(text)
424 423 return s.hexdigest()
425 424
426 425 def fmtline(line):
427 426 l = len(line)
428 427 if l <= 26:
429 428 l = chr(ord('A') + l - 1)
430 429 else:
431 430 l = chr(l - 26 + ord('a') - 1)
432 431 return '%c%s\n' % (l, base85.b85encode(line, True))
433 432
434 433 def chunk(text, csize=52):
435 434 l = len(text)
436 435 i = 0
437 436 while i < l:
438 437 yield text[i:i+csize]
439 438 i += csize
440 439
441 440 tohash = gitindex(to)
442 441 tnhash = gitindex(tn)
443 442 if tohash == tnhash:
444 443 return ""
445 444
446 445 # TODO: deltas
447 446 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
448 447 (tohash, tnhash, len(tn))]
449 448 for l in chunk(zlib.compress(tn)):
450 449 ret.append(fmtline(l))
451 450 ret.append('\n')
452 451 return ''.join(ret)
453 452
454 453 def diff(repo, node1=None, node2=None, files=None, match=util.always,
455 454 fp=None, changes=None, opts=None):
456 455 '''print diff of changes to files between two nodes, or node and
457 456 working directory.
458 457
459 458 if node1 is None, use first dirstate parent instead.
460 459 if node2 is None, compare node1 with working directory.'''
461 460
462 461 if opts is None:
463 462 opts = mdiff.defaultopts
464 463 if fp is None:
465 464 fp = repo.ui
466 465
467 466 if not node1:
468 467 node1 = repo.dirstate.parents()[0]
469 468
470 469 ccache = {}
471 470 def getctx(r):
472 471 if r not in ccache:
473 472 ccache[r] = context.changectx(repo, r)
474 473 return ccache[r]
475 474
476 475 flcache = {}
477 476 def getfilectx(f, ctx):
478 477 flctx = ctx.filectx(f, filelog=flcache.get(f))
479 478 if f not in flcache:
480 479 flcache[f] = flctx._filelog
481 480 return flctx
482 481
483 482 # reading the data for node1 early allows it to play nicely
484 483 # with repo.status and the revlog cache.
485 484 ctx1 = context.changectx(repo, node1)
486 485 # force manifest reading
487 486 man1 = ctx1.manifest()
488 487 date1 = util.datestr(ctx1.date())
489 488
490 489 if not changes:
491 490 changes = repo.status(node1, node2, files, match=match)[:5]
492 491 modified, added, removed, deleted, unknown = changes
493 492
494 493 if not modified and not added and not removed:
495 494 return
496 495
497 496 if node2:
498 497 ctx2 = context.changectx(repo, node2)
499 498 execf2 = ctx2.manifest().execf
500 499 else:
501 500 ctx2 = context.workingctx(repo)
502 501 execf2 = util.execfunc(repo.root, None)
503 502 if execf2 is None:
504 503 execf2 = ctx2.parents()[0].manifest().copy().execf
505 504
506 505 # returns False if there was no rename between ctx1 and ctx2
507 506 # returns None if the file was created between ctx1 and ctx2
508 507 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
509 508 def renamed(f):
510 509 startrev = ctx1.rev()
511 510 c = ctx2
512 511 crev = c.rev()
513 512 if crev is None:
514 513 crev = repo.changelog.count()
515 514 orig = f
516 515 while crev > startrev:
517 516 if f in c.files():
518 517 try:
519 518 src = getfilectx(f, c).renamed()
520 519 except revlog.LookupError:
521 520 return None
522 521 if src:
523 522 f = src[0]
524 523 crev = c.parents()[0].rev()
525 524 # try to reuse
526 525 c = getctx(crev)
527 526 if f not in man1:
528 527 return None
529 528 if f == orig:
530 529 return False
531 530 return f
532 531
533 532 if repo.ui.quiet:
534 533 r = None
535 534 else:
536 535 hexfunc = repo.ui.debugflag and hex or short
537 536 r = [hexfunc(node) for node in [node1, node2] if node]
538 537
539 538 if opts.git:
540 539 copied = {}
541 540 for f in added:
542 541 src = renamed(f)
543 542 if src:
544 543 copied[f] = src
545 544 srcs = [x[1] for x in copied.items()]
546 545
547 546 all = modified + added + removed
548 547 all.sort()
549 548 gone = {}
550 549
551 550 for f in all:
552 551 to = None
553 552 tn = None
554 553 dodiff = True
555 554 header = []
556 555 if f in man1:
557 556 to = getfilectx(f, ctx1).data()
558 557 if f not in removed:
559 558 tn = getfilectx(f, ctx2).data()
560 559 if opts.git:
561 560 def gitmode(x):
562 561 return x and '100755' or '100644'
563 562 def addmodehdr(header, omode, nmode):
564 563 if omode != nmode:
565 564 header.append('old mode %s\n' % omode)
566 565 header.append('new mode %s\n' % nmode)
567 566
568 567 a, b = f, f
569 568 if f in added:
570 569 mode = gitmode(execf2(f))
571 570 if f in copied:
572 571 a = copied[f]
573 572 omode = gitmode(man1.execf(a))
574 573 addmodehdr(header, omode, mode)
575 574 if a in removed and a not in gone:
576 575 op = 'rename'
577 576 gone[a] = 1
578 577 else:
579 578 op = 'copy'
580 579 header.append('%s from %s\n' % (op, a))
581 580 header.append('%s to %s\n' % (op, f))
582 581 to = getfilectx(a, ctx1).data()
583 582 else:
584 583 header.append('new file mode %s\n' % mode)
585 584 if util.binary(tn):
586 585 dodiff = 'binary'
587 586 elif f in removed:
588 587 if f in srcs:
589 588 dodiff = False
590 589 else:
591 590 mode = gitmode(man1.execf(f))
592 591 header.append('deleted file mode %s\n' % mode)
593 592 else:
594 593 omode = gitmode(man1.execf(f))
595 594 nmode = gitmode(execf2(f))
596 595 addmodehdr(header, omode, nmode)
597 596 if util.binary(to) or util.binary(tn):
598 597 dodiff = 'binary'
599 598 r = None
600 599 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
601 600 if dodiff:
602 601 if dodiff == 'binary':
603 602 text = b85diff(fp, to, tn)
604 603 else:
605 604 text = mdiff.unidiff(to, date1,
606 605 # ctx2 date may be dynamic
607 606 tn, util.datestr(ctx2.date()),
608 607 f, r, opts=opts)
609 608 if text or len(header) > 1:
610 609 fp.write(''.join(header))
611 610 fp.write(text)
612 611
613 612 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
614 613 opts=None):
615 614 '''export changesets as hg patches.'''
616 615
617 616 total = len(revs)
618 617 revwidth = max([len(str(rev)) for rev in revs])
619 618
620 619 def single(rev, seqno, fp):
621 620 ctx = repo.changectx(rev)
622 621 node = ctx.node()
623 622 parents = [p.node() for p in ctx.parents() if p]
624 623 branch = ctx.branch()
625 624 if switch_parent:
626 625 parents.reverse()
627 626 prev = (parents and parents[0]) or nullid
628 627
629 628 if not fp:
630 629 fp = cmdutil.make_file(repo, template, node, total=total,
631 630 seqno=seqno, revwidth=revwidth)
632 631 if fp != sys.stdout and hasattr(fp, 'name'):
633 632 repo.ui.note("%s\n" % fp.name)
634 633
635 634 fp.write("# HG changeset patch\n")
636 635 fp.write("# User %s\n" % ctx.user())
637 636 fp.write("# Date %d %d\n" % ctx.date())
638 637 if branch and (branch != 'default'):
639 638 fp.write("# Branch %s\n" % branch)
640 639 fp.write("# Node ID %s\n" % hex(node))
641 640 fp.write("# Parent %s\n" % hex(prev))
642 641 if len(parents) > 1:
643 642 fp.write("# Parent %s\n" % hex(parents[1]))
644 643 fp.write(ctx.description().rstrip())
645 644 fp.write("\n\n")
646 645
647 646 diff(repo, prev, node, fp=fp, opts=opts)
648 647 if fp not in (sys.stdout, repo.ui):
649 648 fp.close()
650 649
651 650 for seqno, rev in enumerate(revs):
652 651 single(rev, seqno+1, fp)
653 652
654 653 def diffstat(patchlines):
655 654 if not util.find_exe('diffstat'):
656 655 return
657 656 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
658 657 try:
659 658 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
660 659 try:
661 660 for line in patchlines: print >> p.tochild, line
662 661 p.tochild.close()
663 662 if p.wait(): return
664 663 fp = os.fdopen(fd, 'r')
665 664 stat = []
666 665 for line in fp: stat.append(line.lstrip())
667 666 last = stat.pop()
668 667 stat.insert(0, last)
669 668 stat = ''.join(stat)
670 669 if stat.startswith('0 files'): raise ValueError
671 670 return stat
672 671 except: raise
673 672 finally:
674 673 try: os.unlink(name)
675 674 except: pass
General Comments 0
You need to be logged in to leave comments. Login now