stagit

static git page generator  https://git.ce9e.org
git clone https://git.ce9e.org/stagit.git

commit
4c226c58ec59049b0b6ae7e4d030b1d8bef8fd16
parent
e05fedbb32c049ebefea6c9e8b47da20144ad21d
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2020-04-23 06:50
rm RW+ distinction

Diffstat

M Makefile 1 -
M README.md 30 +++++++++++-------------------
M gitolite/shell.py 8 ++++----
M gitolite/stagit.py 83 ++++++++++++++++++++++++++-----------------------------------
D gitolite/update.py 38 --------------------------------------

5 files changed, 50 insertions, 110 deletions


diff --git a/Makefile b/Makefile

@@ -21,7 +21,6 @@ install: stagit
   21    21 	install -D -m 644 README.md "${DESTDIR}/usr/share/doc/stagit/README.md"
   22    22 
   23    23 	install -D -m 755 gitolite/shell.py "${DESTDIR}/usr/lib/stagit/shell"
   24    -1 	install -D -m 755 gitolite/update.py "${DESTDIR}/usr/lib/stagit/hooks/update"
   25    24 	install -D -m 755 gitolite/post-update.py "${DESTDIR}/usr/lib/stagit/hooks/post-update"
   26    25 	install -D -m 644 gitolite/stagit.py "${DESTDIR}/usr/lib/python3/dist-packages/stagit.py"
   27    26 

diff --git a/README.md b/README.md

