git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / scripts / host-repo.sh /
scripts/host-repo.sh · 8.1 KiB · raw
view: SourceMarkdown ☑ force

#!/bin/bash

host-repo.sh — publish a git checkout as a bare repo on upspin, ready to be

browsed, cloned, and pushed through gitweb (https://git.cosmocr.at).

Usage:

scripts/host-repo.sh [--public | --access FILE] [--config FILE] \

[--owner-config FILE]

an existing git working copy (committed history is

published; the working tree and remotes are not)

destination, e.g. rpg137@gmail.com/myrepo.git

--public grant read,list to all (anonymous browse + clone);

default is private: only the owner and the service

--access FILE use FILE as the repo's Access file instead of the

generated one (mutually exclusive with --public)

--config FILE upspin config to use (default $HOME/upspin/config)

--owner-config FILE

config of the destination tree's OWNER, used only to put

the Access file. Needed when hosting into another user's

tree: upspin lets only the owner write Access files, while

the content upload runs as the (granted) --config user.

What it does, and why:

- Makes a fresh git clone --bare of the checkout (single packfile +

packed-refs — the fewest upspin files), drops hooks/description and the

origin remote (which would leak the local path), and sanity-checks HEAD.

- Puts the Access file FIRST, then uploads the repo content. With ee

packing, each file's decryption key is wrapped for the readers granted

by the governing Access file at write time — so the grants (the

service, and all for --public) must be in place before the upload, or

every file would need an upspin share -fix afterwards to be readable.

- Uploads HEAD last: gitweb treats a directory as a repo once HEAD and

objects/ both exist, so holding HEAD back keeps the repo from being

served half-copied.

- The Access file grants * (all rights) to the tree owner; to

git@cosmocr.at unless it is itself the owner (the service reads the repo

for web/git clients and git push write-back runs as it); to the Admins

group; and, with --owner-config, to the uploading user, who is not the

owner yet still writes the content. Who may push is whoever holds the

upspin write right here (scripts/update-access.sh edits and re-shares).

Requires: git, upspin (with the destination tree owned by the configured

upspin user — only the owner may put Access files).

set -euo pipefail

die() { echo "host-repo.sh: $*" >&2; exit 1; }

PUBLIC=0 CONFIG="${HOME}/upspin/config" OWNERCONFIG="" ACCESSFILE="" ARGS=() while [ $# -gt 0 ]; do case "$1" in --public) PUBLIC=1 ;; --access) shift; ACCESSFILE="${1:?--access needs a file}" ;; --config) shift; CONFIG="${1:?--config needs a file}" ;; --owner-config) shift; OWNERCONFIG="${1:?--owner-config needs a file}" ;; -h|--help) awk 'NR>1 && /^#/ { sub(/^# ?/, ""); print; next } NR>1 { exit }' "$0"; exit 0 ;; -*) die "unknown flag $1" ;; *) ARGS+=("$1") ;; esac shift done [ "${#ARGS[@]}" -eq 2 ] || die "usage: host-repo.sh [--public | --access FILE] [--config FILE] [--owner-config FILE] " CHECKOUT="${ARGS[0]}" DEST="${ARGS[1]%/}" [ -z "$ACCESSFILE" ] || [ "$PUBLIC" = 0 ] || die "--access and --public are mutually exclusive (put read/list: all in the file)" [ -z "$ACCESSFILE" ] || [ -s "$ACCESSFILE" ] || die "--access file missing or empty: $ACCESSFILE"

command -v git >/dev/null || die "git not found" command -v upspin >/dev/null || die "upspin not found" [ -f "$CONFIG" ] || die "upspin config not found: $CONFIG"

--- validate the source checkout ---

git -C "$CHECKOUT" rev-parse --git-dir >/dev/null 2>&1 || die "$CHECKOUT is not a git checkout" git -C "$CHECKOUT" rev-parse -q --verify HEAD >/dev/null || die "$CHECKOUT has no commits"

--- validate the destination path ---

OWNER="${DEST%%/}" case "$OWNER" in @.) ;; *) die "destination must start with an upspin user (user@domain/...): $DEST" ;; esac [ "$DEST" != "$OWNER" ] || die "destination must include a repo path under the user root" case "$DEST" in *.git) ;; *) echo "note: destination does not end in .git — works, but the convention helps clients" >&2 ;; esac

