grm (10400B)
1 #!/bin/sh 2 # grm: git repo manager for self-hosted git servers 3 4 # This is a customised version of grm. 5 # 6 # The original version can be obtained from: 7 # https://sink.krj.st/grm 8 # 9 # Copyright 2020 by krasjet. 10 # License: https://sink.krj.st/grm/file/LICENSE 11 12 13 #---------------+----------+-----------------# 14 # | config | # 15 # +----------+ # 16 17 # root directory of git repositories. 18 GRM_REPOS_ROOT="/home/git" 19 20 # default owner. 21 GRM_OWNER="Chris Bracken" 22 23 # default url prefix (without ending slash) 24 GRM_URL_PREFIX="https://git.bracken.jp" 25 26 # Add Mastodon verification URL to index header. 27 ME_URL="https://famichiki.jp/@akande" 28 29 # path of the post-receive hooks for gout. 30 GRM_POSTRECV_HOOK="/home/git/git_infra/post-receive" 31 GRM_POSTRECV_HOOKS_DIR="/home/git/git_infra/post-receive.d" 32 33 # root directory of output gout web pages. 34 GOUT_WEB_ROOT="/var/www/git.bracken.jp" 35 36 # root directory of output gout gemini pages. 37 # 38 # agate serves this as a virtual host, so it must be content/<hostname>/ and 39 # agate must be started with more than one --hostname; see gout_gemini. 40 GOUT_GEMINI_ROOT="/var/gemini/content/git.bracken.jp" 41 42 # # 43 # # 44 #--------------------------------------------# 45 46 # for gout 47 export LC_CTYPE="en_US.UTF-8" 48 49 prog_name="${0##*/}" 50 repos_root="$GRM_REPOS_ROOT" 51 web_root="$GOUT_WEB_ROOT" 52 gemini_root="$GOUT_GEMINI_ROOT" 53 54 # agate reads the gemini pages as a different user in a different jail. 55 umask 022 56 57 recompile_repo() { 58 repo_name="$1" 59 repo_dir="${repos_root}/${repo_name}.git" 60 repo_web_dir="${web_root}/${repo_name}" 61 repo_gmi_dir="${gemini_root}/${repo_name}" 62 # One cache per output format: the cache holds log entries already rendered 63 # in that format, so html and gemini must not share one. 64 cachefile="${repo_dir}/.htmlcache" 65 gmicachefile="${repo_dir}/.gmicache" 66 67 [ -d "$repo_dir" ] || { echo "[$1] repo not found"; return 1; } 68 69 if [ -e "$repo_dir/git-daemon-export-ok" ]; then 70 echo "[$1] recompiling gout html pages..." 71 mkdir -p "$repo_web_dir" 72 cd "${repo_web_dir:?}" && \ 73 rm -f "$cachefile" && \ 74 rm -rf "commit" "file" && \ 75 gout -H -c "$cachefile" "$repo_dir" && \ 76 ln -sf log.html index.html && \ 77 ln -sf ../logo.png logo.png && \ 78 ln -sf ../style.css style.css && \ 79 ln -sf ../favicon.png favicon.png || return 1 80 81 echo "[$1] recompiling gout gemini pages..." 82 mkdir -p "$repo_gmi_dir" 83 cd "${repo_gmi_dir:?}" && \ 84 rm -f "$gmicachefile" && \ 85 rm -rf "commit" "file" && \ 86 gout -M -c "$gmicachefile" "$repo_dir" && \ 87 ln -sf log.gmi index.gmi || return 1 88 89 echo "[$1] done!" 90 else 91 echo "[$1] Not recompiling gout pages: private repo" 92 fi 93 } 94 95 # Lists the repos that belong in the index, one shell-quoted path per line. 96 # 97 # 1. find all directories in $repos_root ending with .git 98 # 2. filter all the public repos (with git-daemon-export-ok) 99 # 3. exclude any repo marked with gout-no-index 100 # 4. sort the result 101 # 5. hack for posix compatibility 102 indexable_repos() { 103 find "${repos_root}/." ! -name . -prune \ 104 -type d -name "*.git" \ 105 -exec test -e "{}/git-daemon-export-ok" \;\ 106 -exec test ! -e "{}/gout-no-index" \; \ 107 -print \ 108 | sort -f \ 109 | sed -e 's/"/"\\""/g' -e 's/.*/"&"/' 110 } 111 112 rebuild_index() { 113 echo "[index] rebuilding html index..." 114 mkdir -p "${web_root}" || return 1; 115 indexable_repos \ 116 | xargs gout_index -H -m "${ME_URL}" \ 117 > "${web_root}/index.html" || return 1 118 119 # gout_index -M emits relative links (=> repo/log.gmi) and takes no -m: the 120 # Mastodon verification link is an HTML-only concept. 121 echo "[index] rebuilding gemini index..." 122 mkdir -p "${gemini_root}" || return 1; 123 indexable_repos \ 124 | xargs gout_index -M \ 125 > "${gemini_root}/index.gmi" || return 1 126 127 echo "[index] done!" 128 } 129 130 RED="\033[91m" 131 GREEN="\033[92m" 132 YELLOW="\033[93m" 133 BLUE="\033[94m" 134 RESET="\033[0m" 135 136 grm_new() { 137 set -e 138 default_owner=${GRM_OWNER:-$(logname)} 139 url_prefix=${GRM_URL_PREFIX:-git://$(hostname -f)} 140 default_desc="a work in progress" 141 142 printf "%b\n> " "${BLUE}repo name (without trailing .git)${RESET}" 143 read -r repo_name 144 [ -z "$repo_name" ] && \ 145 { echo "no repo name given, exiting..."; exit 1; } 146 147 # now we have the complete path of the repo 148 repo_path="$repos_root/${repo_name}.git" 149 [ -e "$repo_path" ] && \ 150 { echo "repository already exists"; exit 1; } 151 152 printf "%b\n> " "${YELLOW}visibility: 153 1) public\n 2) private\n 3) unlisted (hide from index) 154 ${BLUE}enter index [default: ${GREEN}1${BLUE}]${RESET}" 155 read -r visibility 156 157 case $visibility in 158 1|public) exported=1 ;; 159 2|private) hidden=1 ;; 160 3|unlisted) exported=1; hidden=1 ;; 161 *) printf "%b\n" "${YELLOW}visibility defaults to ${GREEN}public${RESET}" 162 exported=1 ;; 163 esac 164 165 printf "%b%s%b\n> " "${BLUE}description [${GREEN}" "$default_desc" "${BLUE}]${RESET}" 166 read -r repo_desc 167 repo_desc=${repo_desc:-$default_desc} 168 169 printf "%b%s%b\n> " "${BLUE}owner [${GREEN}" "$default_owner" "${BLUE}]${RESET}" 170 read -r owner 171 owner=${owner:-$default_owner} 172 173 printf "%b%s%b\n> " \ 174 "${BLUE}clone url [${GREEN}" "$url_prefix/$repo_name.git" "${BLUE}]${RESET}" 175 read -r clone_url 176 clone_url=${clone_url:-$url_prefix/$repo_name}.git 177 178 # start creating repo 179 git init --bare "$repo_path" 180 181 echo "writing gout metadata..." 182 printf "%s\n" "$repo_desc" > "$repo_path/description" 183 printf "%s\n" "$owner" > "$repo_path/owner" 184 printf "%s\n" "$clone_url" > "$repo_path/url" 185 186 echo "setting visibility..." 187 [ "$exported" = "1" ] && : >> "$repo_path/git-daemon-export-ok" 188 [ "$hidden" = "1" ] && : >> "$repo_path/gout-no-index" 189 190 # Link the page-generation hooks ONLY. post-receive.d also holds 191 # publish_www and publish_gemini, which check out a work tree over the blog 192 # and the capsule; a new repo linked to those would overwrite them on its 193 # first push. Never replace this with a loop over the directory. 194 echo "installing gout post-receive hooks..." 195 ln -sf "$GRM_POSTRECV_HOOK" "$repo_path/hooks/post-receive" 196 mkdir -p "$repo_path/hooks/post-receive.d" 197 ln -sf "$GRM_POSTRECV_HOOKS_DIR/gout_html" \ 198 "$repo_path/hooks/post-receive.d/gout_html" 199 ln -sf "$GRM_POSTRECV_HOOKS_DIR/gout_gemini" \ 200 "$repo_path/hooks/post-receive.d/gout_gemini" 201 202 echo "done!" 203 } 204 205 grm_remove() { 206 [ $# -gt 0 ] || { echo "no repo name given, exiting..."; exit 1; } 207 208 for repo in "$@" 209 do 210 printf "remove %s? [y/N] " "$repo" 211 read -r resp 212 if echo "$resp" | grep -iq "^y$"; then 213 rm -rf "${repos_root:?}/${repo:?}.git" || continue; 214 rm -rf "${web_root:?}/${repo:?}" || continue; 215 rm -rf "${gemini_root:?}/${repo:?}" || continue; 216 fi 217 done 218 # only rebuild index if gout exists 219 command -v gout_index >/dev/null && rebuild_index & 220 wait 221 } 222 223 grm_list() { 224 case "$1" in 225 public) 226 find "${repos_root}/." ! -name . -prune \ 227 -type d -name "*.git" \ 228 -exec test -e "{}/git-daemon-export-ok" \; \ 229 -exec test ! -e "{}/gout-no-index" \; \ 230 -exec basename {} '.git' \; | sort -f ;; 231 private) 232 find "${repos_root}/." ! -name . -prune \ 233 -type d -name "*.git" \ 234 -exec test ! -e "{}/git-daemon-export-ok" \; \ 235 -exec basename {} '.git' \; | sort -f ;; 236 hidden|unlisted) 237 find "${repos_root}/." ! -name . -prune \ 238 -type d -name "*.git" \ 239 -exec test -e "{}/git-daemon-export-ok" \; \ 240 -exec test -e "{}/gout-no-index" \; \ 241 -exec basename {} '.git' \; | sort -f ;; 242 *) 243 find "${repos_root}/." ! -name . -prune \ 244 -type d -name "*.git" \ 245 -exec basename {} '.git' \; | sort -f ;; 246 esac 247 } 248 249 grm_recompile() { 250 for repo_name in "$@" 251 do 252 recompile_repo "$repo_name" & 253 done 254 rebuild_index & 255 wait 256 echo "recompilation done!" 257 } 258 259 grm_recompileall() { 260 grm_list \ 261 | sed -e 's/"/"\\""/g' -e 's/.*/"&"/' \ 262 | xargs "$0" rc 263 } 264 265 grm_info() { 266 [ -z "$1" ] && { echo "no repo name given, exiting..."; exit 1; } 267 268 repo_name=$1 269 repo_dir="${repos_root}/${repo_name}.git" 270 271 [ -d "$repo_dir" ] || { echo "can't find repo named $repo_name"; exit 1; } 272 echo "name: $repo_name" 273 printf "visibility: " 274 275 if [ -e "${repo_dir}/git-daemon-export-ok" ]; then 276 if [ -e "${repo_dir}/gout-no-index" ]; then 277 printf "%b\n" "${YELLOW}unlisted${RESET}" 278 else 279 printf "%b\n" "${GREEN}public${RESET}" 280 fi 281 else 282 printf "%b\n" "${RED}private${RESET}" 283 fi 284 285 [ -f "${repo_dir}/description" ] && \ 286 echo "description: $(cat "${repo_dir}/description")" 287 288 [ -f "${repo_dir}/owner" ] && \ 289 echo "owner: $(cat "${repo_dir}/owner")" 290 291 [ -f "${repo_dir}/url" ] && \ 292 echo "url: $(cat "${repo_dir}/url")" 293 } 294 295 show_help() { 296 cat << EOF 297 usage: $prog_name <command> [<args>] 298 299 Git repo manager, manage git repositories on self-hosted git servers. 300 301 If you have created a 'git' user for managing git repositories, this 302 script should be run as: 303 $ doas -u git -- $prog_name <command> [<args>] 304 or 305 $ sudo -u git -- $prog_name <command> [<args>] 306 307 commands: 308 new create a new repo 309 info repo_name display metadata of the repo 310 ls list all repos 311 ls public list public repos 312 ls private list private repos 313 ls unlisted list unlisted (hidden) repos 314 rm repo1 [repo2..] remove repos 315 rc recompile gout index (html and gemini) 316 rc repo1 [repo2..] recompile gout pages for repos, 317 and recompile index 318 rca recompile all public repos 319 help show help 320 EOF 321 } 322 323 # parse subcommands 324 case "$1" in 325 new) cmd=new;; 326 ls|list) cmd=list;; 327 rm|remove) cmd=remove;; 328 rc|recompile) cmd=recompile;; 329 rca|recompileall) cmd=recompileall;; 330 info) cmd=info;; 331 *) { show_help; exit; };; 332 esac 333 334 shift 335 grm_"$cmd" "$@"