@@ -5,13 +5,10 @@ combined with access control scripts inspired by
    5     5 [gitolite](https://gitolite.com/gitolite/). Together they are a simple yet
    6     6 powerful solution for hosting git repositories.
    7     7 
    8    -1 -	All (git-)users use the same (ssh-)user.
    9    -1 -	Access to repos is controlled using two scripts:
   10    -1 	-	one is called via the `command` option in `~/.ssh/authorized_keys`
   11    -1 	-	the second one is called via the `update` git hook
   12    -1 -	A third script is called via the `post-update` git hook. If a public
   13    -1 	repository was created/updated, a corresponding static website is
   14    -1 	created/updated automatically.
   -1     8 -	SSH Access to repos is controlled using the `command` option in
   -1     9 	`~/.ssh/authorized_keys`. All (git-)users use the same (ssh-)user.
   -1    10 -	The `post-update` git hook is used to automatically create/update a static
   -1    11 	websites for public repositories.
   15    12 
   16    13 ## Installation and setup
   17    14 
@@ -31,30 +28,25 @@ Then setup access control:
   31    28 
   32    29 ## stagit.conf
   33    30 
   34    -1 The conf file is inspired by the [corresponding file in
   35    -1 gitolite](https://gitolite.com/gitolite/conf.html), but has a more INI-like
   36    -1 structure.
   37    -1 
   38    31 ```
   39    32 [GROUPS]
   40    33 @staff = dilbert alice
   41    34 
   42    35 [private]
   43    -1 !RW+ = admin
   -1    36 ssh  = admin
   44    37 
   45    38 [example]
   46    39 desc = my shiny new project
   47    -1 !RW+ = @staff
   48    -1 !R   = @all @public
   -1    40 ssh  = @staff
   -1    41 http = yes
   49    42 ```
   50    43 
   51    44 Every section defines one repo, except for the special `GROUPS` section which
   52    -1 can be used to define groups of users. Groups are prefixed with a `@`. Access
   53    -1 permissions are prefixed with a `!`. `RW+` controls read, write, and full
   54    -1 access. Full access includes force-pushing and creating tags.
   -1    45 can be used to define groups of users. The special group `@all` matches all
   -1    46 users.
   55    47 
   56    -1 There are two special groups: `@all` matches all users. `@public` enables
   57    -1 anonymous access via website and
   -1    48 The `ssh` key controls which users can access the repositories via ssh. The
   -1    49 `http` key is boolean and enables anonymous access via website and
   58    50 [git-daemon](https://git-scm.com/book/en/v2/Git-on-the-Server-Git-Daemon).
   59    51 
   60    52 ## Authorized keys

diff --git a/gitolite/shell.py b/gitolite/shell.py

@@ -29,14 +29,14 @@ if __name__ == '__main__':
   29    29 	user = sys.argv[1]
   30    30 
   31    31 	if 'SSH_ORIGINAL_COMMAND' not in os.environ:
   32    -1 		for repo in stagit.config.iter_user_repos(user):
   33    -1 			print(repo)
   -1    32 		for repo in stagit.config.iter_repos():
   -1    33 			if stagit.config.can_ssh(repo, user):
   -1    34 				print(repo)
   34    35 		sys.exit(0)
   35    36 
   36    37 	cmd, repo = parse_soc()
   37    38 
   38    -1 	access = 'w' if cmd == 'git-receive-pack' else 'r'
   39    -1 	if not stagit.config.has_perm(repo, user, access):
   -1    39 	if not stagit.config.can_ssh(repo, user):
   40    40 		sys.stderr.write('access denied\n')
   41    41 		sys.exit(1)
   42    42 

diff --git a/gitolite/stagit.py b/gitolite/stagit.py

@@ -11,7 +11,6 @@ WWW_DIR = '/var/www/git/'
   11    11 REPO_DIR = os.path.expanduser('~/repos/')
   12    12 CONF_PATH = os.path.expanduser('~/stagit.conf')
   13    13 
   14    -1 UPDATE_HOOK = '/usr/lib/stagit/hooks/update'
   15    14 POST_UPDATE_HOOK = '/usr/lib/stagit/hooks/post-update'
   16    15 
   17    16 
@@ -32,49 +31,46 @@ class Config:
   32    31 	def get(self, section, key):
   33    32 		return self.config.get(section, key, fallback='')
   34    33 
   35    -1 	def _iter_sets(self, repo, prefix):
   -1    34 	def getboolean(self, section, key):
   -1    35 		return self.config.getboolean(section, key, fallback=False)
   -1    36 
   -1    37 	def can_ssh(self, repo, user):
   -1    38 		users = set([user, '@all'])
   36    39 		try:
   37    -1 			for key, value in self.config[repo].items():
   38    -1 				if key[0] == prefix:
   39    -1 					yield key, set(value.split())
   -1    40 			for group, value in self.config['GROUPS'].items():
   -1    41 				if user in value.split():
   -1    42 					users.add(group)
   40    43 		except KeyError:
   41    44 			pass
   42    -1 
   43    -1 	def has_perm(self, repo, user, access):
   44    -1 		users = set([user, '@all'])
   45    -1 		for group, values in self._iter_sets('GROUPS', '@'):
   46    -1 			if user in values:
   47    -1 				users.add(group)
   48    -1 		for perm, values in self._iter_sets(repo, '!'):
   49    -1 			if access in perm and not users.isdisjoint(values):
   50    -1 				return True
   51    -1 		return False
   52    -1 
   53    -1 	def is_public(self, repo):
   54    -1 		return self.has_perm(repo, '@public', 'r')
   -1    45 		allowed = self.get(repo, 'ssh').split()
   -1    46 		return not users.isdisjoint(allowed)
   55    47 
   56    48 	def iter_repos(self):
   57    49 		for key in self.config:
   58    -1 			if key[0] != '@' and key not in ['GROUPS', 'DEFAULT']:
   -1    50 			if key not in ['GROUPS', 'DEFAULT']:
   59    51 				yield key
   60    52 
   61    -1 	def iter_user_repos(self, user):
   62    -1 		for repo in self.iter_repos():
   63    -1 			if self.has_perm(repo, user, 'r'):
   64    -1 				yield repo
   65    -1 
   66    53 
   67    54 config = Config()
   68    55 
   69    56 
   70    -1 def update_metadata(repo):
   -1    57 def update_repo(repo):
   71    58 	repodir = get_repo_dir(repo)
   -1    59 	if not os.path.exists(repodir):
   -1    60 		os.makedirs(repodir, exist_ok=True)
   -1    61 		subprocess.check_call(['git', 'init', '--bare'], cwd=repodir)
   -1    62 
   72    63 	export_ok = os.path.join(repodir, 'git-daemon-export-ok')
   73    -1 	if config.is_public(repo):
   -1    64 	post_update = os.path.join(repodir, 'hooks', 'post-update')
   -1    65 	if config.getboolean(repo, 'http'):
   -1    66 		shutil.copy(POST_UPDATE_HOOK, post_update)
   74    67 		with open(export_ok, 'a'):
   75    68 			os.utime(export_ok, None)
   76    -1 	elif os.path.exists(export_ok):
   77    -1 		os.unlink(export_ok)
   -1    69 	else:
   -1    70 		if os.path.exists(export_ok):
   -1    71 			os.unlink(export_ok)
   -1    72 		if os.path.exists(post_update):
   -1    73 			os.unlink(post_update)
   78    74 
   79    75 	with open(os.path.join(repodir, 'description'), 'w') as fh:
   80    76 		fh.write(config.get(repo, 'desc'))
@@ -82,16 +78,6 @@ def update_metadata(repo):
   82    78 		fh.write(config.get(repo, 'url'))
   83    79 
   84    80 
   85    -1 def update_repo(repo):
   86    -1 	path = get_repo_dir(repo)
   87    -1 	if not os.path.exists(path):
   88    -1 		os.makedirs(path, exist_ok=True)
   89    -1 		subprocess.check_call(['git', 'init', '--bare'], cwd=path)
   90    -1 	shutil.copy(UPDATE_HOOK, os.path.join(path, 'hooks', 'update'))
   91    -1 	shutil.copy(POST_UPDATE_HOOK, os.path.join(path, 'hooks', 'post-update'))
   92    -1 	update_metadata(repo)
   93    -1 
   94    -1 
   95    81 def render_index():
   96    82 	fh = open(os.path.join(WWW_DIR, 'index.html'), 'w')
   97    83 	fh.write('<!DOCTYPE html>\n')
@@ -107,20 +93,20 @@ def render_index():
  107    93 	fh.write('<table>\n<thead>\n')
  108    94 	fh.write('<tr><th>Name</th><th>Description</th></tr>\n')
  109    95 	fh.write('</thead>\n<tbody>\n')
  110    -1 	for repo in config.iter_user_repos('@public'):
  111    -1 		fh.write('<tr><td><a href="%s/">%s</a></td><td>%s</td></tr>\n' % (
  112    -1 			escape(repo), escape(repo), escape(config.get(repo, 'desc'))
  113    -1 		))
   -1    96 	for repo in config.iter_repos():
   -1    97 		if config.getboolean(repo, 'http'):
   -1    98 			fh.write('<tr><td><a href="%s/">%s</a></td><td>%s</td></tr>\n' % (
   -1    99 				escape(repo), escape(repo), escape(config.get(repo, 'desc'))
   -1   100 			))
  114   101 	fh.write('</tbody>\n</table>\n')
  115   102 	fh.write('</main>\n</body>\n</html>\n')
  116   103 	fh.close()
  117   104 
  118   105 
  119   106 def render_repo(repo):
  120    -1 	if config.is_public(repo):
  121    -1 		target_dir = os.path.join(WWW_DIR, repo)
  122    -1 		os.makedirs(target_dir, exist_ok=True)
  123    -1 		subprocess.check_call(['stagit', get_repo_dir(repo)], cwd=target_dir)
   -1   107 	target_dir = os.path.join(WWW_DIR, repo)
   -1   108 	os.makedirs(target_dir, exist_ok=True)
   -1   109 	subprocess.check_call(['stagit', get_repo_dir(repo)], cwd=target_dir)
  124   110 
  125   111 
  126   112 def render_all():
@@ -128,8 +114,9 @@ def render_all():
  128   114 		path = os.path.join(WWW_DIR, repo)
  129   115 		if os.path.isdir(path):
  130   116 			shutil.rmtree(path)
  131    -1 	for repo in config.iter_user_repos('@public'):
  132    -1 		render_repo(repo)
   -1   117 	for repo in config.iter_repos():
   -1   118 		if config.getboolean(repo, 'http'):
   -1   119 			render_repo(repo)
  133   120 	render_index()
  134   121 
  135   122 

diff --git a/gitolite/update.py b/gitolite/update.py

@@ -1,38 +0,0 @@
    1    -1 #!/usr/bin/env python3
    2    -1 # see man githooks
    3    -1 
    4    -1 import os
    5    -1 import sys
    6    -1 import subprocess
    7    -1 
    8    -1 import stagit
    9    -1 
   10    -1 
   11    -1 def get_access(repo, ref, oldrev, newrev):
   12    -1 	# detect tags
   13    -1 	if ref.startswith('refs/tags/'):
   14    -1 		return '+'
   15    -1 
   16    -1 	# detect create/delete
   17    -1 	if '0' * 40 in [oldrev, newrev]:
   18    -1 		return '+'
   19    -1 
   20    -1 	# detect force push
   21    -1 	mergebase = subprocess.check_output(
   22    -1 		['git', 'merge-base', oldrev, newrev], cwd=stagit.get_repo_dir(repo),
   23    -1 	)
   24    -1 	if oldrev != mergebase.decode('ascii').rstrip():
   25    -1 		return '+'
   26    -1 
   27    -1 	return 'w'
   28    -1 
   29    -1 
   30    -1 if __name__ == '__main__':
   31    -1 	user = os.environ['STAGIT_USER']
   32    -1 	repo = os.environ['STAGIT_REPO']
   33    -1 	ref, oldrev, newrev = sys.argv[1:]
   34    -1 
   35    -1 	access = get_access(repo, ref, oldrev, newrev)
   36    -1 	if not stagit.config.has_perm(repo, user, access):
   37    -1 		sys.stderr.write('access denied\n')
   38    -1 		sys.exit(1)