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