home · contact · privacy
On git server setup right-away self-host this very repo.
authorPlom Heller <plom@plomlompom.com>
Wed, 23 Sep 2026 10:28:42 +0000 (12:28 +0200)
committerPlom Heller <plom@plomlompom.com>
Wed, 23 Sep 2026 10:28:42 +0000 (12:28 +0200)
CLAUDE.md
scripts/setup_git_server.sh

index 6e5e816903e63c29bdd443f2cad5e9689856e320..10d999de4970ac7c1c4c9ed1293032c4165d2b3d 100644 (file)
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -561,13 +561,14 @@ root's home; as the user it picks up entries added after provisioning.
 ## `setup_git_server.sh`
 
 Optional, run by hand on an already-set-up target to turn it into a git
-host — infrastructure only, no specific repo. Motivation: a fresh system
-with no SSH keys exchanged can't use `install_server.sh`; a git host lets it
-instead anonymously `git clone git://<server>/<repo>.git`. Repos themselves
-are added afterwards, one at a time, by `adopt_repo.sh` (below). No
-arguments; safe to rerun — every step here is idempotent, since the one
-thing worth protecting against a rerun (an already-adopted repo) is state
-`adopt_repo.sh` owns, not this script.
+host. Motivation: a fresh system with no SSH keys exchanged can't use
+`install_server.sh`; a git host lets it instead anonymously
+`git clone git://<server>/<repo>.git`. The one repo this script hosts itself
+is this repo's own landed copy (`${PATH_REPO}`, e.g. `/opt/confplom`),
+since it's always already there and is exactly what such a fresh system
+most needs; every other repo is added afterwards, one at a time, by
+`adopt_repo.sh` (below). No arguments; safe to rerun — every step is
+idempotent or skipped if already done.
 
 1. `apt-get -y update` + install `git` (a bare target may lack it).
 2. `mkdir -p "${PATH_GIT_BASE}"` (`/srv/git`) and `chown` it (not `-R`) to
@@ -575,10 +576,10 @@ thing worth protecting against a rerun (an already-adopted repo) is state
    it (over `ssh`, via `adopt_repo.sh`) without sudo — root SSH is already
    disabled by `start_root_server.sh`, and `git://` itself is read-only, so
    this is the only way a repo ever gets created or updated there.
-   Deliberately not recursive: repos under `${PATH_GIT_BASE}` are
-   `adopt_repo.sh`'s to own, not this script's to reassert on a rerun — e.g.
-   a future repo owned by some other account shouldn't get silently
-   reclaimed by a rerun of this one.
+   Deliberately not recursive: repos under `${PATH_GIT_BASE}` are owned
+   repo by repo (step 4 below, or `adopt_repo.sh`), not reasserted on a
+   rerun — e.g. a future repo owned by some other account shouldn't get
+   silently reclaimed by a rerun of this one.
 3. Render `templates/git-daemon.service` (`User=${USERNAME}`; never run a
    network-facing daemon as root needlessly) running
    `git daemon --reuseaddr --base-path=/srv/git /srv/git` — one daemon,
@@ -586,8 +587,36 @@ thing worth protecting against a rerun (an already-adopted repo) is state
    under `${PATH_GIT_BASE}`, so it's never touched again per repo;
    `systemctl daemon-reload` + `enable --now git-daemon` (systemd is already
    present; no inetd needed).
-4. Print the clone/push URL patterns and a reminder that git daemon listens
-   on 9418/tcp in case a firewall is added later.
+4. **Self-hosting**, the same-host counterpart of `adopt_repo.sh`'s steps
+   2–3, skipped (narrated) if `${PATH_GIT_BASE}/<repo-dirname>.git` already
+   exists: `git clone --bare --no-hardlinks` from `${PATH_REPO}` (a bare
+   clone takes exactly branches and tags); `remote remove origin`, since a
+   bare clone still records its source as `origin`, meaningless there; the
+   same `git-daemon-export-ok` + `receive.deny*` hardening as `adopt_repo.sh`
+   (duplicated rather than shared: `adopt_repo.sh` sends it as an `ssh`
+   command string, which a `lib/` helper can't serve); then `chown -R` of
+   just this one new repo to `${USERNAME}`, the account every later push
+   arrives as. `--no-hardlinks` because a local clone otherwise hardlinks
+   object files, and that `chown -R` would then also re-own
+   `${PATH_REPO}`'s objects. The early sanity check that `${PATH_REPO}` has
+   a `.git` runs before anything else changes. Unlike `adopt_repo.sh` this
+   can't repoint the checkout actually committed in (that's on another
+   machine), so it prints the `git remote set-url` line to run there; until
+   then the server is merely seeded, not authoritative. The snapshot may be
+   older than that checkout, which is fine as long as nothing since was
+   rewritten — a rewrite would now be refused by `denyNonFastForwards`.
+5. Point `${PATH_REPO}`'s own `origin` at `git://localhost/<repo>.git`
+   (`set-url`, or `add` if none), unconditionally, so the landed copy can
+   later be refreshed with `git pull` instead of another `install_server.sh`
+   tar. Via `git://` rather than the local path because git refuses to
+   operate in a repo owned by another user (`safe.directory`), root not
+   exempt: a local-path fetch runs `upload-pack` as root inside the
+   `${USERNAME}`-owned bare repo, whereas over `git://` that side is run by
+   `git-daemon` as `${USERNAME}` itself. Also read-only, fitting a copy
+   nobody should push from.
+6. Print the workstation repoint line, the clone/push URL patterns and a
+   reminder that git daemon listens on 9418/tcp in case a firewall is added
+   later.
 
 ## `adopt_repo.sh`
 
