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