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