#!/usr/bin/env bash # push-split.sh — push a subdirectory of the monorepo working-copy to its # matching split repo on Hephaestus, preserving history via git-subtree-split. # # Since silentmode/silentmode was deleted and each project lives in its own # repo on the forge (silentmode/theseus, silentmode/ariadne, silentmode/hephaestus, # silentmode/sirius, …), cross-cutting commits made in this working copy need to # be routed to whichever remote corresponds to the touched project. # # Usage: # scripts/push-split.sh [remote-name] # # Examples: # scripts/push-split.sh Hephaestus # → git remote 'hephaestus' # scripts/push-split.sh TheseusNavigator # → git remote 'theseus' (Navigator suffix stripped) # scripts/push-split.sh AriadneResolver # → git remote 'ariadne' (Resolver suffix stripped) # scripts/push-split.sh site-sirius-x sirius # → remote 'sirius' (explicit override) # # List of configured remotes: `git remote -v` # # Notes: # - Only ONE prefix per invocation. Cross-cutting changes that touched multiple # projects need to be run once per touched directory. # - Uses --branch , pushes, then deletes it. Nothing sticks around. # - Push target is always :master. Passing --branch was intentional # over --squash: split history is preserved so file blame on the split repo # still points at the real commits. set -euo pipefail prefix="${1:?usage: push-split.sh [remote-name]}" # Derive a remote name from the directory if not passed. # Heuristic: lowercase, strip Navigator/Resolver suffix. Otherwise pass explicitly. default_remote=$(echo "$prefix" | tr '[:upper:]' '[:lower:]' | sed 's/navigator$//; s/resolver$//') remote="${2:-$default_remote}" [ -d "$prefix" ] || { echo "error: no such directory: $prefix" >&2; exit 1; } git remote get-url "$remote" >/dev/null 2>&1 || { echo "error: no git remote named '$remote'." >&2 echo "available remotes: $(git remote | paste -sd ' ' -)" >&2 echo "pass an explicit remote name as the 2nd arg." >&2 exit 1 } # Uncommitted changes get carried into the split — usually not what you want. # Warn (but don't block; sometimes you know what you're doing). if ! git diff --quiet HEAD -- "$prefix" 2>/dev/null; then echo "warning: $prefix has uncommitted changes — those will NOT be included in the split." >&2 fi branch="_push_${remote}_$$" trap 'git branch -D "$branch" >/dev/null 2>&1 || true' EXIT echo "→ splitting $prefix/ into ephemeral branch $branch …" git subtree split --prefix="$prefix" HEAD -b "$branch" >/dev/null # Silent Mode public repos do not carry AI-tool attribution in commit metadata # (see CLAUDE.md at the repo root). Strip any "Co-Authored-By: Claude ..." / # "Co-authored-by: Claude ..." trailers from the ephemeral branch before push. # Local monorepo history is untouched — only what lands on the forge is filtered. echo "→ scrubbing Co-Authored-By: Claude trailers from $branch …" # filter-branch refuses to run from a dirty working tree ("You have unstaged # changes"), and with set -e that silently aborted the push whenever another # session had uncommitted edits. Run it from a throwaway worktree of the # ephemeral branch instead — refs are shared, the main tree is never touched. scrubwt=$(mktemp -d "${TMPDIR:-/tmp}/push-split-XXXXXX") git worktree add --detach --quiet "$scrubwt" "$branch" if ! (cd "$scrubwt" && FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --force --msg-filter ' sed -e "/^Co-[Aa]uthored-[Bb]y: Claude.*/d" ' "$branch" >/dev/null 2>&1); then git worktree remove --force "$scrubwt" >/dev/null 2>&1 || true echo "error: trailer scrub failed — not pushing." >&2 exit 1 fi git worktree remove --force "$scrubwt" >/dev/null 2>&1 || true # filter-branch leaves refs/original/refs/heads/$branch as a backup — delete it # so it can not be pushed by accident (--mirror isn't used here, but be safe). git update-ref -d "refs/original/refs/heads/$branch" 2>/dev/null || true echo "→ pushing $branch → $remote:master …" git push --force "$remote" "$branch:master" echo "✓ done — see https://code.silentmode.st/silentmode/$remote"