template/rename.sh

223 lines
8.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# rename.sh — rename this template to your actual project.
#
# Usage:
# ./rename.sh # interactive prompts
# ./rename.sh acme-corp my-svc # non-interactive (org, project)
#
# What it changes:
# go.mod module path
# **/*.go import paths
# .devcontainer/devcontainer.json name field
# README.md heading + module path references
# CLAUDE.md Module line
# .golangci.yml goimports local-prefixes
# git tags all template tags deleted
# git history squashed into one INIT commit
set -euo pipefail
# ── Colour helpers ────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${GREEN}${RESET} $*"; }
warn() { echo -e "${YELLOW}!${RESET} $*"; }
error() { echo -e "${RED}${RESET} $*" >&2; }
heading() { echo -e "\n${BOLD}$*${RESET}"; }
# ── Default Git host ──────────────────────────────────────────────────────────
# Change this if you ever migrate to a different server.
DEFAULT_HOST="gitea.djmil.dev"
# ── Validation ────────────────────────────────────────────────────────────────
validate_slug() {
local val="$1" label="$2"
if [[ -z "$val" ]]; then
error "$label cannot be empty."
return 1
fi
if [[ ! "$val" =~ ^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$ ]]; then
error "$label must be lowercase alphanumeric (hyphens/dots/underscores allowed, no leading/trailing)."
return 1
fi
}
# ── Convert kebab-case / snake_case to Title Case ─────────────────────────────
to_title() {
echo "$1" | sed -E 's/[-_]/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)} 1'
}
# ── Determine script's own directory (works with symlinks) ───────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# ── Check we're in the right repo ────────────────────────────────────────────
if [[ ! -f go.mod ]]; then
error "go.mod not found. Run this script from the project root."
exit 1
fi
CURRENT_MODULE=$(grep '^module ' go.mod | awk '{print $2}')
TEMPLATE_MODULE="${DEFAULT_HOST}/go/template"
if [[ "$CURRENT_MODULE" != "$TEMPLATE_MODULE" ]]; then
warn "Module is already '$CURRENT_MODULE' (not the default template value)."
warn "Continuing will replace '$CURRENT_MODULE' with your new path."
echo
fi
# ── Capture template version before touching anything ─────────────────────────
TEMPLATE_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "untagged")
# ── Gather inputs ─────────────────────────────────────────────────────────────
heading "Go Template — Project Renamer"
echo "This script rewrites the module path and project name throughout the codebase."
echo
INTERACTIVE=true
if [[ $# -ge 2 ]]; then
NEW_ORG="$1"
NEW_PROJECT="$2"
INTERACTIVE=false
else
while true; do
read -rp "Org / username (e.g. djmil): " NEW_ORG
validate_slug "$NEW_ORG" "Org/username" && break
done
while true; do
read -rp "Project name (e.g. my-service): " NEW_PROJECT
validate_slug "$NEW_PROJECT" "Project name" && break
done
fi
validate_slug "$NEW_ORG" "Org/username"
validate_slug "$NEW_PROJECT" "Project name"
NEW_MODULE="${DEFAULT_HOST}/${NEW_ORG}/${NEW_PROJECT}"
OLD_MODULE="$CURRENT_MODULE"
OLD_PROJECT=$(basename "$OLD_MODULE") # e.g. username
NEW_DISPLAY=$(to_title "$NEW_PROJECT") # e.g. My Service
OLD_DISPLAY=$(to_title "$OLD_PROJECT") # e.g. Template
# ── Preview ───────────────────────────────────────────────────────────────────
heading "Changes to be applied"
printf " %-22s %s → %s\n" "Module path:" "$OLD_MODULE" "$NEW_MODULE"
printf " %-22s %s → %s\n" "Project name:" "$OLD_PROJECT" "$NEW_PROJECT"
printf " %-22s %s → %s\n" "Display name:" "$OLD_DISPLAY" "$NEW_DISPLAY"
printf " %-22s %s\n" "Template version:" "$TEMPLATE_TAG"
echo
if $INTERACTIVE; then
read -rp "Apply these changes? [y/N] " CONFIRM
case "$CONFIRM" in
[yY][eE][sS]|[yY]) ;;
*) echo "Aborted."; exit 0 ;;
esac
fi
# ── Helper: portable in-place sed ────────────────────────────────────────────
# macOS sed requires an extension argument for -i; GNU sed does not.
sedi() {
if sed --version &>/dev/null 2>&1; then
sed -i "$@"
else
sed -i '' "$@"
fi
}
# ── Apply substitutions ───────────────────────────────────────────────────────
heading "Applying changes"
# 1. go.mod — module declaration
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" go.mod
info "go.mod"
# 2. All Go source files — import paths
GO_FILES=$(find . \
-not -path './.git/*' \
-not -path './bin/*' \
-name '*.go' \
-type f)
CHANGED_GO=0
for f in $GO_FILES; do
if grep -q "$OLD_MODULE" "$f" 2>/dev/null; then
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" "$f"
CHANGED_GO=$((CHANGED_GO + 1))
fi
done
info "${CHANGED_GO} Go source file(s)"
# 3. .devcontainer/devcontainer.json — "name" field
if [[ -f .devcontainer/devcontainer.json ]]; then
sedi "s|\"name\": \"${OLD_DISPLAY}\"|\"name\": \"${NEW_DISPLAY}\"|g" \
.devcontainer/devcontainer.json
info ".devcontainer/devcontainer.json"
fi
# 4. README.md — heading + all module path occurrences
if [[ -f README.md ]]; then
sedi "s|^# ${OLD_PROJECT}$|# ${NEW_PROJECT}|g" README.md
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" README.md
info "README.md"
fi
# 5. CLAUDE.md — Module line
if [[ -f CLAUDE.md ]]; then
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" CLAUDE.md
info "CLAUDE.md"
fi
# 6. .golangci.yml — goimports local-prefixes
if [[ -f .golangci.yml ]]; then
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" .golangci.yml
info ".golangci.yml"
fi
# 7. git remote origin — rewrite URL preserving scheme (https or ssh)
if git remote get-url origin &>/dev/null 2>&1; then
OLD_REMOTE=$(git remote get-url origin)
if [[ "$OLD_REMOTE" == https://* ]]; then
NEW_REMOTE="https://${DEFAULT_HOST}/${NEW_ORG}/${NEW_PROJECT}.git"
else
# SSH form: git@host:org/project.git or ssh://git@host/org/project.git
NEW_REMOTE="git@${DEFAULT_HOST}:${NEW_ORG}/${NEW_PROJECT}.git"
fi
git remote set-url origin "$NEW_REMOTE"
info "git remote origin → ${NEW_REMOTE}"
else
warn "No 'origin' remote found — skipping remote update."
fi
# ── Squash git history into a single INIT commit ──────────────────────────────
heading "Git history"
# Stage rename changes, fold all commits back to root, rewrite as single commit.
git add -A
ROOT_COMMIT=$(git rev-list --max-parents=0 HEAD)
git reset --soft "$ROOT_COMMIT"
git commit --amend -m "init: bootstrap ${NEW_PROJECT} from go-template ${TEMPLATE_TAG}
Initialized from ${OLD_MODULE} @ ${TEMPLATE_TAG}.
Module renamed to ${NEW_MODULE}."
info "History squashed → single INIT commit (template: ${TEMPLATE_TAG})"
# Delete all template tags — version history belongs to the template, not the fork.
TAGS=$(git tag)
if [[ -n "$TAGS" ]]; then
echo "$TAGS" | xargs git tag -d
TAG_COUNT=$(echo "$TAGS" | wc -l | tr -d '[:space:]')
info "Deleted ${TAG_COUNT} template tag(s)"
fi
# ── Post-rename suggestions ───────────────────────────────────────────────────
heading "Done"
echo "Module is now: ${BOLD}${NEW_MODULE}${RESET}"
echo
echo "Recommended next steps:"
echo " go mod tidy # sync go.sum after path change"
echo " make build # verify it compiles"
echo " make test # verify tests pass"
echo " git push -u origin main --force # publish new repo"