#!/usr/bin/env bash
# symgit -- manage self-hosted Git repositories

. lib.bash || exit

host=star
fsroot=/srv/git
urlprefix=":"
webroot=https://git.symlink.lt/cgit

argtorepo() {
	local arg=$1
	if [[ $arg == . ]]; then
		arg=$(git config remote.origin.url)
	fi
	if [[ $arg == *:* && $arg != @(:*|%:*) ]]; then
		vdie "not a symgit repo: $arg"
	fi
	arg=${arg#'%'}
	arg=${arg#$urlprefix}
	case $arg in [!A-Za-z_]*|*/|"")
		vdie "bad repository name: $arg"
	esac
	arg=${arg%.git}
	echo "$arg"
}

repodir() {
	local repo=$1
	case $repo in [!A-Za-z_]*|*.git)
		lib:crash "repo should have been canonicalized: $repo"
	esac
	echo "$fsroot/$repo.git"
}

repourl() {
	local repo=$1
	case $repo in [!A-Za-z_]*|*.git)
		lib:crash "repo should have been canonicalized: $repo"
	esac
	echo "$urlprefix$repo"
}

flushconf() {
	vmsg "flushing cgit config"
	ssh $host "sudo find /var/cache/cgit -name 'rc-*' -delete"
}

flushall() {
	vmsg "flushing cgit cache"
	ssh $host "sudo find /var/cache/cgit -type f -delete"
}

vusage() {
	local argc=$1 min=$2 max=$3 text=$4
	if (( max >= 0 && argc > max )); then
		vdie "excess arguments"
	elif (( argc < min )); then
		vdie "usage: $progname $text" >&2
	fi
}

klist -s || kinit || vdie "no Kerberos credentials"

cmd=$1; shift

set -u
set -e