index 768c3e3b099fde4aa083d8a18b549444d4754997..4890b3eb857e1b6fb7bb09bd07f334ee8fb8c4f3 100755 (executable)
@@ -1,13 +1,23 @@
 #!/bin/sh
 . "$(dirname "$0")/_lib.sh"
+include DIRNAME_REPO
 include PATH_GIT_BASE
+include PATH_REPO
 include USERNAME
+include error
 include msg
 include render_template
+include try_quiet
 
 # constants derived from changeables
 FNAME_GIT_DAEMON_UNIT=git-daemon.service
 PATH_GIT_DAEMON_UNIT="/etc/systemd/system/${FNAME_GIT_DAEMON_UNIT}"
+PATH_SELF_HOSTED="${PATH_GIT_BASE}/${DIRNAME_REPO}.git"
+URL_SELF_HOSTED_LOCAL="git://localhost/${DIRNAME_REPO}.git"
+
+# early sanity check, so we fail before changing anything
+[ -d "${PATH_REPO}/.git" ]\
+    || error "${PATH_REPO}: not a git checkout, so cannot host it"
 
 msg 'Ensuring installation of git …'
 apt-get -y update
@@ -23,7 +33,31 @@ render_template "${FNAME_GIT_DAEMON_UNIT}" >| "${PATH_GIT_DAEMON_UNIT}"
 systemctl daemon-reload
 systemctl enable --now git-daemon
 
-msg 'Ready. Adopt a repo onto this server from wherever it lives with:'
+if [ -e "${PATH_SELF_HOSTED}" ]; then
+    msg '%s already exists, leaving it alone.' "${PATH_SELF_HOSTED}"
+else
+    msg 'Hosting this very repo at %s, hardened against history loss …' \
+        "${PATH_SELF_HOSTED}"
+    git clone --quiet --bare --no-hardlinks "${PATH_REPO}" \
+        "${PATH_SELF_HOSTED}"
+    git -C "${PATH_SELF_HOSTED}" remote remove origin
+    touch "${PATH_SELF_HOSTED}/git-daemon-export-ok"
+    git -C "${PATH_SELF_HOSTED}" config receive.denyNonFastForwards true
+    git -C "${PATH_SELF_HOSTED}" config receive.denyDeletes true
+    chown -R "${USERNAME}:${USERNAME}" "${PATH_SELF_HOSTED}"
+fi
+
+msg "Pointing %s's origin at %s …" "${PATH_REPO}" "${URL_SELF_HOSTED_LOCAL}"
+if try_quiet git -C "${PATH_REPO}" remote get-url origin; then
+    git -C "${PATH_REPO}" remote set-url origin "${URL_SELF_HOSTED_LOCAL}"
+else
+    git -C "${PATH_REPO}" remote add origin "${URL_SELF_HOSTED_LOCAL}"
+fi
+
+msg 'Ready. To make the checkout you commit in push here, run there:'
+msg '    git remote set-url origin ssh://%s@<this-server>%s' \
+    "${USERNAME}" "${PATH_SELF_HOSTED}"
+msg 'Adopt any other repo onto this server from wherever it lives with:'
 msg '    sh scripts/adopt_repo.sh <path-repo> <this-server>'
 msg 'Anonymous clone URL pattern (read-only): git://<this-server>/<repo>.git'
 msg 'Push URL pattern (read-write): ssh://%s@<this-server>%s/<repo>.git' \