Merge branch 'ss/svn-prompt' into maint
[git] / contrib / remote-helpers / git-remote-hg
CommitLineData
5085a4e2
FC
1#!/usr/bin/env python
2#
3# Copyright (c) 2012 Felipe Contreras
4#
5
6# Inspired by Rocco Rutte's hg-fast-export
7
8# Just copy to your ~/bin, or anywhere in your $PATH.
9# Then you can clone with:
10# git clone hg::/path/to/mercurial/repo/
11
ff247d9e 12from mercurial import hg, ui, bookmarks, context, util, encoding
5085a4e2
FC
13
14import re
15import sys
16import os
17import json
ffaf84c6 18import shutil
422ab5be 19import subprocess
9490bd04 20import urllib
422ab5be
FC
21
22#
23# If you want to switch to hg-git compatibility mode:
24# git config --global remote-hg.hg-git-compat true
25#
26# git:
27# Sensible defaults for git.
28# hg bookmarks are exported as git branches, hg branches are prefixed
46cc3adb 29# with 'branches/', HEAD is a special case.
422ab5be
FC
30#
31# hg:
32# Emulate hg-git.
33# Only hg bookmarks are exported as git branches.
831d57a0 34# Commits are modified to preserve hg information and allow bidirectionality.
422ab5be 35#
5085a4e2
FC
36
37NAME_RE = re.compile('^([^<>]+)')
12ba4bd4
FC
38AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
39AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$')
40RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')
5085a4e2
FC
41
42def die(msg, *args):
43 sys.stderr.write('ERROR: %s\n' % (msg % args))
44 sys.exit(1)
45
46def warn(msg, *args):
47 sys.stderr.write('WARNING: %s\n' % (msg % args))
48
49def gitmode(flags):
50 return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
51
52def gittz(tz):
53 return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
54
23b4a11f
FC
55def hgmode(mode):
56 m = { '0100755': 'x', '0120000': 'l' }
57 return m.get(mode, '')
58
1e310551
FC
59def get_config(config):
60 cmd = ['git', 'config', '--get', config]
61 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
62 output, _ = process.communicate()
63 return output
64
5085a4e2
FC
65class Marks:
66
67 def __init__(self, path):
68 self.path = path
69 self.tips = {}
70 self.marks = {}
23b4a11f 71 self.rev_marks = {}
5085a4e2
FC
72 self.last_mark = 0
73
74 self.load()
75
76 def load(self):
77 if not os.path.exists(self.path):
78 return
79
80 tmp = json.load(open(self.path))
81
82 self.tips = tmp['tips']
83 self.marks = tmp['marks']
84 self.last_mark = tmp['last-mark']
85
23b4a11f
FC
86 for rev, mark in self.marks.iteritems():
87 self.rev_marks[mark] = int(rev)
88
5085a4e2
FC
89 def dict(self):
90 return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
91
92 def store(self):
93 json.dump(self.dict(), open(self.path, 'w'))
94
95 def __str__(self):
96 return str(self.dict())
97
98 def from_rev(self, rev):
99 return self.marks[str(rev)]
100
23b4a11f
FC
101 def to_rev(self, mark):
102 return self.rev_marks[mark]
103
5085a4e2
FC
104 def get_mark(self, rev):
105 self.last_mark += 1
106 self.marks[str(rev)] = self.last_mark
107 return self.last_mark
108
23b4a11f
FC
109 def new_mark(self, rev, mark):
110 self.marks[str(rev)] = mark
111 self.rev_marks[mark] = rev
112 self.last_mark = mark
113
5085a4e2
FC
114 def is_marked(self, rev):
115 return self.marks.has_key(str(rev))
116
117 def get_tip(self, branch):
118 return self.tips.get(branch, 0)
119
120 def set_tip(self, branch, tip):
121 self.tips[branch] = tip
122
123class Parser:
124
125 def __init__(self, repo):
126 self.repo = repo
127 self.line = self.get_line()
128
129 def get_line(self):
130 return sys.stdin.readline().strip()
131
132 def __getitem__(self, i):
133 return self.line.split()[i]
134
135 def check(self, word):
136 return self.line.startswith(word)
137
138 def each_block(self, separator):
139 while self.line != separator:
140 yield self.line
141 self.line = self.get_line()
142
143 def __iter__(self):
144 return self.each_block('')
145
146 def next(self):
147 self.line = self.get_line()
148 if self.line == 'done':
149 self.line = None
150
23b4a11f
FC
151 def get_mark(self):
152 i = self.line.index(':') + 1
153 return int(self.line[i:])
154
155 def get_data(self):
156 if not self.check('data'):
157 return None
158 i = self.line.index(' ') + 1
159 size = int(self.line[i:])
160 return sys.stdin.read(size)
161
162 def get_author(self):
9490bd04
FC
163 global bad_mail
164
165 ex = None
23b4a11f
FC
166 m = RAW_AUTHOR_RE.match(self.line)
167 if not m:
168 return None
169 _, name, email, date, tz = m.groups()
9490bd04
FC
170 if name and 'ext:' in name:
171 m = re.match('^(.+?) ext:\((.+)\)$', name)
172 if m:
173 name = m.group(1)
174 ex = urllib.unquote(m.group(2))
23b4a11f 175
9490bd04 176 if email != bad_mail:
23b4a11f
FC
177 if name:
178 user = '%s <%s>' % (name, email)
179 else:
180 user = '<%s>' % (email)
181 else:
182 user = name
183
9490bd04
FC
184 if ex:
185 user += ex
186
23b4a11f
FC
187 tz = int(tz)
188 tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
189 return (user, int(date), -tz)
190
5085a4e2
FC
191def export_file(fc):
192 d = fc.data()
193 print "M %s inline %s" % (gitmode(fc.flags()), fc.path())
194 print "data %d" % len(d)
195 print d
196
197def get_filechanges(repo, ctx, parent):
198 modified = set()
199 added = set()
200 removed = set()
201
202 cur = ctx.manifest()
203 prev = repo[parent].manifest().copy()
204
205 for fn in cur:
206 if fn in prev:
207 if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
208 modified.add(fn)
209 del prev[fn]
210 else:
211 added.add(fn)
212 removed |= set(prev.keys())
213
214 return added | modified, removed
215
9490bd04 216def fixup_user_git(user):
5085a4e2 217 name = mail = None
9490bd04 218 user = user.replace('"', '')
5085a4e2
FC
219 m = AUTHOR_RE.match(user)
220 if m:
221 name = m.group(1)
222 mail = m.group(2).strip()
223 else:
224 m = NAME_RE.match(user)
225 if m:
226 name = m.group(1).strip()
9490bd04
FC
227 return (name, mail)
228
229def fixup_user_hg(user):
230 def sanitize(name):
231 # stole this from hg-git
232 return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))
233
234 m = AUTHOR_HG_RE.match(user)
235 if m:
236 name = sanitize(m.group(1))
237 mail = sanitize(m.group(2))
238 ex = m.group(3)
239 if ex:
240 name += ' ext:(' + urllib.quote(ex) + ')'
241 else:
242 name = sanitize(user)
243 if '@' in user:
244 mail = name
245 else:
246 mail = None
247
248 return (name, mail)
249
250def fixup_user(user):
251 global mode, bad_mail
252
253 if mode == 'git':
254 name, mail = fixup_user_git(user)
255 else:
256 name, mail = fixup_user_hg(user)
5085a4e2
FC
257
258 if not name:
9490bd04 259 name = bad_name
5085a4e2 260 if not mail:
9490bd04 261 mail = bad_mail
5085a4e2
FC
262
263 return '%s <%s>' % (name, mail)
264
265def get_repo(url, alias):
b4e956f7 266 global dirname, peer
5085a4e2
FC
267
268 myui = ui.ui()
269 myui.setconfig('ui', 'interactive', 'off')
270
271 if hg.islocal(url):
272 repo = hg.repository(myui, url)
273 else:
274 local_path = os.path.join(dirname, 'clone')
275 if not os.path.exists(local_path):
276 peer, dstpeer = hg.clone(myui, {}, url, local_path, update=False, pull=True)
277 repo = dstpeer.local()
278 else:
279 repo = hg.repository(myui, local_path)
280 peer = hg.peer(myui, {}, url)
281 repo.pull(peer, heads=None, force=True)
282
283 return repo
284
285def rev_to_mark(rev):
286 global marks
287 return marks.from_rev(rev)
288
23b4a11f
FC
289def mark_to_rev(mark):
290 global marks
291 return marks.to_rev(mark)
292
5085a4e2 293def export_ref(repo, name, kind, head):
422ab5be 294 global prefix, marks, mode
5085a4e2
FC
295
296 ename = '%s/%s' % (kind, name)
297 tip = marks.get_tip(ename)
298
299 # mercurial takes too much time checking this
300 if tip and tip == head.rev():
301 # nothing to do
302 return
cc8433fa 303 revs = xrange(tip, head.rev() + 1)
5085a4e2
FC
304 count = 0
305
306 revs = [rev for rev in revs if not marks.is_marked(rev)]
307
308 for rev in revs:
309
310 c = repo[rev]
311 (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(c.node())
312 rev_branch = extra['branch']
313
314 author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
315 if 'committer' in extra:
316 user, time, tz = extra['committer'].rsplit(' ', 2)
317 committer = "%s %s %s" % (user, time, gittz(int(tz)))
318 else:
319 committer = author
320
321 parents = [p for p in repo.changelog.parentrevs(rev) if p >= 0]
322
323 if len(parents) == 0:
324 modified = c.manifest().keys()
325 removed = []
326 else:
327 modified, removed = get_filechanges(repo, c, parents[0])
328
422ab5be
FC
329 if mode == 'hg':
330 extra_msg = ''
331
332 if rev_branch != 'default':
333 extra_msg += 'branch : %s\n' % rev_branch
334
335 renames = []
336 for f in c.files():
337 if f not in c.manifest():
338 continue
339 rename = c.filectx(f).renamed()
340 if rename:
341 renames.append((rename[0], f))
342
343 for e in renames:
344 extra_msg += "rename : %s => %s\n" % e
345
346 for key, value in extra.iteritems():
347 if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'):
348 continue
349 else:
350 extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value))
351
352 desc += '\n'
353 if extra_msg:
354 desc += '\n--HG--\n' + extra_msg
355
5085a4e2
FC
356 if len(parents) == 0 and rev:
357 print 'reset %s/%s' % (prefix, ename)
358
359 print "commit %s/%s" % (prefix, ename)
360 print "mark :%d" % (marks.get_mark(rev))
361 print "author %s" % (author)
362 print "committer %s" % (committer)
363 print "data %d" % (len(desc))
364 print desc
365
366 if len(parents) > 0:
367 print "from :%s" % (rev_to_mark(parents[0]))
368 if len(parents) > 1:
369 print "merge :%s" % (rev_to_mark(parents[1]))
370
371 for f in modified:
372 export_file(c.filectx(f))
373 for f in removed:
374 print "D %s" % (f)
375 print
376
377 count += 1
378 if (count % 100 == 0):
379 print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs))
380 print "#############################################################"
381
382 # make sure the ref is updated
383 print "reset %s/%s" % (prefix, ename)
384 print "from :%u" % rev_to_mark(rev)
385 print
386
387 marks.set_tip(ename, rev)
388
389def export_tag(repo, tag):
390 export_ref(repo, tag, 'tags', repo[tag])
391
392def export_bookmark(repo, bmark):
393 head = bmarks[bmark]
394 export_ref(repo, bmark, 'bookmarks', head)
395
396def export_branch(repo, branch):
397 tip = get_branch_tip(repo, branch)
398 head = repo[tip]
399 export_ref(repo, branch, 'branches', head)
400
401def export_head(repo):
402 global g_head
403 export_ref(repo, g_head[0], 'bookmarks', g_head[1])
404
405def do_capabilities(parser):
406 global prefix, dirname
407
408 print "import"
23b4a11f 409 print "export"
5085a4e2
FC
410 print "refspec refs/heads/branches/*:%s/branches/*" % prefix
411 print "refspec refs/heads/*:%s/bookmarks/*" % prefix
412 print "refspec refs/tags/*:%s/tags/*" % prefix
23b4a11f
FC
413
414 path = os.path.join(dirname, 'marks-git')
415
416 if os.path.exists(path):
417 print "*import-marks %s" % path
418 print "*export-marks %s" % path
419
5085a4e2
FC
420 print
421
422def get_branch_tip(repo, branch):
423 global branches
424
425 heads = branches.get(branch, None)
426 if not heads:
427 return None
428
429 # verify there's only one head
430 if (len(heads) > 1):
431 warn("Branch '%s' has more than one head, consider merging" % branch)
432 # older versions of mercurial don't have this
433 if hasattr(repo, "branchtip"):
434 return repo.branchtip(branch)
435
436 return heads[0]
437
438def list_head(repo, cur):
46cc3adb 439 global g_head, bmarks
5085a4e2
FC
440
441 head = bookmarks.readcurrent(repo)
46cc3adb
FC
442 if head:
443 node = repo[head]
444 else:
445 # fake bookmark from current branch
446 head = cur
447 node = repo['.']
55dd56e0
FC
448 if not node:
449 node = repo['tip']
46cc3adb
FC
450 if not node:
451 return
452 if head == 'default':
453 head = 'master'
454 bmarks[head] = node
455
5085a4e2
FC
456 print "@refs/heads/%s HEAD" % head
457 g_head = (head, node)
458
459def do_list(parser):
e30473c1 460 global branches, bmarks, mode, track_branches
5085a4e2
FC
461
462 repo = parser.repo
5085a4e2
FC
463 for bmark, node in bookmarks.listbookmarks(repo).iteritems():
464 bmarks[bmark] = repo[node]
465
466 cur = repo.dirstate.branch()
467
468 list_head(repo, cur)
422ab5be 469
e30473c1
FC
470 if track_branches:
471 for branch in repo.branchmap():
472 heads = repo.branchheads(branch)
473 if len(heads):
474 branches[branch] = heads
475
422ab5be
FC
476 for branch in branches:
477 print "? refs/heads/branches/%s" % branch
478
5085a4e2
FC
479 for bmark in bmarks:
480 print "? refs/heads/%s" % bmark
481
482 for tag, node in repo.tagslist():
483 if tag == 'tip':
484 continue
485 print "? refs/tags/%s" % tag
486
487 print
488
489def do_import(parser):
490 repo = parser.repo
491
492 path = os.path.join(dirname, 'marks-git')
493
494 print "feature done"
495 if os.path.exists(path):
496 print "feature import-marks=%s" % path
497 print "feature export-marks=%s" % path
498 sys.stdout.flush()
499
ff247d9e
FC
500 tmp = encoding.encoding
501 encoding.encoding = 'utf-8'
502
5085a4e2
FC
503 # lets get all the import lines
504 while parser.check('import'):
505 ref = parser[1]
506
507 if (ref == 'HEAD'):
508 export_head(repo)
509 elif ref.startswith('refs/heads/branches/'):
510 branch = ref[len('refs/heads/branches/'):]
511 export_branch(repo, branch)
512 elif ref.startswith('refs/heads/'):
513 bmark = ref[len('refs/heads/'):]
514 export_bookmark(repo, bmark)
515 elif ref.startswith('refs/tags/'):
516 tag = ref[len('refs/tags/'):]
517 export_tag(repo, tag)
518
519 parser.next()
520
ff247d9e
FC
521 encoding.encoding = tmp
522
5085a4e2
FC
523 print 'done'
524
23b4a11f
FC
525def parse_blob(parser):
526 global blob_marks
527
528 parser.next()
529 mark = parser.get_mark()
530 parser.next()
531 data = parser.get_data()
532 blob_marks[mark] = data
533 parser.next()
534 return
535
6497a2ba
FC
536def get_merge_files(repo, p1, p2, files):
537 for e in repo[p1].files():
538 if e not in files:
539 if e not in repo[p1].manifest():
540 continue
541 f = { 'ctx' : repo[p1][e] }
542 files[e] = f
543
23b4a11f
FC
544def parse_commit(parser):
545 global marks, blob_marks, bmarks, parsed_refs
422ab5be 546 global mode
23b4a11f
FC
547
548 from_mark = merge_mark = None
549
550 ref = parser[1]
551 parser.next()
552
553 commit_mark = parser.get_mark()
554 parser.next()
555 author = parser.get_author()
556 parser.next()
557 committer = parser.get_author()
558 parser.next()
559 data = parser.get_data()
560 parser.next()
561 if parser.check('from'):
562 from_mark = parser.get_mark()
563 parser.next()
564 if parser.check('merge'):
565 merge_mark = parser.get_mark()
566 parser.next()
567 if parser.check('merge'):
568 die('octopus merges are not supported yet')
569
570 files = {}
571
572 for line in parser:
573 if parser.check('M'):
418673c4 574 t, m, mark_ref, path = line.split(' ', 3)
23b4a11f
FC
575 mark = int(mark_ref[1:])
576 f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
577 elif parser.check('D'):
578 t, path = line.split(' ')
579 f = { 'deleted' : True }
580 else:
581 die('Unknown file command: %s' % line)
582 files[path] = f
583
584 def getfilectx(repo, memctx, f):
585 of = files[f]
586 if 'deleted' in of:
587 raise IOError
6497a2ba
FC
588 if 'ctx' in of:
589 return of['ctx']
23b4a11f
FC
590 is_exec = of['mode'] == 'x'
591 is_link = of['mode'] == 'l'
422ab5be
FC
592 rename = of.get('rename', None)
593 return context.memfilectx(f, of['data'],
594 is_link, is_exec, rename)
23b4a11f
FC
595
596 repo = parser.repo
597
598 user, date, tz = author
599 extra = {}
600
601 if committer != author:
602 extra['committer'] = "%s %u %u" % committer
603
604 if from_mark:
605 p1 = repo.changelog.node(mark_to_rev(from_mark))
606 else:
607 p1 = '\0' * 20
608
609 if merge_mark:
610 p2 = repo.changelog.node(mark_to_rev(merge_mark))
611 else:
612 p2 = '\0' * 20
613
6497a2ba
FC
614 #
615 # If files changed from any of the parents, hg wants to know, but in git if
616 # nothing changed from the first parent, nothing changed.
617 #
618 if merge_mark:
619 get_merge_files(repo, p1, p2, files)
620
422ab5be
FC
621 if mode == 'hg':
622 i = data.find('\n--HG--\n')
623 if i >= 0:
624 tmp = data[i + len('\n--HG--\n'):].strip()
625 for k, v in [e.split(' : ') for e in tmp.split('\n')]:
626 if k == 'rename':
627 old, new = v.split(' => ', 1)
628 files[new]['rename'] = old
629 elif k == 'branch':
630 extra[k] = v
631 elif k == 'extra':
632 ek, ev = v.split(' : ', 1)
633 extra[ek] = urllib.unquote(ev)
634 data = data[:i]
635
23b4a11f
FC
636 ctx = context.memctx(repo, (p1, p2), data,
637 files.keys(), getfilectx,
638 user, (date, tz), extra)
639
ff247d9e
FC
640 tmp = encoding.encoding
641 encoding.encoding = 'utf-8'
642
23b4a11f
FC
643 node = repo.commitctx(ctx)
644
ff247d9e
FC
645 encoding.encoding = tmp
646
23b4a11f
FC
647 rev = repo[node].rev()
648
649 parsed_refs[ref] = node
650
651 marks.new_mark(rev, commit_mark)
652
653def parse_reset(parser):
654 ref = parser[1]
655 parser.next()
656 # ugh
657 if parser.check('commit'):
658 parse_commit(parser)
659 return
660 if not parser.check('from'):
661 return
662 from_mark = parser.get_mark()
663 parser.next()
664
665 node = parser.repo.changelog.node(mark_to_rev(from_mark))
666 parsed_refs[ref] = node
667
668def parse_tag(parser):
669 name = parser[1]
670 parser.next()
671 from_mark = parser.get_mark()
672 parser.next()
673 tagger = parser.get_author()
674 parser.next()
675 data = parser.get_data()
676 parser.next()
677
678 # nothing to do
679
680def do_export(parser):
b4e956f7 681 global parsed_refs, bmarks, peer
23b4a11f
FC
682
683 parser.next()
684
685 for line in parser.each_block('done'):
686 if parser.check('blob'):
687 parse_blob(parser)
688 elif parser.check('commit'):
689 parse_commit(parser)
690 elif parser.check('reset'):
691 parse_reset(parser)
692 elif parser.check('tag'):
693 parse_tag(parser)
694 elif parser.check('feature'):
695 pass
696 else:
697 die('unhandled export command: %s' % line)
698
699 for ref, node in parsed_refs.iteritems():
700 if ref.startswith('refs/heads/branches'):
701 pass
702 elif ref.startswith('refs/heads/'):
703 bmark = ref[len('refs/heads/'):]
704 if bmark in bmarks:
705 old = bmarks[bmark].hex()
706 else:
707 old = ''
708 if not bookmarks.pushbookmark(parser.repo, bmark, old, node):
709 continue
710 elif ref.startswith('refs/tags/'):
711 tag = ref[len('refs/tags/'):]
712 parser.repo.tag([tag], node, None, True, None, {})
08c2599c
FC
713 else:
714 # transport-helper/fast-export bugs
715 continue
23b4a11f
FC
716 print "ok %s" % ref
717
718 print
719
b4e956f7
FC
720 if peer:
721 parser.repo.push(peer, force=False)
722
5085a4e2 723def main(args):
23b4a11f
FC
724 global prefix, dirname, branches, bmarks
725 global marks, blob_marks, parsed_refs
9490bd04 726 global peer, mode, bad_mail, bad_name
e30473c1 727 global track_branches
5085a4e2
FC
728
729 alias = args[1]
730 url = args[2]
b4e956f7 731 peer = None
422ab5be 732
422ab5be 733 hg_git_compat = False
e30473c1 734 track_branches = True
422ab5be 735 try:
1e310551 736 if get_config('remote-hg.hg-git-compat') == 'true\n':
422ab5be 737 hg_git_compat = True
e30473c1 738 track_branches = False
1e310551 739 if get_config('remote-hg.track-branches') == 'false\n':
e30473c1 740 track_branches = False
422ab5be
FC
741 except subprocess.CalledProcessError:
742 pass
743
744 if hg_git_compat:
745 mode = 'hg'
9490bd04
FC
746 bad_mail = 'none@none'
747 bad_name = ''
422ab5be
FC
748 else:
749 mode = 'git'
9490bd04
FC
750 bad_mail = 'unknown'
751 bad_name = 'Unknown'
5085a4e2 752
ffaf84c6
FC
753 if alias[4:] == url:
754 is_tmp = True
755 alias = util.sha1(alias).hexdigest()
756 else:
757 is_tmp = False
758
5085a4e2
FC
759 gitdir = os.environ['GIT_DIR']
760 dirname = os.path.join(gitdir, 'hg', alias)
761 branches = {}
762 bmarks = {}
23b4a11f
FC
763 blob_marks = {}
764 parsed_refs = {}
5085a4e2
FC
765
766 repo = get_repo(url, alias)
767 prefix = 'refs/hg/%s' % alias
768
769 if not os.path.exists(dirname):
770 os.makedirs(dirname)
771
772 marks_path = os.path.join(dirname, 'marks-hg')
773 marks = Marks(marks_path)
774
775 parser = Parser(repo)
776 for line in parser:
777 if parser.check('capabilities'):
778 do_capabilities(parser)
779 elif parser.check('list'):
780 do_list(parser)
781 elif parser.check('import'):
782 do_import(parser)
783 elif parser.check('export'):
784 do_export(parser)
785 else:
786 die('unhandled command: %s' % line)
787 sys.stdout.flush()
788
ffaf84c6
FC
789 if not is_tmp:
790 marks.store()
791 else:
792 shutil.rmtree(dirname)
5085a4e2
FC
793
794sys.exit(main(sys.argv))