- commit
- f60aee3a165ae1cf788e09218f18275a97e8d033
- parent
- 90dd920e85e8208caef5fb79439fa3b6d1881d66
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2020-03-28 17:12
add gitolite functionality
Diffstat
A | gitolite/post-update.py | 14 | ++++++++++++++ |
A | gitolite/shell.py | 51 | +++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | gitolite/stagit.py | 185 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | gitolite/update.py | 38 | ++++++++++++++++++++++++++++++++++++++ |
4 files changed, 288 insertions, 0 deletions
diff --git a/gitolite/post-update.py b/gitolite/post-update.py
@@ -0,0 +1,14 @@ -1 1 #!/usr/bin/env python3 -1 2 # see man githooks -1 3 -1 4 import os -1 5 -1 6 import stagit -1 7 -1 8 -1 9 repo = os.environ['STAGIT_REPO'] -1 10 -1 11 if repo == stagit.ADMIN_REPO: -1 12 stagit.update_admin() -1 13 else: -1 14 stagit.render_html(repo)
diff --git a/gitolite/shell.py b/gitolite/shell.py
@@ -0,0 +1,51 @@ -1 1 #!/usr/bin/env python3 -1 2 # see man git-shell -1 3 -1 4 import os -1 5 import sys -1 6 import re -1 7 import subprocess -1 8 -1 9 import stagit -1 10 -1 11 SOC_RE = ( -1 12 r"^(git-receive-pack|git-upload-pack|git-upload-archive) " -1 13 r"'?/?([a-z0-9_:-]+)(\.git)?/?'?$" -1 14 ) -1 15 -1 16 -1 17 def parse_soc(): -1 18 cmd, repo = os.environ['SSH_ORIGINAL_COMMAND'].split() -1 19 -1 20 soc = re.match(SOC_RE, os.environ['SSH_ORIGINAL_COMMAND']) -1 21 if not soc: -1 22 sys.stderr.write('invalid command\n') -1 23 sys.exit(1) -1 24 cmd, repo, _ = soc.groups() -1 25 return cmd, repo -1 26 -1 27 -1 28 if __name__ == '__main__': -1 29 user = sys.argv[1] -1 30 -1 31 if 'SSH_ORIGINAL_COMMAND' not in os.environ: -1 32 for repo in stagit.config.iter_user_repos(user): -1 33 print(repo) -1 34 sys.exit(0) -1 35 -1 36 cmd, repo = parse_soc() -1 37 -1 38 access = 'w' if cmd == 'git-receive-pack' else 'r' -1 39 if not stagit.config.has_perm(repo, user, access, None): -1 40 sys.stderr.write('access denied\n') -1 41 sys.exit(1) -1 42 -1 43 sys.exit(subprocess.call( -1 44 [*cmd.split('-', 1), repo], -1 45 cwd=stagit.REPO_DIR, -1 46 env={ -1 47 **os.environ, -1 48 'STAGIT_USER': user, -1 49 'STAGIT_REPO': repo, -1 50 }, -1 51 ))
diff --git a/gitolite/stagit.py b/gitolite/stagit.py
@@ -0,0 +1,185 @@ -1 1 import os -1 2 import sys -1 3 import shutil -1 4 import subprocess -1 5 import configparser -1 6 -1 7 USER = 'git' -1 8 ADMIN_REPO = 'stagit-admin' -1 9 -1 10 WWW_DIR = '/var/www/git/' -1 11 REPO_DIR = os.path.expanduser('~/repos/') -1 12 ADMIN_DIR = os.path.expanduser('~/.stagit/') -1 13 -1 14 SHELL = '/usr/lib/stagit/shell' -1 15 UPDATE_HOOK = '/usr/lib/stagit/hooks/update' -1 16 POST_UPDATE_HOOK = '/usr/lib/stagit/hooks/post-update' -1 17 CONF = '/usr/lib/stagit/conf.skel' -1 18 -1 19 KEY_OPTS = 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty' -1 20 KEY_TPL = 'command="%s %%s",%s %%s' % (SHELL, KEY_OPTS) -1 21 -1 22 -1 23 if os.environ.get('USER') != USER: -1 24 sys.stderr.write('wrong user\n') -1 25 sys.exit(1) -1 26 -1 27 -1 28 def get_repo_dir(repo): -1 29 return os.path.join(REPO_DIR, repo + '.git') -1 30 -1 31 -1 32 class Config: -1 33 def __init__(self): -1 34 self._cache = None -1 35 -1 36 @property -1 37 def config(self): -1 38 if not self._cache: -1 39 self._cache = configparser.ConfigParser() -1 40 self._cache.read(os.path.join(ADMIN_DIR, 'conf')) -1 41 return self._cache -1 42 -1 43 def invalidate(self): -1 44 self._cache = None -1 45 -1 46 def _iter_sets(self, repo, prefix): -1 47 try: -1 48 for key, value in self.config[repo].items(): -1 49 if key[0] == prefix: -1 50 yield key, set(value.split()) -1 51 except KeyError: -1 52 pass -1 53 -1 54 def has_perm(self, repo, user, access, ref): -1 55 users = set([user, '@all']) -1 56 repos = set([repo, '@all']) -1 57 for key, values in self._iter_sets('GROUPS', '@'): -1 58 if user in values: -1 59 users.add(key) -1 60 if repo in values: -1 61 repos.add(key) -1 62 -1 63 ret = False -1 64 for r in repos: -1 65 for key, values in self._iter_sets(r, '!'): -1 66 if users.isdisjoint(values): -1 67 continue -1 68 refex, perm = key.rsplit('!', 1) -1 69 if ref and refex and refex[1:] != ref: -1 70 continue -1 71 if perm == '-': -1 72 return False -1 73 elif access in perm: -1 74 ret = True -1 75 return ret -1 76 -1 77 def is_public(self, repo): -1 78 return self.has_perm(repo, '@public', 'r', None) -1 79 -1 80 def iter_repos(self): -1 81 for key in self.config: -1 82 if key[0] != '@' and key not in ['GROUPS', 'DEFAULT']: -1 83 yield key -1 84 -1 85 def iter_user_repos(self, user): -1 86 for repo in self.iter_repos(): -1 87 if self.has_perm(repo, user, 'r', None): -1 88 yield repo -1 89 -1 90 -1 91 config = Config() -1 92 -1 93 -1 94 def update_admin_dir(): -1 95 subprocess.call( -1 96 ['git', 'checkout', '-f', '--quiet'], -1 97 cwd=get_repo_dir(ADMIN_REPO), -1 98 env={ -1 99 **os.environ, -1 100 'GIT_WORK_TREE': ADMIN_DIR, -1 101 }, -1 102 ) -1 103 config.invalidate() -1 104 -1 105 -1 106 def update_keys(): -1 107 keydir = os.path.join(ADMIN_DIR, 'keydir') -1 108 lines = [] -1 109 -1 110 for fn in os.listdir(keydir): -1 111 if fn.endswith('.pub'): -1 112 path = os.path.join(keydir, fn) -1 113 name = fn[:-4] -1 114 with open(path) as fh: -1 115 lines.append(KEY_TPL % (name, fh.read())) -1 116 -1 117 with open(os.path.expanduser('~/.ssh/authorized_keys'), 'w') as fh: -1 118 for line in lines: -1 119 fh.write(line) -1 120 -1 121 -1 122 def update_daemon_export_ok(repo): -1 123 export_ok = os.path.join(get_repo_dir(repo), 'git-daemon-export-ok') -1 124 if config.is_public(repo): -1 125 with open(export_ok, 'a'): -1 126 os.utime(export_ok, None) -1 127 elif os.path.exists(export_ok): -1 128 os.unlink(export_ok) -1 129 -1 130 -1 131 def update_repo(repo): -1 132 path = get_repo_dir(repo) -1 133 if not os.path.exists(path): -1 134 os.makedirs(path, exist_ok=True) -1 135 subprocess.check_call(['git', 'init', '--bare'], cwd=path) -1 136 shutil.copy(UPDATE_HOOK, os.path.join(path, 'hooks', 'update')) -1 137 shutil.copy(POST_UPDATE_HOOK, os.path.join(path, 'hooks', 'post-update')) -1 138 -1 139 -1 140 def update_admin(): -1 141 update_admin_dir() -1 142 update_keys() -1 143 for repo in config.iter_repos(): -1 144 update_repo(repo) -1 145 update_daemon_export_ok(repo) -1 146 -1 147 -1 148 def render_html(repo): -1 149 if config.is_public(repo): -1 150 target_dir = os.path.join(WWW_DIR, repo) -1 151 os.makedirs(target_dir, exist_ok=True) -1 152 subprocess.check_call(['stagit', get_repo_dir(repo)], cwd=target_dir) -1 153 -1 154 -1 155 def bootstrap(keyfile): -1 156 update_repo(ADMIN_REPO) -1 157 repodir = get_repo_dir(ADMIN_REPO) -1 158 -1 159 if not os.path.exists(ADMIN_DIR): -1 160 workdir = os.path.join(REPO_DIR, '.bootstrap') -1 161 keydir = os.path.join(workdir, 'keydir') -1 162 os.mkdir(workdir) -1 163 os.mkdir(keydir) -1 164 shutil.copy(CONF, os.path.join(workdir, 'conf')) -1 165 shutil.copy(keyfile, os.path.join(workdir, 'keydir', 'admin.pub')) -1 166 -1 167 subprocess.call( -1 168 ['git', 'add', 'conf', 'keydir/admin.pub'], -1 169 cwd=repodir, env={**os.environ, 'GIT_WORK_TREE': workdir}, -1 170 ) -1 171 subprocess.call( -1 172 ['git', 'commit', '-m', 'bootstrap'], -1 173 cwd=repodir, env={**os.environ, 'GIT_WORK_TREE': workdir}, -1 174 ) -1 175 -1 176 os.unlink(os.path.join(workdir, 'conf')) -1 177 os.unlink(os.path.join(workdir, 'keydir', 'admin.pub')) -1 178 os.rmdir(os.path.join(workdir, 'keydir')) -1 179 os.rmdir(os.path.join(workdir)) -1 180 -1 181 update_admin() -1 182 -1 183 -1 184 if __name__ == '__main__': -1 185 bootstrap(sys.argv[1])
diff --git a/gitolite/update.py b/gitolite/update.py
@@ -0,0 +1,38 @@ -1 1 #!/usr/bin/env python3 -1 2 # see man githooks -1 3 -1 4 import os -1 5 import sys -1 6 import subprocess -1 7 -1 8 import stagit -1 9 -1 10 -1 11 def get_access(repo, ref, oldrev, newrev): -1 12 # detect tags -1 13 if ref.startswith('refs/tags/'): -1 14 return '+' -1 15 -1 16 # detect create/delete -1 17 if '0' * 40 in [oldrev, newrev]: -1 18 return '+' -1 19 -1 20 # detect force push -1 21 mergebase = subprocess.check_output( -1 22 ['git', 'merge-base', oldrev, newrev], cwd=stagit.get_repo_dir(repo), -1 23 ) -1 24 if oldrev != mergebase.decode('ascii').rstrip(): -1 25 return '+' -1 26 -1 27 return 'w' -1 28 -1 29 -1 30 if __name__ == '__main__': -1 31 user = os.environ['STAGIT_USER'] -1 32 repo = os.environ['STAGIT_REPO'] -1 33 ref, oldrev, newrev = sys.argv[1:] -1 34 -1 35 access = get_access(repo, ref, oldrev, newrev) -1 36 if not stagit.config.has_perm(repo, user, access, ref): -1 37 sys.stderr.write('access denied\n') -1 38 sys.exit(1)