gitserver

self-hosted git server tooling
git clone https://git.ryansepassi.com/git/gitserver.git
Log | Files | Refs | README

commit fb6d5d5749b90f49c9bcf7632361b855fc7d5fa6
parent 26f271b922b2189c77fea1e47269dfcbe4b3705c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 26 Aug 2026 11:06:26 -0700

Replace repository setup script and add landing pages

Diffstat:
MREADME.md | 31++++++++++++++-----------------
Dbin/add-repo | 39---------------------------------------
Mbin/bringup | 2+-
Abin/repo-index | 23+++++++++++++++++++++++
Mbin/stagit-update | 2++
Adev/test.sh | 17+++++++++++++++++
Ajustfile | 2++
Mpush | 2+-
8 files changed, 60 insertions(+), 58 deletions(-)

diff --git a/README.md b/README.md @@ -59,11 +59,12 @@ Everything on the server lives under `~/repos/`: ### Add a repo ```sh -ssh $GIT_HOST ~/repos/bin/add-repo <name> [--public] [-d "description"] +ryops repos add [--public] [-d "description"] <name> ``` -Creates `~/repos/<name>.git`, symlinks the `post-receive` hook, writes -`description` and `url`, and kicks off an initial `stagit-update`. The `url` +Connects to `debian-wedding`, creates `~/repos/<name>.git`, symlinks the +`post-receive` hook, writes `description` and `url`, and kicks off an initial +`stagit-update`. The `url` file determines the clone URL shown on the stagit page: - private → `ssh://USER@$GIT_HOSTNAME/~/repos/<name>.git` - public → `https://$PUBLIC_DOMAIN/git/<name>.git` @@ -80,7 +81,9 @@ git push -u wedding main Normal `git push`. The `post-receive` hook runs: 1. `git update-server-info` (for dumb-HTTP clone on public repos) 2. `stagit-update <name>` — regenerates HTML in `www/`, and if the repo has a - `public` marker, also in `www-public/` + mirrors the bare repo into + `public` marker, also in `www-public/`. Each repo gets an `index.html` that + redirects to its rendered README when present and to `log.html` otherwise. + The update also mirrors the bare repo into `www-public/git/<name>.git/` (excluding `hooks/` and the `public` marker). 3. `publish-public` — PUTs everything in `www-public/` to Bunny via HTTPS. @@ -103,20 +106,12 @@ Public requires: the `public` marker file, and (for clean clone URL display) a correct `url` file. ```sh -# make public -ssh $GIT_HOST 'touch ~/repos/<name>.git/public \ - && echo https://git.ryansepassi.com/git/<name>.git > ~/repos/<name>.git/url \ - && ~/repos/bin/stagit-update <name>' - -# make private again -ssh $GIT_HOST 'rm ~/repos/<name>.git/public \ - && echo ssh://user@debian-wedding/~/repos/<name>.git > ~/repos/<name>.git/url \ - && ~/repos/bin/stagit-update <name>' +ryops repos publish <name> +ryops repos unpublish <name> ``` -`stagit-update` removes the public mirror when the marker is gone; the next -`publish-public` run on the next push will still have stale files on Bunny. -Run `~/repos/bin/publish-public-prune` to clean them up. +Both commands regenerate the stagit output and publish it. `unpublish` also +runs `publish-public-prune` so the stale public mirror is removed from Bunny. ### Update tooling @@ -131,7 +126,7 @@ reloads caddy, and runs `stagit-update` if stagit is already installed. | `./push` from laptop | sync tooling, write `config.env`, reload caddy, stagit-update | | `git push <repo>` to the server | `post-receive` → `update-server-info` + `stagit-update` + `publish-public` | | `bringup` on the server | one-time install (stagit, caddy snippet) | -| `add-repo` | creates bare repo + hook + initial HTML | +| `ryops repos add` | creates bare repo + hook + initial HTML | | `remove-repo` | deletes bare repo + HTML + archive + Bunny orphans | | `publish-public-prune` | deletes Bunny objects absent from local `www-public/` | | Bunny CDN | serves public repos automatically; edge rules enforce no-cache on git refs + stagit HTML | @@ -145,6 +140,8 @@ Everything is push-driven. Caddy's `remote_ip` matcher rejects anything outside `100.64.0.0/10` + `fd7a:115c:a1e0::/48` with a 403. - **Public**: `https://$PUBLIC_DOMAIN/` — lists only public repos. + `https://$PUBLIC_DOMAIN/<name>` opens the rendered README when one exists and + the commit log otherwise. ## Cloning diff --git a/bin/add-repo b/bin/add-repo @@ -1,39 +0,0 @@ -#!/bin/sh -# Create a bare repo with hook + metadata. -# Usage: add-repo <name> [--public] [-d "description"] -set -eu - -. "$HOME/repos/config.env" 2>/dev/null || true -: "${GIT_HOSTNAME:=$(hostname)}" -: "${PUBLIC_DOMAIN:=git.example.com}" - -name="" -public=0 -desc="" -while [ $# -gt 0 ]; do - case $1 in - --public) public=1 ;; - -d) desc=$2; shift ;; - -*) echo "unknown flag: $1" >&2; exit 2 ;; - *) name=$1 ;; - esac - shift -done -[ -n "$name" ] || { echo "usage: add-repo <name> [--public] [-d desc]" >&2; exit 2; } - -dir=$HOME/repos/$name.git -[ -e "$dir" ] && { echo "already exists: $dir" >&2; exit 1; } - -git init --bare "$dir" -ln -s "$HOME/repos/bin/post-receive" "$dir/hooks/post-receive" -echo "${desc:-$name}" > "$dir/description" - -if [ "$public" = "1" ]; then - touch "$dir/public" - echo "https://$PUBLIC_DOMAIN/git/$name.git" > "$dir/url" -else - echo "ssh://$USER@$GIT_HOSTNAME/~/repos/$name.git" > "$dir/url" -fi - -"$HOME/repos/bin/stagit-update" "$name" -echo "ok: $dir" diff --git a/bin/bringup b/bin/bringup @@ -47,4 +47,4 @@ sudo systemctl reload caddy # Initial build over anything already present "$REPOS/bin/stagit-update" -echo "ok: server ready. next: ~/repos/bin/add-repo <name> [--public]" +echo "ok: server ready. next, from the laptop: ryops repos add [--public] <name>" diff --git a/bin/repo-index b/bin/repo-index @@ -0,0 +1,23 @@ +#!/bin/sh +# Generate the default page for a stagit repository. Prefer the rendered +# README and fall back to the commit log. +set -eu + +out=${1:-} +name=${2:-} +[ -d "$out" ] && [ -n "$name" ] || { echo "usage: repo-index <output-dir> <repo-name>" >&2; exit 2; } +case $name in *[!A-Za-z0-9._-]*) echo "invalid repo name: $name" >&2; exit 2 ;; esac + +target=/$name/log.html +if [ -f "$out/file/README.md.html" ]; then + target=/$name/file/README.md.html +fi + +cat > "$out/index.html" <<EOF +<!doctype html> +<meta charset="utf-8"> +<meta http-equiv="refresh" content="0; url=$target"> +<link rel="canonical" href="$target"> +<title>Redirecting</title> +<a href="$target">Continue to the repository</a> +EOF diff --git a/bin/stagit-update b/bin/stagit-update @@ -92,6 +92,7 @@ build_one() { link_assets "$out" render_markdown "$out" "$repo" render_file_tree "$out" "$repo" + "$REPOS/bin/repo-index" "$out" "$name" if [ -f "$repo/public" ]; then out=$PUB/$name @@ -100,6 +101,7 @@ build_one() { link_assets "$out" render_markdown "$out" "$repo" render_file_tree "$out" "$repo" + "$REPOS/bin/repo-index" "$out" "$name" # bare repo mirror for `git clone` (skip server-side bits) mkdir -p "$PUB/git/$name.git" rsync -a --delete --exclude=hooks --exclude=public "$repo/" "$PUB/git/$name.git/" diff --git a/dev/test.sh b/dev/test.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +OUT=$TMP/boot2 +mkdir -p "$OUT/file" + +: > "$OUT/log.html" +: > "$OUT/file/README.md.html" +"$ROOT/bin/repo-index" "$OUT" boot2 +grep -F 'url=/boot2/file/README.md.html' "$OUT/index.html" >/dev/null + +rm "$OUT/file/README.md.html" "$OUT/index.html" +"$ROOT/bin/repo-index" "$OUT" boot2 +grep -F 'url=/boot2/log.html' "$OUT/index.html" >/dev/null diff --git a/justfile b/justfile @@ -0,0 +1,2 @@ +test: + ./dev/test.sh diff --git a/push b/push @@ -18,7 +18,7 @@ rsync -av --delete "$DIR/bin/" "$GIT_HOST:repos/bin/" rsync -av --delete "$DIR/assets/" "$GIT_HOST:repos/assets/" rsync -av --delete "$DIR/caddy/" "$GIT_HOST:repos/caddy/" -# Server-side config for scripts (add-repo, etc.) +# Server-side config for publishing scripts and ryops-managed repo metadata. ssh "$GIT_HOST" "cat > ~/repos/config.env" <<EOF GIT_HOSTNAME=$GIT_HOSTNAME PUBLIC_DOMAIN=$PUBLIC_DOMAIN