#!/usr/bin/env bash # Publishes this repository to Tangled WITHOUT Google's BITE source archives. # # scripts/ship-tangled.sh dry run: builds and checks what would be published, pushes nothing # scripts/ship-tangled.sh --push the same, then pushes # add --allow-uncommitted when you are mid-change and want to publish only what is committed (the changes in your working tree are NOT published; they are listed) # # Why: the two BITE zips (84 MB, mostly third-party libraries) are kept as reference in the private copy of this repository only. Removing them in a new commit would # leave them in the history, so the published history is rebuilt without them. The rebuild happens in a scratch clone: this repository and its history are never touched. # It is deterministic (commit dates and authors are kept), so a later publish produces the same commits for what was already published and adds the new ones on top: # the push is always a plain fast-forward and is never forced. Never run `git push` of this repository's own main to Tangled: that would publish the archives. # # Settings: AITE_SHIP_REMOTE (default git@tangled.org:distributed.camp/aite), AITE_SHIP_BRANCH (default main). set -euo pipefail REMOTE="${AITE_SHIP_REMOTE:-git@tangled.org:distributed.camp/aite}" BRANCH="${AITE_SHIP_BRANCH:-main}" LEAVE_OUT=(source-archive-bite.zip build.source-archive.zip) MAX_BLOB_BYTES=$((5 * 1024 * 1024)) push=no dirty_ok=no for arg in "$@"; do case "$arg" in --push) push=yes ;; --allow-uncommitted) dirty_ok=yes ;; *) echo "usage: $0 [--push] [--allow-uncommitted]" >&2; exit 2 ;; esac done here="$(git rev-parse --show-toplevel)" if [ -n "$(git -C "$here" status --porcelain)" ]; then if [ "$dirty_ok" != yes ]; then echo "refusing: this repository has uncommitted changes, and only commits are published. Commit them, or pass --allow-uncommitted to publish the commits without them." >&2 exit 1 fi echo "note: these uncommitted changes are NOT published:" git -C "$here" status --short | sed 's/^/ /' fi original_tip="$(git -C "$here" rev-parse "$BRANCH")" work="$(mktemp -d "${TMPDIR:-/tmp}/aite-ship.XXXXXX")" echo "scratch clone: $work/repo (left in place; delete it when you are done)" git clone --quiet --no-local --single-branch --branch "$BRANCH" "$here" "$work/repo" cd "$work/repo" FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --force --index-filter "git rm --cached --ignore-unmatch -q -- ${LEAVE_OUT[*]}" -- "$BRANCH" >/dev/null 2>&1 # Checks: it must be what we think it is before anything leaves this machine. fail() { echo "refusing: $1" >&2; exit 1; } before="$(git -C "$here" rev-list --count "$BRANCH")" after="$(git rev-list --count "$BRANCH")" [ "$before" = "$after" ] || fail "the rebuilt history has $after commits, the original has $before" objects="$(git rev-list --objects "$BRANCH")" # kept in a variable: grep -q ending a pipe early would make the check below pass by accident under pipefail for name in "${LEAVE_OUT[@]}"; do if grep -q -F " $name" <<<"$objects"; then fail "$name is still in the history"; fi done biggest="$(git rev-list --objects "$BRANCH" | git cat-file --batch-check='%(objecttype) %(objectsize) %(rest)' | awk '$1=="blob"{print $2}' | sort -rn | head -1)" [ "$biggest" -lt "$MAX_BLOB_BYTES" ] || fail "a file of $biggest bytes is in the history (limit $MAX_BLOB_BYTES)" changed="$(git diff --name-status "$original_tip" "$BRANCH" | sort)" expected="$(for n in "${LEAVE_OUT[@]}"; do printf 'D\t%s\n' "$n"; done | sort)" [ "$changed" = "$expected" ] || fail "the published tree differs from this repository's in more than the left-out files: $changed" echo "ok: $after commits, largest file $biggest bytes, the tree differs from this repository's only by: ${LEAVE_OUT[*]}" echo "would publish $(git rev-parse --short "$BRANCH") to $REMOTE ($BRANCH)" if [ "$push" = yes ]; then git push "$REMOTE" "$BRANCH:$BRANCH" echo "published." else echo "dry run: nothing was pushed. Run again with --push to publish." fi