aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTommi Virtanen <tv@eagain.net>2007-09-01 18:27:25 -0700
committerTommi Virtanen <tv@eagain.net>2007-09-01 18:59:13 -0700
commit97c093470e2cd5f968422be938b2086d07e68757 (patch)
tree2c858db70a4753347191adc1f9167f8b5ec35e8b
parentAdd gitosis-run-hook, to be run from git hooks. (diff)
downloadgitosis-dakkar-97c093470e2cd5f968422be938b2086d07e68757.tar.gz
gitosis-dakkar-97c093470e2cd5f968422be938b2086d07e68757.tar.bz2
gitosis-dakkar-97c093470e2cd5f968422be938b2086d07e68757.zip
Add gitosis-init, for bootstrapping a new installation.
-rw-r--r--gitosis/init.py178
-rw-r--r--gitosis/repository.py26
-rw-r--r--gitosis/templates/__init__.py3
-rwxr-xr-xgitosis/templates/admin/hooks/post-update4
-rw-r--r--gitosis/test/test_init.py83
-rw-r--r--gitosis/test/test_repository.py28
-rw-r--r--gitosis/test/util.py13
-rwxr-xr-xsetup.py1
8 files changed, 336 insertions, 0 deletions
diff --git a/gitosis/init.py b/gitosis/init.py
new file mode 100644
index 0000000..bf0eb9b
--- /dev/null
+++ b/gitosis/init.py
@@ -0,0 +1,178 @@
+"""
+Initialize a user account for use with gitosis.
+"""
+
+import errno
+import logging
+import optparse
+import os
+import re
+import subprocess
+import sys
+
+from pkg_resources import resource_filename
+from cStringIO import StringIO
+from ConfigParser import RawConfigParser
+
+from gitosis import repository
+from gitosis import util
+
+log = logging.getLogger('gitosis.init')
+
+def die(msg):
+ log.error(msg)
+ sys.exit(1)
+
+def read_ssh_pubkey(fp=None):
+ if fp is None:
+ fp = sys.stdin
+ line = fp.readline()
+ return line
+
+_ACCEPTABLE_USER_RE = re.compile(r'^[a-z][a-z0-9]*@[a-z][a-z0-9]*$')
+
+class InsecureSSHKeyUsername(Exception):
+ """Username contains not allowed characters"""
+
+ def __str__(self):
+ return '%s: %s' % (self.__doc__, ': '.join(self.args))
+
+def ssh_extract_user(pubkey):
+ _, user = pubkey.rsplit(None, 1)
+ if _ACCEPTABLE_USER_RE.match(user):
+ return user
+ else:
+ raise InsecureSSHKeyUsername(repr(user))
+
+def initial_commit(git_dir, cfg, pubkey, user):
+ repository.fast_import(
+ git_dir=git_dir,
+ commit_msg='Automatic creation of gitosis repository.',
+ committer='Gitosis Admin <%s>' % user,
+ files=[
+ ('keydir/%s.pub' % user, pubkey),
+ ('gitosis.conf', cfg),
+ ],
+ )
+
+def run_post_update(git_dir):
+ args = [os.path.join(git_dir, 'hooks', 'post-update')]
+ returncode = subprocess.call(
+ args=args,
+ cwd=git_dir,
+ close_fds=True,
+ env=dict(GIT_DIR='.'),
+ )
+ if returncode != 0:
+ die(
+ ("post-update returned non-zero exit status %d"
+ % returncode),
+ )
+
+def symlink_config(git_dir):
+ dst = os.path.expanduser('~/.gitosis.conf')
+ tmp = '%s.%d.tmp' % (dst, os.getpid())
+ try:
+ os.unlink(tmp)
+ except OSError, e:
+ if e.errno == errno.ENOENT:
+ pass
+ else:
+ raise
+ os.symlink(
+ os.path.join(git_dir, 'gitosis.conf'),
+ tmp,
+ )
+ os.rename(tmp, dst)
+
+def getParser():
+ parser = optparse.OptionParser(
+ usage='%prog',
+ description='Initialize a user account for use with gitosis',
+ )
+ parser.set_defaults(
+ config=os.path.expanduser('~/.gitosis.conf'),
+ )
+ parser.add_option('--config',
+ metavar='FILE',
+ help='read config from FILE',
+ )
+ return parser
+
+def init_admin_repository(
+ git_dir,
+ pubkey,
+ user,
+ ):
+ repository.init(
+ path=git_dir,
+ template=resource_filename('gitosis.templates', 'admin')
+ )
+ repository.init(
+ path=git_dir,
+ )
+ if not repository.has_initial_commit(git_dir):
+ log.info('Making initial commit...')
+ # ConfigParser does not guarantee order, so jump through hoops
+ # to make sure [gitosis] is first
+ cfg_file = StringIO()
+ print >>cfg_file, '[gitosis]'
+ print >>cfg_file
+ cfg = RawConfigParser()
+ cfg.add_section('group gitosis-admin')
+ cfg.set('group gitosis-admin', 'members', user)
+ cfg.set('group gitosis-admin', 'writable', 'gitosis-admin')
+ cfg.write(cfg_file)
+ initial_commit(
+ git_dir=git_dir,
+ cfg=cfg_file.getvalue(),
+ pubkey=pubkey,
+ user=user,
+ )
+
+def main():
+ logging.basicConfig(level=logging.INFO)
+ os.umask(0022)
+
+ parser = getParser()
+ (options, args) = parser.parse_args()
+ if args:
+ parser.error('Did not expect arguments.')
+
+ cfg = RawConfigParser()
+ try:
+ conffile = file(options.config)
+ except (IOError, OSError), e:
+ if e.errno == errno.ENOENT:
+ # not existing is ok
+ pass
+ else:
+ # I trust the exception has the path.
+ die("Unable to read config file: %s." % e)
+ else:
+ try:
+ cfg.readfp(conffile)
+ finally:
+ conffile.close()
+
+
+ log.info('Reading SSH public key...')
+ pubkey = read_ssh_pubkey()
+ user = ssh_extract_user(pubkey)
+ if user is None:
+ die('Cannot parse user from SSH public key.')
+ log.info('Admin user is %r', user)
+ log.info('Creating repository structure...')
+ repositories = util.getRepositoryDir(cfg)
+ util.mkdir(repositories)
+ admin_repository = os.path.join(repositories, 'gitosis-admin.git')
+ init_admin_repository(
+ git_dir=admin_repository,
+ pubkey=pubkey,
+ user=user,
+ )
+ log.info('Running post-update hook...')
+ run_post_update(git_dir=admin_repository)
+ log.info('Symlinking ~/.gitosis.conf to repository...')
+ symlink_config(git_dir=admin_repository)
+ log.info('Done.')
diff --git a/gitosis/repository.py b/gitosis/repository.py
index 764c980..9558494 100644
--- a/gitosis/repository.py
+++ b/gitosis/repository.py
@@ -1,4 +1,5 @@
import os
+import re
import subprocess
from gitosis import util
@@ -119,3 +120,28 @@ def export(git_dir, path):
)
if returncode != 0:
raise GitCheckoutIndexError('exit status %d' % returncode)
+
+class GitHasInitialCommitError(GitError):
+ """Check for initial commit failed"""
+
+class GitRevParseError(GitError):
+ """rev-parse failed"""
+
+def has_initial_commit(git_dir):
+ child = subprocess.Popen(
+ args=['git', 'rev-parse', 'HEAD'],
+ cwd=git_dir,
+ stdout=subprocess.PIPE,
+ close_fds=True,
+ env=dict(GIT_DIR='.'),
+ )
+ got = child.stdout.read()
+ returncode = child.wait()
+ if returncode != 0:
+ raise GitRevParseError('exit status %d' % returncode)
+ if got == 'HEAD\n':
+ return False
+ elif re.match('^[0-9a-f]{40}\n$', got):
+ return True
+ else:
+ raise GitHasInitialCommitError('Unknown git HEAD: %r' % got)
diff --git a/gitosis/templates/__init__.py b/gitosis/templates/__init__.py
new file mode 100644
index 0000000..b697fb8
--- /dev/null
+++ b/gitosis/templates/__init__.py
@@ -0,0 +1,3 @@
+"""
+Git templates for use by gitosis-init.
+"""
diff --git a/gitosis/templates/admin/hooks/post-update b/gitosis/templates/admin/hooks/post-update
new file mode 100755
index 0000000..704ecb2
--- /dev/null
+++ b/gitosis/templates/admin/hooks/post-update
@@ -0,0 +1,4 @@
+#!/bin/sh
+set -e
+gitosis-run-hook post-update
+git-update-server-info
diff --git a/gitosis/test/test_init.py b/gitosis/test/test_init.py
new file mode 100644
index 0000000..8f72b2d
--- /dev/null
+++ b/gitosis/test/test_init.py
@@ -0,0 +1,83 @@
+from nose.tools import eq_ as eq
+from gitosis.test.util import assert_raises, maketemp
+
+import os
+from ConfigParser import RawConfigParser
+
+from gitosis import init
+from gitosis import repository
+
+from gitosis.test import util
+
+def test_ssh_extract_user_simple():
+ got = init.ssh_extract_user(
+ 'ssh-somealgo '
+ +'0123456789ABCDEFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ +'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ +'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ +'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= fakeuser@fakehost')
+ eq(got, 'fakeuser@fakehost')
+
+def test_ssh_extract_user_bad():
+ e = assert_raises(
+ init.InsecureSSHKeyUsername,
+ init.ssh_extract_user,
+ 'ssh-somealgo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ +'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ +'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ +'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= ER3%#@e%')
+ eq(str(e), "Username contains not allowed characters: 'ER3%#@e%'")
+
+def test_init_admin_repository():
+ tmp = maketemp()
+ admin_repository = os.path.join(tmp, 'admin.git')
+ pubkey = (
+ 'ssh-somealgo '
+ +'0123456789ABCDEFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ +'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ +'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ +'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= fakeuser@fakehost')
+ user = 'jdoe'
+ init.init_admin_repository(
+ git_dir=admin_repository,
+ pubkey=pubkey,
+ user=user,
+ )
+ eq(os.listdir(tmp), ['admin.git'])
+ hook = os.path.join(
+ tmp,
+ 'admin.git',
+ 'hooks',
+ 'post-update',
+ )
+ util.check_mode(hook, 0755, is_file=True)
+ got = util.readFile(hook).splitlines()
+ assert 'gitosis-run-hook post-update' in got
+ export_dir = os.path.join(tmp, 'export')
+ repository.export(git_dir=admin_repository,
+ path=export_dir)
+ eq(sorted(os.listdir(export_dir)),
+ sorted(['gitosis.conf', 'keydir']))
+ eq(os.listdir(os.path.join(export_dir, 'keydir')),
+ ['jdoe.pub'])
+ got = util.readFile(
+ os.path.join(export_dir, 'keydir', 'jdoe.pub'))
+ eq(got, pubkey)
+ # the only thing guaranteed of initial config file ordering is
+ # that [gitosis] is first
+ got = util.readFile(os.path.join(export_dir, 'gitosis.conf'))
+ got = got.splitlines()[0]
+ eq(got, '[gitosis]')
+ cfg = RawConfigParser()
+ cfg.read(os.path.join(export_dir, 'gitosis.conf'))
+ eq(sorted(cfg.sections()),
+ sorted([
+ 'gitosis',
+ 'group gitosis-admin',
+ ]))
+ eq(cfg.items('gitosis'), [])
+ eq(sorted(cfg.items('group gitosis-admin')),
+ sorted([
+ ('writable', 'gitosis-admin'),
+ ('members', 'jdoe'),
+ ]))
diff --git a/gitosis/test/test_repository.py b/gitosis/test/test_repository.py
index 79d1561..9d22d0a 100644
--- a/gitosis/test/test_repository.py
+++ b/gitosis/test/test_repository.py
@@ -6,6 +6,7 @@ import subprocess
from gitosis import repository
from gitosis.test.util import mkdir, maketemp, readFile, check_mode
+from gitosis.test.util import assert_raises
def check_bare(path):
# we want it to be a bare repository
@@ -102,3 +103,30 @@ Frobitz the quux and eschew obfuscation.
eq(got[5], '')
eq(got[6], 'Frobitz the quux and eschew obfuscation.')
eq(got[7:], [])
+
+def test_has_initial_commit_fail_notAGitDir():
+ tmp = maketemp()
+ e = assert_raises(
+ repository.GitRevParseError,
+ repository.has_initial_commit,
+ git_dir=tmp)
+ eq(str(e), 'rev-parse failed: exit status 128')
+
+def test_has_initial_commit_no():
+ tmp = maketemp()
+ repository.init(path=tmp)
+ got = repository.has_initial_commit(git_dir=tmp)
+ eq(got, False)
+
+def test_has_initial_commit_yes():
+ tmp = maketemp()
+ repository.init(path=tmp)
+ repository.fast_import(
+ git_dir=tmp,
+ commit_msg='fakecommit',
+ committer='John Doe <jdoe@example.com>',
+ files=[],
+ )
+ got = repository.has_initial_commit(git_dir=tmp)
+ eq(got, True)
+
diff --git a/gitosis/test/util.py b/gitosis/test/util.py
index aa5a4a2..592b766 100644
--- a/gitosis/test/util.py
+++ b/gitosis/test/util.py
@@ -52,6 +52,19 @@ def readFile(path):
f.close()
return data
+def assert_raises(excClass, callableObj, *args, **kwargs):
+ """
+ Like unittest.TestCase.assertRaises, but returns the exception.
+ """
+ try:
+ callableObj(*args, **kwargs)
+ except excClass, e:
+ return e
+ else:
+ if hasattr(excClass,'__name__'): excName = excClass.__name__
+ else: excName = str(excClass)
+ raise AssertionError("%s not raised" % excName)
+
def check_mode(path, mode, is_file=None, is_dir=None):
st = os.stat(path)
if is_dir:
diff --git a/setup.py b/setup.py
index 697d952..3b2dd97 100755
--- a/setup.py
+++ b/setup.py
@@ -18,6 +18,7 @@ setup(
'gitosis-serve = gitosis.serve:main',
'gitosis-gitweb = gitosis.gitweb:main',
'gitosis-run-hook = gitosis.run_hook:main',
+ 'gitosis-init = gitosis.init:main',
],
},
)