commit 58ab4127ea864997b089fb8f14453d06345025f4
parent 24ad282a109f886f614e1bb8da5ac61c06194e73
Author: William Lindholm <william_lindholm@outlook.com>
Date: Mon, 21 Sep 2026 20:16:47 +0200
Added stagit implementation
Diffstat:
11 files changed, 268 insertions(+), 1 deletion(-)
diff --git a/.gitattributes b/.gitattributes
@@ -0,0 +1 @@
+*.sh text eol=lf
diff --git a/README.md b/README.md
@@ -14,6 +14,23 @@ To start with automatic application updates during development, run:
docker compose watch
```
+# Git frontend
+
+Add one public HTTPS clone URL per line to `stagit/repos.txt`. Repository names
+must be unique. Run `docker compose up -d --build`, then open <http://localhost>.
+The same pages are served at <https://git.lindholmlabs.com> in production.
+
+The stagit container clones or fetches immediately, generates static pages, then
+sleeps for 24 hours. Restart it with `docker compose restart stagit` to sync now.
+Mirrors and generated pages persist in named volumes. Unavailable remotes keep
+their last local copy; removing a URL does not delete its mirror. Caddy serves the
+completed output read-only; stagit exposes no ports.
+
+The Git pages use local copies of Codemadness's
+[stylesheet](https://codemadness.org/git/style.css),
+[favicon](https://codemadness.org/git/favicon.png), and
+[logo](https://codemadness.org/git/logo.png).
+
# Software Requirements Specification
## Personal Résumé Website
@@ -42,8 +59,18 @@ Use this structure:
```
/
├── .gitignore
+├── .gitattributes
├── README.md
├── compose.yml
+├── caddy/
+│ └── Caddyfile
+├── stagit/
+│ ├── Dockerfile
+│ ├── sync.sh
+│ ├── repos.txt
+│ ├── style.css
+│ ├── favicon.png
+│ └── logo.png
└── cv/
├── main.py
├── Dockerfile
@@ -58,7 +85,8 @@ Do not add directories.
The README shall contain only:
1. Instructions to start the container.
-2. This SRS.
+2. Git frontend instructions.
+3. This SRS.
Place the start instructions first.
diff --git a/caddy/Caddyfile b/caddy/Caddyfile
@@ -1,3 +1,8 @@
cv.lindholmlabs.com {
reverse_proxy cv:8000
}
+
+git.lindholmlabs.com, http://localhost {
+ root * /www/current
+ file_server
+}
diff --git a/compose.yml b/compose.yml
@@ -7,6 +7,7 @@ services:
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
+ - stagit_www:/www:ro
depends_on:
- cv
@@ -30,3 +31,16 @@ services:
path: ./cv/Dockerfile
- action: rebuild
path: ./cv/requirements.txt
+
+ stagit:
+ build: ./stagit
+ restart: unless-stopped
+ init: true
+ volumes:
+ - ./stagit:/config:ro
+ - stagit_git:/git
+ - stagit_www:/www
+
+volumes:
+ stagit_git:
+ stagit_www:
diff --git a/stagit/.gitkeep b/stagit/.gitkeep
diff --git a/stagit/Dockerfile b/stagit/Dockerfile
@@ -0,0 +1,5 @@
+FROM alpine:3.22
+RUN apk add --no-cache git ca-certificates stagit
+COPY sync.sh /sync.sh
+COPY style.css favicon.png logo.png /assets/
+CMD ["sh", "/sync.sh"]
diff --git a/stagit/favicon.png b/stagit/favicon.png
Binary files differ.
diff --git a/stagit/logo.png b/stagit/logo.png
Binary files differ.
diff --git a/stagit/repos.txt b/stagit/repos.txt
@@ -0,0 +1 @@
+https://github.com/LindholmLabs/DSA-Assignments.git
+\ No newline at end of file
diff --git a/stagit/style.css b/stagit/style.css
@@ -0,0 +1,154 @@
+body {
+ color: #000;
+ background-color: #fff;
+ font-family: monospace;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-size: 1em;
+ margin: 0;
+}
+
+img, h1, h2 {
+ vertical-align: middle;
+}
+
+img {
+ border: 0;
+}
+
+a:target {
+ background-color: #ccc;
+}
+
+a.d,
+a.h,
+a.i,
+a.line {
+ text-decoration: none;
+}
+
+#blob a {
+ color: #555;
+}
+
+#blob a:hover {
+ color: blue;
+ text-decoration: none;
+}
+
+table thead td {
+ font-weight: bold;
+}
+
+table td {
+ padding: 0 0.4em;
+}
+
+#content table td {
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+#branches tr:hover td,
+#tags tr:hover td,
+#index tr:hover td,
+#log tr:hover td,
+#files tr:hover td {
+ background-color: #eee;
+}
+
+#index tr td:nth-child(2),
+#tags tr td:nth-child(3),
+#branches tr td:nth-child(3),
+#log tr td:nth-child(2) {
+ white-space: normal;
+}
+
+td.num {
+ text-align: right;
+}
+
+.desc {
+ color: #555;
+}
+
+hr {
+ border: 0;
+ border-top: 1px solid #555;
+ height: 1px;
+}
+
+pre {
+ font-family: monospace;
+}
+
+pre a.h {
+ color: #00a;
+}
+
+.A,
+span.i,
+pre a.i {
+ color: #070;
+}
+
+.D,
+span.d,
+pre a.d {
+ color: #e00;
+}
+
+pre a.h:hover,
+pre a.i:hover,
+pre a.d:hover {
+ text-decoration: none;
+}
+
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #000;
+ color: #bdbdbd;
+ }
+ hr {
+ border-color: #222;
+ }
+ a {
+ color: #56c8ff;
+ }
+ a:target {
+ background-color: #222;
+ }
+ .desc {
+ color: #aaa;
+ }
+ #blob a {
+ color: #555;
+ }
+ #blob a:target {
+ color: #eee;
+ }
+ #blob a:hover {
+ color: #56c8ff;
+ }
+ pre a.h {
+ color: #00cdcd;
+ }
+ .A,
+ span.i,
+ pre a.i {
+ color: #00cd00;
+ }
+ .D,
+ span.d,
+ pre a.d {
+ color: #cd0000;
+ }
+ #branches tr:hover td,
+ #tags tr:hover td,
+ #index tr:hover td,
+ #log tr:hover td,
+ #files tr:hover td {
+ background-color: #111;
+ }
+}
diff --git a/stagit/sync.sh b/stagit/sync.sh
@@ -0,0 +1,58 @@
+#!/bin/sh
+set -eu
+export GIT_TERMINAL_PROMPT=0
+mkdir -p /git /www
+rm -rf /git/.clone
+
+while :; do
+ echo 'Synchronizing repositories'
+ tr -d '\r' < /config/repos.txt > /tmp/repos.txt
+ while IFS= read -r url || [ -n "$url" ]; do
+ case "$url" in ''|'#'*) continue ;; esac
+ name=${url##*/}
+ name=${name%.git}
+ case "$name" in ''|.*|*[!a-zA-Z0-9._-]*) echo "Invalid name: $name"; continue ;; esac
+ mirror="/git/$name.git"
+ if [ -d "$mirror" ]; then
+ if git --git-dir="$mirror" fetch --atomic --prune origin; then
+ echo "Updated $name"
+ else
+ echo "Unavailable: $name; keeping local mirror"
+ fi
+ elif git clone --mirror "$url" /git/.clone; then
+ mv /git/.clone "$mirror"
+ : > "$mirror/description"
+ echo "Cloned $name"
+ else
+ rm -rf /git/.clone
+ echo "Could not clone $name; skipping"
+ fi
+ done < /tmp/repos.txt
+
+ echo 'Generating stagit pages'
+ # Build in the inactive directory, then switch Caddy to the finished site.
+ build=/www/a
+ [ "$(readlink /www/current || true)" != a ] || build=/www/b
+ rm -rf "$build"
+ mkdir "$build"
+ cp /assets/* "$build/"
+ for mirror in /git/*.git; do
+ [ -d "$mirror" ] || continue
+ name=${mirror##*/}
+ mkdir "$build/${name%.git}"
+ (cd "$build/${name%.git}" && stagit "$mirror" &&
+ ln -s log.html index.html &&
+ ln -s ../style.css ../favicon.png ../logo.png .)
+ done
+ set -- /git/*.git
+ if [ -d "$1" ]; then
+ stagit-index "$@" > "$build/index.html"
+ else
+ echo '<!doctype html><title>Repositories</title><link rel="stylesheet" href="style.css"><link rel="icon" href="favicon.png"><img src="logo.png" alt=""><p>No repositories configured.</p>' > "$build/index.html"
+ fi
+ ln -sfn "${build##*/}" /www/current.new
+ mv -Tf /www/current.new /www/current
+ echo 'Site published; next synchronization in 24 hours'
+ sleep 86400 &
+ wait "$!"
+done