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