- admin workstation: `install_server.sh`
- inside a target system: `start_root_t490s.sh`, `start_root_server.sh`,
`start_user.sh`, `update_efi.sh` (local systems only, see below)
- - optional, by hand on an already-set-up target: `setup_git_mirror.sh`
+ - optional, by hand on an already-set-up target: `setup_git_server.sh`
+ - optional, by hand from any machine holding a checkout, targeting a
+ repos server over ssh: `adopt_repo.sh`
- `to_install/` — per-target package lists (`t490s`, `server`), read in place
by `start_root` from `${PATH_REPO}/to_install/${TARGET}`.
- `to_copy/` — plain files installed verbatim by a target-side script.
- `templates/` — files rendered by `render_template` with `${VAR}`/`$(cmd)`
expansion, instead of inline heredocs: `fstab`, `wifi.nmconnection`
(rendered by `install_debian.sh`), `git-daemon.service`
- (`setup_git_mirror.sh`), `update-efi-hook` (`start_root_t490s.sh`).
+ (`setup_git_server.sh`), `update-efi-hook` (`start_root_t490s.sh`).
- `home/` — dotfile skeletons `home/any/`, `home/root/`, `home/user/`,
symlinked file-by-file into an account's home by `link_home` (see below).
`link_home <target>` always links `home/any/` (what every account wants)
`start_root_t490s.sh` already does it): `sh scripts/update_efi.sh`
- Push repo to a remote server and start its first-login setup (from the
admin's workstation): `sh scripts/install_server.sh <server>`
+- Make an already-set-up server a git host: `sh scripts/setup_git_server.sh`
+- Make it the authoritative home of a repo (from any machine holding a
+ checkout of that repo): `sh scripts/adopt_repo.sh <path-repo> <server>`
- First login on a new local system: nothing to type — root's first console
login runs `start_root_t490s.sh` via a hook left by `install_debian.sh`.
- First login on a new remote server: triggered by `install_server.sh`.
- Both first-login scripts can be rerun by hand later (e.g.
`/data/confplom/scripts/start_root_t490s.sh`, or
`~/confplom/scripts/start_root_server.sh`); so can `start_user.sh`.
-- Make an already-set-up target a distribution point for fresh systems:
- `sh scripts/setup_git_mirror.sh`; a fresh system then runs
- `git clone git://<that-server>/<repo-dirname>.git`.
There is no test runner. For logic changes prefer a dry read-through plus
`sh -n`/`shellcheck -s dash`; never execute the scripts against real block
parsed at runtime out of the rescue environment's
`/etc/network/interfaces` and re-emitted into a NetworkManager keyfile in
the target. The repo therefore carries nothing secret, which is what makes
- serving it anonymously via `setup_git_mirror.sh` acceptable.
+ serving it anonymously via `setup_git_server.sh` acceptable.
## Shared library
script**, because each script's `$HOME` differs: `install_server.sh` reads
the *admin's* `~/.ssh/known_hosts`; `start_root_server.sh` reads *root's*
`~/.ssh` on the freshly provisioned target.
-- `FNAME_INITRD`/`FNAME_VMLINUZ`, `FNAME_PROFILE`, `DIRNAME_SSH`, `TO_RBIND`:
- the remaining constants shared by two or more scripts.
+- `FNAME_INITRD`/`FNAME_VMLINUZ`, `FNAME_PROFILE`, `DIRNAME_SSH`, `TO_RBIND`,
+ `PATH_GIT_BASE` (`/srv/git`), `OPTS_SSH_NEW_HOST` (the `ssh`/`scp` option
+ accepting an unseen host key on first contact, so a first-ever connection
+ doesn't block on a prompt): the remaining constants shared by two or more
+ scripts.
Anything derived from values only known after argument parsing is *not* a
shared constant: build it from `path_luks_mapper`/`path_vg` (each taking the
debootstrap; the second's `close_luksvg` would tear the VG down under the
first. Run them serially.
-Exempt: `start_root_*.sh`, `start_user.sh`, `setup_git_mirror.sh` (already
-booted into a target, never touch the LUKS+VG), `install_server.sh` (only
-talks to a remote server over SSH; can run concurrently with the others or
-with itself against another server), and `update_efi.sh` (only mounts the
-plain EFI partition).
+Exempt: `start_root_*.sh`, `start_user.sh`, `setup_git_server.sh` (already
+booted into a target, never touch the LUKS+VG), `install_server.sh` and
+`adopt_repo.sh` (only talk to a remote server over SSH; either can run
+concurrently with the others or with itself against another server), and
+`update_efi.sh` (only mounts the plain EFI partition).
## `setup_luksvg.sh`
4. `ssh` in to `tar xf --no-same-owner` (lands at `~/<repo-dirname>`). The
flag matters: GNU `tar` as root defaults to `--same-owner`, which would
restore the *workstation's* numeric UID onto files under `/root`; that
- silent mismatch later makes root-run `git` (e.g. `setup_git_mirror.sh`'s
- `git clone --bare`) refuse the tree as "dubious ownership". Then `ssh -t`
+ silent mismatch would otherwise make any later root-run `git` operation
+ against that tree refuse it as "dubious ownership". Then `ssh -t`
(pseudo-tty, since the script ends prompting for a password) to run
`./${RELPATH_SETUP_SCRIPT}`.
hand as any account: as root it additionally picks up `home/user/` into
root's home; as the user it picks up entries added after provisioning.
-## `setup_git_mirror.sh`
+## `setup_git_server.sh`
-Optional, run by hand on an already-set-up target to make it a distribution
-point. Motivation: a fresh system with no SSH keys exchanged can't use
-`install_server.sh`; it can instead anonymously
-`git clone git://<server>/<repo-dirname>.git`. No arguments; refuses to run
-if `${PATH_GIT_MIRROR}` (`/srv/git/${DIRNAME_REPO}.git`) exists — one-time
-setup, not a sync step.
+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.
1. `apt-get -y update` + install `git` (a bare target may lack it).
-2. `git clone --bare "${PATH_REPO}"` into the mirror path — a snapshot, not
- a live link.
-3. `touch git-daemon-export-ok` in it, rather than `--export-all`, so other
- repos later placed under `/srv/git` aren't served automatically.
-4. `chown -R "${USERNAME}:${USERNAME}"`: `git://` is read-only, so keeping
- the mirror current means the admin `git push`ing over `ssh` as
- `${USERNAME}` (root SSH is already disabled), so that account must own it.
- Consequence, not a bug: root running `git` against the mirror trips
- "dubious ownership"; inspect via
- `sudo -u "${USERNAME}" git -C "${PATH_GIT_MIRROR}" log`, or for a
- one-off `git config --global --add safe.directory "${PATH_GIT_MIRROR}"`.
-5. Render `templates/git-daemon.service` (`User=${USERNAME}`; never run a
+2. `mkdir -p "${PATH_GIT_BASE}"` (`/srv/git`) and `chown` it (not `-R`) to
+ `${USERNAME}`, so that account can later `git init --bare` repos under
+ 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.
+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`;
+ `git daemon --reuseaddr --base-path=/srv/git /srv/git` — one daemon,
+ started once, ends up serving every repo `adopt_repo.sh` later adds
+ 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).
-6. Print the anonymous clone URL, the `ssh` push URL, and a reminder that
- git daemon listens on 9418/tcp in case a firewall is added later.
+4. Print 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`
+
+Optional, run by hand from any machine holding a checkout of the repo to
+adopt — this repo's own or another project's; only this repo's own
+`scripts/lib/` is needed, so per "Hardcoded machine assumptions" the running
+machine just needs *some* checkout of this repo present, same as any other
+script here — against a `setup_git_server.sh`-provisioned server. Two
+arguments: the local repo's path and the server. Makes that server the
+repo's authoritative, single copy of history, so it also repoints the local
+checkout's own `origin` there. Unlike every other server-facing script here
+it never runs *on* the server via a console/first-login hook — it drives the
+server purely over `ssh`, closer to `install_server.sh` than to a
+target-side script.
+
+1. Sanity checks: `${PATH_SRC_REPO}` looks like a git checkout; its current
+ branch (`NAME_BRANCH`, via `symbolic-ref`) resolves — refuses a detached
+ `HEAD`; the target `${PATH_GIT_BASE}/<repo>.git` doesn't already exist on
+ the server (checked over `ssh`) — one-time adoption per repo, not a sync
+ step.
+2. Over `ssh`, as `${USERNAME}` (root SSH is disabled by
+ `start_root_server.sh`): `git init --bare
+ --initial-branch="${NAME_BRANCH}"` — genuinely empty, not a clone, since
+ there's nothing local to the server to clone from; content arrives in
+ the next step. `--initial-branch` matters because an empty bare repo has
+ nothing to derive a default branch from otherwise, and its `HEAD`
+ (pointing at a ref that doesn't exist yet) only resolves once that
+ same-named branch is pushed. Then `touch git-daemon-export-ok` (rather
+ than `--export-all`, so other repos later adopted onto the same server
+ aren't served automatically) and sets `receive.denyNonFastForwards` +
+ `receive.denyDeletes`: once this is the repo's only copy, a force-push or
+ branch deletion has nothing to recover from, so pushes may only add
+ commits. Deliberate friction; lift either by hand on the server for a
+ rare, genuine history rewrite.
+3. `git push --mirror` from the local checkout carries over every branch
+ and tag in one shot — the actual content transfer, analogous to a
+ same-host `git clone --bare`.
+4. Only once that push has succeeded: point the local checkout's own
+ `origin` at the new `ssh://` URL (`remote set-url`, or `remote add` if it
+ had none). `set-url` alone never touches a separately configured
+ `remote.origin.pushurl` — which git prefers over `url` for pushes — so if
+ one was already set (and now differs), print how to also repoint it by
+ hand rather than silently dropping what may be a deliberate split.
+5. Print the new anonymous clone URL.
+
+Both scripts share `PATH_GIT_BASE` and (with `install_server.sh`)
+`OPTS_SSH_NEW_HOST` — see "Shared library".
## `home/` skeletons
--- /dev/null
+#!/bin/sh
+. "$(dirname "$0")/_lib.sh"
+include OPTS_SSH_NEW_HOST
+include PATH_GIT_BASE
+include USERNAME
+include check_tools
+include error
+include msg
+include try_quiet
+include usage
+
+# inputs to confirm
+usage $# "path-repo" "repos-server"
+PATH_SRC_REPO=$1
+SERVER=$2
+LOGIN="${USERNAME}@${SERVER}"
+
+# early sanity check, with further-down constant declarations relying on this …
+[ -d "${PATH_SRC_REPO}/.git" ]\
+ || error "${PATH_SRC_REPO}: not a git checkout"
+
+# constants derived from changeables
+NAME_REPO=$(basename "$(cd "${PATH_SRC_REPO}" && pwd)")
+PATH_REMOTE_REPO="${PATH_GIT_BASE}/${NAME_REPO}.git"
+URL_REMOTE_REPO="ssh://${LOGIN}${PATH_REMOTE_REPO}"
+
+# sanity checks and determination of current active repo branch as NAME_BRANCH
+# (needed for git init's --initial-branch, so the history-free remote repo's
+# HEAD resolves once our later git push --mirror creates that branch)
+check_tools git ssh
+NAME_BRANCH=$(git -C "${PATH_SRC_REPO}" symbolic-ref --quiet --short HEAD)\
+ || error "${PATH_SRC_REPO}: not on a branch (detached HEAD?)"
+msg 'Checking %s does not already host %s …' "${SERVER}" "${NAME_REPO}"
+ssh ${OPTS_SSH_NEW_HOST} "${LOGIN}" "[ ! -e '${PATH_REMOTE_REPO}' ]"\
+ || error "${PATH_REMOTE_REPO} already exists on ${SERVER}"
+
+msg 'Creating bare repo on %s, hardened against history loss …' "${SERVER}"
+ssh ${OPTS_SSH_NEW_HOST} "${LOGIN}" "git init --quiet --bare \
+ --initial-branch='${NAME_BRANCH}' '${PATH_REMOTE_REPO}' \
+ && touch '${PATH_REMOTE_REPO}/git-daemon-export-ok' \
+ && git -C '${PATH_REMOTE_REPO}' config receive.denyNonFastForwards true \
+ && git -C '${PATH_REMOTE_REPO}' config receive.denyDeletes true"
+
+msg 'Pushing all branches and tags to %s …' "${SERVER}"
+GIT_SSH_COMMAND="ssh ${OPTS_SSH_NEW_HOST}" \
+ git -C "${PATH_SRC_REPO}" push --quiet --mirror "${URL_REMOTE_REPO}"
+
+msg "Pointing this checkout's origin at %s …" "${SERVER}"
+if try_quiet git -C "${PATH_SRC_REPO}" remote get-url origin; then
+ PUSHURL=$(git -C "${PATH_SRC_REPO}" config --get remote.origin.pushurl)\
+ || PUSHURL=""
+ git -C "${PATH_SRC_REPO}" remote set-url origin "${URL_REMOTE_REPO}"
+ if [ -n "${PUSHURL}" ] && [ "${PUSHURL}" != "${URL_REMOTE_REPO}" ]; then
+ msg 'Note: origin also has a separate pushurl (%s)' "${PUSHURL}"
+ msg 'left untouched; a bare git push will still use it. To also'
+ msg 'repoint pushes here:'
+ msg ' git remote set-url --push origin %s' "${URL_REMOTE_REPO}"
+ fi
+else
+ git -C "${PATH_SRC_REPO}" remote add origin "${URL_REMOTE_REPO}"
+fi
+
+msg '%s is now authoritative for %s.' "${SERVER}" "${NAME_REPO}"
+msg 'Anonymous clone URL: git://%s/%s.git' "${SERVER}" "${NAME_REPO}"