remote-bzr: improve progress reporting
[git.git] / contrib / remote-helpers / git-remote-bzr
blob2e210089bfac32512bc2435665fc4c29bea805e6
1 #!/usr/bin/env python
3 # Copyright (c) 2012 Felipe Contreras
7 # Just copy to your ~/bin, or anywhere in your $PATH.
8 # Then you can clone with:
9 # % git clone bzr::/path/to/bzr/repo/or/url
11 # For example:
12 # % git clone bzr::$HOME/myrepo
13 # or
14 # % git clone bzr::lp:myrepo
16 # If you want to specify which branches you want track (per repo):
17 # git config remote-bzr.branches 'trunk, devel, test'
20 import sys
22 import bzrlib
23 if hasattr(bzrlib, "initialize"):
24 bzrlib.initialize()
26 import bzrlib.plugin
27 bzrlib.plugin.load_plugins()
29 import bzrlib.generate_ids
30 import bzrlib.transport
31 import bzrlib.errors
32 import bzrlib.ui
33 import bzrlib.urlutils
35 import sys
36 import os
37 import json
38 import re
39 import StringIO
40 import atexit, shutil, hashlib, urlparse, subprocess
42 NAME_RE = re.compile('^([^<>]+)')
43 AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
44 EMAIL_RE = re.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
45 RAW_AUTHOR_RE = re.compile('^(\w+) (.+)? <(.*)> (\d+) ([+-]\d+)')
47 def die(msg, *args):
48 sys.stderr.write('ERROR: %s\n' % (msg % args))
49 sys.exit(1)
51 def warn(msg, *args):
52 sys.stderr.write('WARNING: %s\n' % (msg % args))
54 def gittz(tz):
55 return '%+03d%02d' % (tz / 3600, tz % 3600 / 60)
57 def get_config(config):
58 cmd = ['git', 'config', '--get', config]
59 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
60 output, _ = process.communicate()
61 return output
63 class Marks:
65 def __init__(self, path):
66 self.path = path
67 self.tips = {}
68 self.marks = {}
69 self.rev_marks = {}
70 self.last_mark = 0
71 self.load()
73 def load(self):
74 if not os.path.exists(self.path):
75 return
77 tmp = json.load(open(self.path))
78 self.tips = tmp['tips']
79 self.marks = tmp['marks']
80 self.last_mark = tmp['last-mark']
82 for rev, mark in self.marks.iteritems():
83 self.rev_marks[mark] = rev
85 def dict(self):
86 return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
88 def store(self):
89 json.dump(self.dict(), open(self.path, 'w'))
91 def __str__(self):
92 return str(self.dict())
94 def from_rev(self, rev):
95 return self.marks[rev]
97 def to_rev(self, mark):
98 return self.rev_marks[mark]
100 def next_mark(self):
101 self.last_mark += 1
102 return self.last_mark
104 def get_mark(self, rev):
105 self.last_mark += 1
106 self.marks[rev] = self.last_mark
107 return self.last_mark
109 def is_marked(self, rev):
110 return rev in self.marks
112 def new_mark(self, rev, mark):
113 self.marks[rev] = mark
114 self.rev_marks[mark] = rev
115 self.last_mark = mark
117 def get_tip(self, branch):
118 return self.tips.get(branch, None)
120 def set_tip(self, branch, tip):
121 self.tips[branch] = tip
123 class Parser:
125 def __init__(self, repo):
126 self.repo = repo
127 self.line = self.get_line()
129 def get_line(self):
130 return sys.stdin.readline().strip()
132 def __getitem__(self, i):
133 return self.line.split()[i]
135 def check(self, word):
136 return self.line.startswith(word)
138 def each_block(self, separator):
139 while self.line != separator:
140 yield self.line
141 self.line = self.get_line()
143 def __iter__(self):
144 return self.each_block('')
146 def next(self):
147 self.line = self.get_line()
148 if self.line == 'done':
149 self.line = None
151 def get_mark(self):
152 i = self.line.index(':') + 1
153 return int(self.line[i:])
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)
162 def get_author(self):
163 m = RAW_AUTHOR_RE.match(self.line)
164 if not m:
165 return None
166 _, name, email, date, tz = m.groups()
167 committer = '%s <%s>' % (name, email)
168 tz = int(tz)
169 tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
170 return (committer, int(date), tz)
172 def rev_to_mark(rev):
173 global marks
174 return marks.from_rev(rev)
176 def mark_to_rev(mark):
177 global marks
178 return marks.to_rev(mark)
180 def fixup_user(user):
181 name = mail = None
182 user = user.replace('"', '')
183 m = AUTHOR_RE.match(user)
184 if m:
185 name = m.group(1)
186 mail = m.group(2).strip()
187 else:
188 m = EMAIL_RE.match(user)
189 if m:
190 name = m.group(1)
191 mail = m.group(2)
192 else:
193 m = NAME_RE.match(user)
194 if m:
195 name = m.group(1).strip()
197 if not name:
198 name = 'unknown'
199 if not mail:
200 mail = 'Unknown'
202 return '%s <%s>' % (name, mail)
204 def get_filechanges(cur, prev):
205 modified = {}
206 removed = {}
208 changes = cur.changes_from(prev)
210 def u(s):
211 return s.encode('utf-8')
213 for path, fid, kind in changes.added:
214 modified[u(path)] = fid
215 for path, fid, kind in changes.removed:
216 removed[u(path)] = None
217 for path, fid, kind, mod, _ in changes.modified:
218 modified[u(path)] = fid
219 for oldpath, newpath, fid, kind, mod, _ in changes.renamed:
220 removed[u(oldpath)] = None
221 if kind == 'directory':
222 lst = cur.list_files(from_dir=newpath, recursive=True)
223 for path, file_class, kind, fid, entry in lst:
224 if kind != 'directory':
225 modified[u(newpath + '/' + path)] = fid
226 else:
227 modified[u(newpath)] = fid
229 return modified, removed
231 def export_files(tree, files):
232 global marks, filenodes
234 final = []
235 for path, fid in files.iteritems():
236 kind = tree.kind(fid)
238 h = tree.get_file_sha1(fid)
240 if kind == 'symlink':
241 d = tree.get_symlink_target(fid)
242 mode = '120000'
243 elif kind == 'file':
245 if tree.is_executable(fid):
246 mode = '100755'
247 else:
248 mode = '100644'
250 # is the blob already exported?
251 if h in filenodes:
252 mark = filenodes[h]
253 final.append((mode, mark, path))
254 continue
256 d = tree.get_file_text(fid)
257 elif kind == 'directory':
258 continue
259 else:
260 die("Unhandled kind '%s' for path '%s'" % (kind, path))
262 mark = marks.next_mark()
263 filenodes[h] = mark
265 print "blob"
266 print "mark :%u" % mark
267 print "data %d" % len(d)
268 print d
270 final.append((mode, mark, path))
272 return final
274 def export_branch(repo, name):
275 global prefix
277 ref = '%s/heads/%s' % (prefix, name)
278 tip = marks.get_tip(name)
280 branch = branches[name]
281 repo = branch.repository
283 branch.lock_read()
284 revs = branch.iter_merge_sorted_revisions(None, tip, 'exclude', 'forward')
285 tip_revno = branch.revision_id_to_revno(tip)
286 last_revno, _ = branch.last_revision_info()
287 total = last_revno - tip_revno
289 revs = [revid, seq for revid, _, seq, _ in revs if not marks.is_marked(revid)]
291 for revid, seq in revs:
293 rev = repo.get_revision(revid)
294 revno = seq[0]
296 parents = rev.parent_ids
297 time = rev.timestamp
298 tz = rev.timezone
299 committer = rev.committer.encode('utf-8')
300 committer = "%s %u %s" % (fixup_user(committer), time, gittz(tz))
301 authors = rev.get_apparent_authors()
302 if authors:
303 author = authors[0].encode('utf-8')
304 author = "%s %u %s" % (fixup_user(author), time, gittz(tz))
305 else:
306 author = committer
307 msg = rev.message.encode('utf-8')
309 msg += '\n'
311 if len(parents) == 0:
312 parent = bzrlib.revision.NULL_REVISION
313 else:
314 parent = parents[0]
316 cur_tree = repo.revision_tree(revid)
317 prev = repo.revision_tree(parent)
318 modified, removed = get_filechanges(cur_tree, prev)
320 modified_final = export_files(cur_tree, modified)
322 if len(parents) == 0:
323 print 'reset %s' % ref
325 print "commit %s" % ref
326 print "mark :%d" % (marks.get_mark(revid))
327 print "author %s" % (author)
328 print "committer %s" % (committer)
329 print "data %d" % (len(msg))
330 print msg
332 for i, p in enumerate(parents):
333 try:
334 m = rev_to_mark(p)
335 except KeyError:
336 # ghost?
337 continue
338 if i == 0:
339 print "from :%s" % m
340 else:
341 print "merge :%s" % m
343 for f in removed:
344 print "D %s" % (f,)
345 for f in modified_final:
346 print "M %s :%u %s" % f
347 print
349 if len(seq) > 1:
350 # let's skip branch revisions from the progress report
351 continue
353 progress = (revno - tip_revno)
354 if (progress % 100 == 0):
355 print "progress revision %d '%s' (%d/%d)" % (revno, name, progress, total)
357 branch.unlock()
359 revid = branch.last_revision()
361 # make sure the ref is updated
362 print "reset %s" % ref
363 print "from :%u" % rev_to_mark(revid)
364 print
366 marks.set_tip(name, revid)
368 def export_tag(repo, name):
369 global tags, prefix
371 ref = '%s/tags/%s' % (prefix, name)
372 print "reset %s" % ref
373 print "from :%u" % rev_to_mark(tags[name])
374 print
376 def do_import(parser):
377 global dirname
379 repo = parser.repo
380 path = os.path.join(dirname, 'marks-git')
382 print "feature done"
383 if os.path.exists(path):
384 print "feature import-marks=%s" % path
385 print "feature export-marks=%s" % path
386 print "feature force"
387 sys.stdout.flush()
389 while parser.check('import'):
390 ref = parser[1]
391 if ref.startswith('refs/heads/'):
392 name = ref[len('refs/heads/'):]
393 export_branch(repo, name)
394 if ref.startswith('refs/tags/'):
395 name = ref[len('refs/tags/'):]
396 export_tag(repo, name)
397 parser.next()
399 print 'done'
401 sys.stdout.flush()
403 def parse_blob(parser):
404 global blob_marks
406 parser.next()
407 mark = parser.get_mark()
408 parser.next()
409 data = parser.get_data()
410 blob_marks[mark] = data
411 parser.next()
413 class CustomTree():
415 def __init__(self, branch, revid, parents, files):
416 global files_cache
418 self.updates = {}
419 self.branch = branch
421 def copy_tree(revid):
422 files = files_cache[revid] = {}
423 branch.lock_read()
424 tree = branch.repository.revision_tree(revid)
425 try:
426 for path, entry in tree.iter_entries_by_dir():
427 files[path] = [entry.file_id, None]
428 finally:
429 branch.unlock()
430 return files
432 if len(parents) == 0:
433 self.base_id = bzrlib.revision.NULL_REVISION
434 self.base_files = {}
435 else:
436 self.base_id = parents[0]
437 self.base_files = files_cache.get(self.base_id, None)
438 if not self.base_files:
439 self.base_files = copy_tree(self.base_id)
441 self.files = files_cache[revid] = self.base_files.copy()
442 self.rev_files = {}
444 for path, data in self.files.iteritems():
445 fid, mark = data
446 self.rev_files[fid] = [path, mark]
448 for path, f in files.iteritems():
449 fid, mark = self.files.get(path, [None, None])
450 if not fid:
451 fid = bzrlib.generate_ids.gen_file_id(path)
452 f['path'] = path
453 self.rev_files[fid] = [path, mark]
454 self.updates[fid] = f
456 def last_revision(self):
457 return self.base_id
459 def iter_changes(self):
460 changes = []
462 def get_parent(dirname, basename):
463 parent_fid, mark = self.base_files.get(dirname, [None, None])
464 if parent_fid:
465 return parent_fid
466 parent_fid, mark = self.files.get(dirname, [None, None])
467 if parent_fid:
468 return parent_fid
469 if basename == '':
470 return None
471 fid = bzrlib.generate_ids.gen_file_id(path)
472 add_entry(fid, dirname, 'directory')
473 return fid
475 def add_entry(fid, path, kind, mode = None):
476 dirname, basename = os.path.split(path)
477 parent_fid = get_parent(dirname, basename)
479 executable = False
480 if mode == '100755':
481 executable = True
482 elif mode == '120000':
483 kind = 'symlink'
485 change = (fid,
486 (None, path),
487 True,
488 (False, True),
489 (None, parent_fid),
490 (None, basename),
491 (None, kind),
492 (None, executable))
493 self.files[path] = [change[0], None]
494 changes.append(change)
496 def update_entry(fid, path, kind, mode = None):
497 dirname, basename = os.path.split(path)
498 parent_fid = get_parent(dirname, basename)
500 executable = False
501 if mode == '100755':
502 executable = True
503 elif mode == '120000':
504 kind = 'symlink'
506 change = (fid,
507 (path, path),
508 True,
509 (True, True),
510 (None, parent_fid),
511 (None, basename),
512 (None, kind),
513 (None, executable))
514 self.files[path] = [change[0], None]
515 changes.append(change)
517 def remove_entry(fid, path, kind):
518 dirname, basename = os.path.split(path)
519 parent_fid = get_parent(dirname, basename)
520 change = (fid,
521 (path, None),
522 True,
523 (True, False),
524 (parent_fid, None),
525 (None, None),
526 (None, None),
527 (None, None))
528 del self.files[path]
529 changes.append(change)
531 for fid, f in self.updates.iteritems():
532 path = f['path']
534 if 'deleted' in f:
535 remove_entry(fid, path, 'file')
536 continue
538 if path in self.base_files:
539 update_entry(fid, path, 'file', f['mode'])
540 else:
541 add_entry(fid, path, 'file', f['mode'])
543 self.files[path][1] = f['mark']
544 self.rev_files[fid][1] = f['mark']
546 return changes
548 def get_content(self, file_id):
549 path, mark = self.rev_files[file_id]
550 if mark:
551 return blob_marks[mark]
553 # last resort
554 tree = self.branch.repository.revision_tree(self.base_id)
555 return tree.get_file_text(file_id)
557 def get_file_with_stat(self, file_id, path=None):
558 content = self.get_content(file_id)
559 return (StringIO.StringIO(content), None)
561 def get_symlink_target(self, file_id):
562 return self.get_content(file_id)
564 def id2path(self, file_id):
565 path, mark = self.rev_files[file_id]
566 return path
568 def c_style_unescape(string):
569 if string[0] == string[-1] == '"':
570 return string.decode('string-escape')[1:-1]
571 return string
573 def parse_commit(parser):
574 global marks, blob_marks, parsed_refs
575 global mode
577 parents = []
579 ref = parser[1]
580 parser.next()
582 if ref.startswith('refs/heads/'):
583 name = ref[len('refs/heads/'):]
584 branch = branches[name]
585 else:
586 die('unknown ref')
588 commit_mark = parser.get_mark()
589 parser.next()
590 author = parser.get_author()
591 parser.next()
592 committer = parser.get_author()
593 parser.next()
594 data = parser.get_data()
595 parser.next()
596 if parser.check('from'):
597 parents.append(parser.get_mark())
598 parser.next()
599 while parser.check('merge'):
600 parents.append(parser.get_mark())
601 parser.next()
603 # fast-export adds an extra newline
604 if data[-1] == '\n':
605 data = data[:-1]
607 files = {}
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' : m, 'mark' : mark }
614 elif parser.check('D'):
615 t, path = line.split(' ')
616 f = { 'deleted' : True }
617 else:
618 die('Unknown file command: %s' % line)
619 path = c_style_unescape(path).decode('utf-8')
620 files[path] = f
622 committer, date, tz = committer
623 parents = [str(mark_to_rev(p)) for p in parents]
624 revid = bzrlib.generate_ids.gen_revision_id(committer, date)
625 props = {}
626 props['branch-nick'] = branch.nick
628 mtree = CustomTree(branch, revid, parents, files)
629 changes = mtree.iter_changes()
631 branch.lock_write()
632 try:
633 builder = branch.get_commit_builder(parents, None, date, tz, committer, props, revid)
634 try:
635 list(builder.record_iter_changes(mtree, mtree.last_revision(), changes))
636 builder.finish_inventory()
637 builder.commit(data.decode('utf-8', 'replace'))
638 except Exception, e:
639 builder.abort()
640 raise
641 finally:
642 branch.unlock()
644 parsed_refs[ref] = revid
645 marks.new_mark(revid, commit_mark)
647 def parse_reset(parser):
648 global parsed_refs
650 ref = parser[1]
651 parser.next()
653 # ugh
654 if parser.check('commit'):
655 parse_commit(parser)
656 return
657 if not parser.check('from'):
658 return
659 from_mark = parser.get_mark()
660 parser.next()
662 parsed_refs[ref] = mark_to_rev(from_mark)
664 def do_export(parser):
665 global parsed_refs, dirname
667 parser.next()
669 for line in parser.each_block('done'):
670 if parser.check('blob'):
671 parse_blob(parser)
672 elif parser.check('commit'):
673 parse_commit(parser)
674 elif parser.check('reset'):
675 parse_reset(parser)
676 elif parser.check('tag'):
677 pass
678 elif parser.check('feature'):
679 pass
680 else:
681 die('unhandled export command: %s' % line)
683 for ref, revid in parsed_refs.iteritems():
684 name = ref[len('refs/heads/'):]
685 branch = branches[name]
686 branch.generate_revision_history(revid, marks.get_tip(name))
688 if name in peers:
689 peer = peers[name]
690 try:
691 peer.bzrdir.push_branch(branch, revision_id=revid)
692 except bzrlib.errors.DivergedBranches:
693 print "error %s non-fast forward" % ref
694 continue
696 try:
697 wt = branch.bzrdir.open_workingtree()
698 wt.update()
699 except bzrlib.errors.NoWorkingTree:
700 pass
702 print "ok %s" % ref
704 print
706 def do_capabilities(parser):
707 global dirname
709 print "import"
710 print "export"
711 print "refspec refs/heads/*:%s/heads/*" % prefix
712 print "refspec refs/tags/*:%s/tags/*" % prefix
714 path = os.path.join(dirname, 'marks-git')
716 if os.path.exists(path):
717 print "*import-marks %s" % path
718 print "*export-marks %s" % path
720 print
722 def ref_is_valid(name):
723 return not True in [c in name for c in '~^: \\']
725 def do_list(parser):
726 global tags
728 master_branch = None
730 for name in branches:
731 if not master_branch:
732 master_branch = name
733 print "? refs/heads/%s" % name
735 branch = branches[master_branch]
736 branch.lock_read()
737 for tag, revid in branch.tags.get_tag_dict().items():
738 try:
739 branch.revision_id_to_dotted_revno(revid)
740 except bzrlib.errors.NoSuchRevision:
741 continue
742 if not ref_is_valid(tag):
743 continue
744 print "? refs/tags/%s" % tag
745 tags[tag] = revid
746 branch.unlock()
748 print "@refs/heads/%s HEAD" % master_branch
749 print
751 def get_remote_branch(origin, remote_branch, name):
752 global dirname, peers
754 branch_path = os.path.join(dirname, 'clone', name)
755 if os.path.exists(branch_path):
756 # pull
757 d = bzrlib.bzrdir.BzrDir.open(branch_path)
758 branch = d.open_branch()
759 try:
760 branch.pull(remote_branch, [], None, False)
761 except bzrlib.errors.DivergedBranches:
762 # use remote branch for now
763 return remote_branch
764 else:
765 # clone
766 d = origin.sprout(branch_path, None,
767 hardlink=True, create_tree_if_local=False,
768 force_new_repo=False,
769 source_branch=remote_branch)
770 branch = d.open_branch()
772 return branch
774 def find_branches(repo, wanted):
775 transport = repo.user_transport
777 for fn in transport.iter_files_recursive():
778 if not fn.endswith('.bzr/branch-format'):
779 continue
781 name = subdir = fn[:-len('/.bzr/branch-format')]
782 name = name if name != '' else 'master'
783 name = name.replace('/', '+')
785 if wanted and not name in wanted:
786 continue
788 try:
789 cur = transport.clone(subdir)
790 branch = bzrlib.branch.Branch.open_from_transport(cur)
791 except bzrlib.errors.NotBranchError:
792 continue
793 else:
794 yield name, branch
796 def get_repo(url, alias):
797 global dirname, peer, branches
799 normal_url = bzrlib.urlutils.normalize_url(url)
800 origin = bzrlib.bzrdir.BzrDir.open(url)
801 is_local = isinstance(origin.transport, bzrlib.transport.local.LocalTransport)
803 shared_path = os.path.join(gitdir, 'bzr')
804 try:
805 shared_dir = bzrlib.bzrdir.BzrDir.open(shared_path)
806 except bzrlib.errors.NotBranchError:
807 shared_dir = bzrlib.bzrdir.BzrDir.create(shared_path)
808 try:
809 shared_repo = shared_dir.open_repository()
810 except bzrlib.errors.NoRepositoryPresent:
811 shared_repo = shared_dir.create_repository(shared=True)
813 if not is_local:
814 clone_path = os.path.join(dirname, 'clone')
815 if not os.path.exists(clone_path):
816 os.mkdir(clone_path)
818 try:
819 repo = origin.open_repository()
820 except bzrlib.errors.NoRepositoryPresent:
821 # branch
823 name = 'master'
824 branch = origin.open_branch()
826 if not is_local:
827 peers[name] = branch
828 branches[name] = get_remote_branch(origin, branch, name)
829 else:
830 branches[name] = branch
832 return branch.repository
833 else:
834 # repository
836 wanted = get_config('remote-bzr.branches').rstrip().split(', ')
837 # stupid python
838 wanted = [e for e in wanted if e]
840 for name, branch in find_branches(repo, wanted):
842 if not is_local:
843 peers[name] = branch
844 branches[name] = get_remote_branch(origin, branch, name)
845 else:
846 branches[name] = branch
848 return repo
850 def fix_path(alias, orig_url):
851 url = urlparse.urlparse(orig_url, 'file')
852 if url.scheme != 'file' or os.path.isabs(url.path):
853 return
854 abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
855 cmd = ['git', 'config', 'remote.%s.url' % alias, "bzr::%s" % abs_url]
856 subprocess.call(cmd)
858 def main(args):
859 global marks, prefix, gitdir, dirname
860 global tags, filenodes
861 global blob_marks
862 global parsed_refs
863 global files_cache
864 global is_tmp
865 global branches, peers
867 alias = args[1]
868 url = args[2]
870 tags = {}
871 filenodes = {}
872 blob_marks = {}
873 parsed_refs = {}
874 files_cache = {}
875 marks = None
876 branches = {}
877 peers = {}
879 if alias[5:] == url:
880 is_tmp = True
881 alias = hashlib.sha1(alias).hexdigest()
882 else:
883 is_tmp = False
885 prefix = 'refs/bzr/%s' % alias
886 gitdir = os.environ['GIT_DIR']
887 dirname = os.path.join(gitdir, 'bzr', alias)
889 if not is_tmp:
890 fix_path(alias, url)
892 if not os.path.exists(dirname):
893 os.makedirs(dirname)
895 bzrlib.ui.ui_factory.be_quiet(True)
897 repo = get_repo(url, alias)
899 marks_path = os.path.join(dirname, 'marks-int')
900 marks = Marks(marks_path)
902 parser = Parser(repo)
903 for line in parser:
904 if parser.check('capabilities'):
905 do_capabilities(parser)
906 elif parser.check('list'):
907 do_list(parser)
908 elif parser.check('import'):
909 do_import(parser)
910 elif parser.check('export'):
911 do_export(parser)
912 else:
913 die('unhandled command: %s' % line)
914 sys.stdout.flush()
916 def bye():
917 if not marks:
918 return
919 if not is_tmp:
920 marks.store()
921 else:
922 shutil.rmtree(dirname)
924 atexit.register(bye)
925 sys.exit(main(sys.argv))