git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / scripts / host-repo.sh /
scripts/host-repo.sh · 8.1 KiB · raw
view: Source ☐ force
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#!/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] <checkout-path> <upspin-path>
#
#   <checkout-path>  an existing git working copy (committed history is
#                    published; the working tree and remotes are not)
#   <upspin-path>    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-path> <upspin-path>"
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 '^\s*read\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"