ME=$(sed -n 's/^username: *//p' "$CONFIG" | head -1) if [ -n "$OWNERCONFIG" ]; then [ -f "$OWNERCONFIG" ] || die "owner config not found: $OWNERCONFIG" OC_USER=$(sed -n 's/^username: *//p' "$OWNERCONFIG" | head -1) [ "$OC_USER" = "$OWNER" ] || die "--owner-config user is $OC_USER but the destination tree is owned by $OWNER" elif [ "$ME" != "$OWNER" ]; then echo "warning: destination tree is owned by $OWNER but the config user is $ME;" >&2 echo " the Access put will fail unless run as the owner (see --owner-config)" >&2 fi

if upspin -config="$CONFIG" ls "$DEST" >/dev/null 2>&1; then die "$DEST already exists — refusing to overwrite" fi

--- stage a clean bare repo ---

TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT BASE=$(basename "$DEST") STAGE="$TMP/$BASE" # cp -R below copies the dir itself, so its name is final

git clone --quiet --bare "$CHECKOUT" "$STAGE" git -C "$STAGE" config --remove-section remote.origin 2>/dev/null || true git -C "$STAGE" pack-refs --all rm -rf "$STAGE/hooks" "$STAGE/description"

HEAD must be a symref to a branch that exists, or clients see an empty repo.

HEADREF=$(git -C "$STAGE" symbolic-ref -q HEAD || true) if [ -z "$HEADREF" ] || ! git -C "$STAGE" show-ref -q --verify "$HEADREF"; then FIRST=$(git -C "$STAGE" for-each-ref --format='%(refname)' refs/heads | head -1) [ -n "$FIRST" ] || die "staged repo has no branches" git -C "$STAGE" symbolic-ref HEAD "$FIRST" echo "note: HEAD pointed at a missing branch; reset to $FIRST" >&2 fi

--- upspin with config ---

UP() { upspin -config="$CONFIG" "$@"; }

--- upload: parents, Access first, content, HEAD last ---

if [ "$(dirname "$DEST")" != "$OWNER" ]; then

Create intermediate directories one level at a time (no mkdir -p).

REL=$(dirname "${DEST#"$OWNER"/}") P="$OWNER" IFS='/' read -ra SEGS <<< "$REL" for seg in "${SEGS[@]}"; do P="$P/$seg" UP mkdir "$P" 2>/dev/null || true done fi UP mkdir "$DEST"

Access before content, so ee-packed uploads wrap keys for these readers.

Only the tree owner may put Access files, hence the --owner-config escape.

ACCESS="$TMP/Access" if [ -n "$ACCESSFILE" ]; then cp "$ACCESSFILE" "$ACCESS" else

The "*" list, with nobody named twice: the owner; the service, unless it is

the owner; the Admins group; and, when --owner-config is in play, the

uploading user $ME, who is not the owner but still writes the content.

STAR="$OWNER" [ "$OWNER" = "git@cosmocr.at" ] || STAR="$STAR, git@cosmocr.at" STAR="$STAR, upspin@cosmocr.at/Group/Admins" if [ -n "$OWNERCONFIG" ] && [ "$ME" != "$OWNER" ] && [ "$ME" != "git@cosmocr.at" ]; then STAR="$STAR, $ME" fi { echo "*: $STAR" if [ "$PUBLIC" = 1 ]; then echo "read: all" echo "list: all" echo "write: upspin@cosmocr.at/Group/Writers" fi } > "$ACCESS" fi if [ -n "$OWNERCONFIG" ]; then upspin -config="$OWNERCONFIG" cp "$ACCESS" "$DEST/Access" else UP cp "$ACCESS" "$DEST/Access" fi

echo "==> uploading $(du -sh "$STAGE" | cut -f1) to $DEST"

Everything except HEAD, then HEAD: the repo only becomes servable once

HEAD lands. upspin cp -R dir dst/ copies dir into dst by basename.

for entry in "$STAGE"/*; do name=$(basename "$entry") [ "$name" = "HEAD" ] && continue if [ -d "$entry" ]; then UP cp -R "$entry" "$DEST/" else UP cp "$entry" "$DEST/" fi done UP cp "$STAGE/HEAD" "$DEST/"

--- verify ---

UP ls "$DEST/HEAD" >/dev/null || die "verify failed: $DEST/HEAD not readable" URL="https://git.cosmocr.at/$DEST" echo "==> hosted at $DEST" if grep -qE '^\sread\s[:,].*\ball\b' "$ACCESS"; then echo " browse/clone (anonymous): $URL" else echo " private: browse/clone/push need credentials (vault SSO via" echo " git-credential-cosmocr, or a PAT in $OWNER/gitweb/tokens)" echo " clone: git clone $URL" fi echo " push access: users with 'write' in $DEST/Access"