case ${cmd:-help} in
	help|--help)
		vusage $# 0 0 "$cmd"
		lib_config[opt_width]=28
		echo "Commands:"
		echo_opt "ls|list"			"list repositories"
		echo_opt "vlist"			"verbose listing"
		echo_opt "new <repo> [<desc>]"		"create new repository"
		echo_opt "mv|rename <old> <new>"	"rename repository"
		echo_opt "describe <repo> [<desc>]"	"set public description"
		echo_opt "head <repo> [<branch>]"	"set default branch"
		echo_opt "export|publish <repo>..."	"publish repository"
		echo_opt "unexport <repo>..."		"unpublish repository"
		echo_opt "mirror <repo> <upstream>"	"set up automatic pull mirroring"
		echo_opt "unmirror <repo>"		"disable pull mirroring"
		echo_opt "web <repo>..."		"show Cgit web URL"
		echo_opt "flush"			"flush Cgit caches"
		;;
	ls|list)
		vusage $# 0 0 "$cmd"
		ssh $host "find '$fsroot/' -type d -name '*.git' -printf '%P\n' -prune" \
			| sort \
			| sed "s|\\.git\$||"
		;;
	vlist)
		vusage $# 0 0 "$cmd"
		lib_config[opt_width]=32
		barchars='/-\|' x=0
		ssh $host "find '$fsroot/' -type d -name '*.git' -printf '%P\n' -prune" \
			| sort \
			| while read -r dir; do
				printf 'loading %s (%d repositories loaded)\r' \
					"${barchars:$((x++ % ${#barchars})):1}" $x >&2
				repo=${dir%.git}
				descfile="/net/$host$fsroot/$repo.git/description"
				expfile="/net/$host$fsroot/$repo.git/git-daemon-export-ok"
				if [ -s "$descfile" ]; then
					desc=$(< "$descfile")
				else
					desc="--"
				fi
				if [ -e "$expfile" ]; then
					exp="PUB"
				else
					exp="---"
				fi
				# experimental, `column` can't handle it yet
				#url="$webroot/$repo.git/"
				#repo=$'\e]8;;'$url$'\e\\'$repo$'\e]8;;\e\\'
				printf '%s\t%s\t%s\n' "$exp" "$repo" "$desc"
			done \
			| column -t -s $'\t' -N PUB,NAME,DESCRIPTION -T DESCRIPTION -c $COLUMNS \
			| less
		;;
	new)
		vusage $# 1 2 "$cmd <repo> [<description>]"
		arg=$1 newdesc=${2-}
		repo=$(argtorepo "$arg")
		dir=$(repodir "$repo")
		url=$(repourl "$repo")
		if ssh $host "test -d ${dir@Q}"; then
			vdie "repository already exists: $dir"
		fi
		vmsg "creating repository $dir"
		pdir=${dir%/*}
		ssh $host "umask 022; mkdir -p ${pdir@Q}"
		ssh $host "umask 077; git init --bare ${dir@Q}"
		ssh $host "chmod go-rx ${dir@Q}"
		ssh $host "echo defbranch=main > ${dir@Q}/cgitrc"
		if [[ $newdesc ]]; then
			ssh $host "echo ${newdesc@Q} > ${dir@Q}/description"
		fi
		flushconf
		if git rev-parse --git-dir &> /dev/null; then
			if [[ "$(git remote)" == "" ]]; then
				vmsg "adding repository as git remote"
				git remote add origin "$url"
			fi
		fi
		;;
	mv|move|rename)
		vusage $# 2 2 "$cmd <old> <new>"
		old=$1
		new=$2
		oldrepo=$(argtorepo "$old")
		newrepo=$(argtorepo "$new")
		olddir=$(repodir "$oldrepo")
		newdir=$(repodir "$newrepo")
		oldurl=$(repourl "$oldrepo")
		newurl=$(repourl "$newrepo")
		if ! ssh $host "test -d ${olddir@Q}"; then
			vdie "old repository does not exist: $olddir"
		elif ssh $host "test -d ${newdir@Q}"; then
			vdie "new repository already exists: $newdir"
		else
			vmsg "renaming repository: $oldrepo => $newrepo"
			oldpdir=${olddir%/*}
			newpdir=${newdir%/*}
			ssh $host "umask 022; mkdir -vp ${newpdir@Q}"
			ssh $host "mv -Tvn ${olddir@Q} ${newdir@Q}"
			ssh $host "rmdir -p --ignore-fail-on-non-empty ${oldpdir@Q}"
		fi
		flushconf
		;;
	head)
		vusage $# 1 2 "$cmd <repo> [<head>]"
		arg=$1 newhead=${2-}
		repo=$(argtorepo "$arg")
		dir=$(repodir "$repo")
		if [[ $newhead ]]; then
			newhead=${newhead#refs/heads/}
			newref=refs/heads/$newhead
			vmsg "setting default branch for $repo to \"$newhead\""
			ssh $host "git -C ${dir@Q} symbolic-ref HEAD ${newref@Q}"
			ssh $host "echo defbranch=${newhead@Q} > ${dir@Q}/cgitrc"
			ssh $host "chmod a+r ${dir@Q}/cgitrc"
		else
			ssh $host "git -C ${dir@Q} symbolic-ref HEAD"
			ssh $host "cat ${dir@Q}/cgitrc"
		fi
		;;
	desc|describe)
		vusage $# 1 2 "$cmd <repo> [<description>]"
		if (( $# == 1 )) && [[ $1 == *\ * ]]; then
			arg=. newdesc=$1
		else
			arg=$1 newdesc=${2-}
		fi
		repo=$(argtorepo "$arg")
		dir=$(repodir "$repo")
		if [[ $newdesc ]]; then
			oldtag=
			if ssh $host "test -s ${dir@Q}/description"; then
				olddesc=$(ssh $host "cat ${dir@Q}/description")
				if [[ $olddesc = "Mirror of "* ]]; then
					oldtag=$olddesc
				elif [[ $olddesc = "[Mirror of "*"]"* ]]; then
					oldtag=$(echo "$olddesc" | sed -E 's/^\[(Mirror of [^ ]*)\] .*/\1/')
				fi
			fi
			if [[ $oldtag && $newdesc = "-" ]]; then
				newdesc="$oldtag"
			elif [[ $oldtag ]]; then
				newdesc="[$oldtag] $newdesc"
			elif [[ $newdesc = "-" ]]; then
				newdesc=
			fi
			if [[ $newdesc ]]; then
				vmsg "setting description for $repo to \"$newdesc\""
				ssh $host "echo ${newdesc@Q} > ${dir@Q}/description"
			else
				vmsg "removing description for $repo"
				ssh $host "rm -f ${dir@Q}/description"
			fi
		else
			if ! ssh $host "test -s ${dir@Q}/description"; then
				vmsg "$repo has no description"
			else
				ssh $host "cat ${dir@Q}/description"
			fi
		fi
		;;
	mirror)
		vusage $# 2 2 "$cmd <repo> <upstream>"
		arg=$1 upstream=$2
		repo=$(argtorepo "$arg")
		dir=$(repodir "$repo")
		# Set up parameters for the hourly re-fetch cronjob, which looks for
		# repositories with a remote that have [Mirror] in their description.
		vmsg "adding remote \"origin\" from \"$upstream\""
		ssh $host "git -C ${dir@Q} remote remove origin &>/dev/null || true"
		ssh $host "git -C ${dir@Q} remote add --mirror=fetch origin ${upstream@Q}"
		ssh $host "git -C ${dir@Q} fetch origin"
		if ssh $host "test -s ${dir@Q}/description"; then
			desc=$(ssh $host "cat ${dir@Q}/description")
			desc=$(echo "$desc" | sed 's/^Mirror of [^ ]*$//')
			desc=$(echo "$desc" | sed 's/^\[Mirror of [^ ]*\] //')
			desc="[Mirror of $upstream] $desc"
		else
			desc="Mirror of $upstream"
		fi
		ssh $host "echo ${desc@Q} > ${dir@Q}/description"
		;;
	unmirror)
		vusage $# 1 1 "$cmd <repo>"
		arg=$1
		repo=$(argtorepo "$arg")
		dir=$(repodir "$repo")
		vmsg "removing \"origin\" remote"
		ssh $host "git -C ${dir@Q} remote remove origin &>/dev/null || true"
		if ssh $host "test -s ${dir@Q}/description"; then
			desc=$(ssh $host "cat ${dir@Q}/description")
			desc=$(echo "$desc" | sed 's/^Mirror of [^ ]*$//')
			desc=$(echo "$desc" | sed 's/^\[Mirror of [^ ]*\] //')
		else
			desc=""
		fi
		ssh $host "echo ${desc@Q} > ${dir@Q}/description"
		;;
	pub|publish|exp|export)
		vusage $# 1 -1 "$cmd <repo>..."
		for arg; do
			repo=$(argtorepo "$arg")
			dir=$(repodir "$repo")
			url=$(repourl "$repo")
			# Make world-readable and create the 'export-ok' flag
			# that both Cgit and git-daemon look for.
			if ! ssh $host "test -d ${dir@Q}"; then
				vdie "repository does not exist: $dir"
				continue
			elif ssh $host "test -f ${dir@Q}/git-daemon-export-ok"; then
				vmsg "$repo is already public"
				continue
			fi
			if ! ssh $host "test -s ${dir@Q}/description"; then
				vmsg "$repo has no description; use '$progname desc' to set"
			fi
			vmsg "publishing repository $repo"
			ssh $host "touch ${dir@Q}/git-daemon-export-ok"
			ssh $host "chmod -R go+rX ${dir@Q}"
		done
		;;
	unpub|unpublish|unexp|unexport)
		vusage $# 1 -1 "$cmd <repo>..."
		for arg; do
			repo=$(argtorepo "$arg")
			dir=$(repodir "$repo")
			url=$(repourl "$repo")
			if ! ssh $host "test -d ${dir@Q}"; then
				vdie "repository does not exist: $dir"
				continue
			fi
			vmsg "unpublishing repository $repo"
			ssh $host "rm -f ${dir@Q}/git-daemon-export-ok"
			ssh $host "chmod -R go-rx ${dir@Q}"
		done
		;;
	web)
		vusage $# 1 -1 "$cmd <repo>..."
		for arg; do
			repo=$(argtorepo "$arg")
			url="$webroot/$repo.git/"
			echo "$repo => $url"
		done
		;;
	flush)
		vusage $# 0 0 "$cmd"
		flushall
		;;
	*)
		vdie "bad command: $cmd"
		;;
esac

((!errors))
