fuzzyworld

fuzzyworld-tools / commit

Add fw repoint: move working copies' origin from GitLab to DreamHost; v1.0.5

b75ceb4ee8ab699d1e18121963e4bafbc59d968a · Pavel Safronov · 2026-10-26 20:33:36 -0700 · parents a16147a424a4

Add fw repoint: move working copies' origin from GitLab to DreamHost; v1.0.5

For a second machine: renames origin to gitlab, adds origin on dh: (or read-only https on code.fuzzyworld.net with --https), fetches and retargets tracking. Only remotes change; a fingerprint of uncommitted work, HEAD and stashes is compared before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
 README.md      |  1 +
 bin/fw         |  3 +++
 lib/repoint.sh | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 package.json   |  2 +-
 4 files changed, 71 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index b8f3d50..4558830 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ On the laptop, `bin/fw` runs `deploy` locally and forwards repo commands to Drea
 | Command | What it does |
 |---|---|
 | `fw deploy [--ref B] [--here] [--dry-run]` | build and publish this repo's app (above) |
+| `fw repoint [DIR] [--https] [--dry-run]` | for each repo in DIR whose `origin` is on gitlab.com: rename it `gitlab`, add `origin` → `dh:git/<repo>.git` (or read-only `https://code.fuzzyworld.net/<repo>.git` with `--https`, public repos only), fetch, and retarget branch tracking. Changes only remotes; checks that uncommitted work, HEAD and stashes are unchanged |
 | `fw list` | every repo, its visibility and last commit |
 | `fw new <name> [--public] [--description TEXT]` | create `~/git/<name>.git` on DreamHost; **private unless `--public`** |
 | `fw public <name>` | publish the source on code.fuzzyworld.net right away |
diff --git a/bin/fw b/bin/fw
index 6ec609d..3815490 100755
--- a/bin/fw
+++ b/bin/fw
@@ -1,6 +1,7 @@
 #!/bin/bash
 # fw (laptop side).
 #   fw deploy [--ref <branch>] [--here] [--dry-run]   build and publish this repo's app (see lib/deploy.mjs)
+#   fw repoint [DIR] [--https] [--dry-run]            move working copies' origin from GitLab to DreamHost
 #   fw list | new <name> [--public] [--description TEXT] | public <name> | private <name>
 #   fw describe <name> TEXT | publish <name> | publish-all      (run on DreamHost over ssh)
 set -euo pipefail
@@ -11,6 +12,8 @@ HERE=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)
 case "${1:-help}" in
   deploy)
     shift; exec node "$HERE/lib/deploy.mjs" "$@" ;;
+  repoint)
+    shift; exec bash "$HERE/lib/repoint.sh" "$@" ;;
   list|new|public|private|describe|publish|publish-all|help|-h|--help)
     # printf %q keeps arguments (e.g. a description with spaces) intact through ssh.
     exec ssh "$HOST" "~/bin/fw $(printf '%q ' "$@")" ;;
diff --git a/lib/repoint.sh b/lib/repoint.sh
new file mode 100755
index 0000000..687462f
--- /dev/null
+++ b/lib/repoint.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+# fw repoint: move working copies from GitLab to DreamHost. Changes only remotes and tracking.
+#
+#   fw repoint [DIR] [--https] [--dry-run]
+#
+# For every git repo directly inside DIR (default: current directory) whose `origin` is
+# gitlab.com/PavelSafronov/<repo> (or the old gitlab.com/fuzzy-rp/<repo>):
+#   origin -> renamed `gitlab`;  new origin -> dh:git/<repo>.git   (needs the `dh` ssh alias)
+#   with --https: new origin -> https://code.fuzzyworld.net/<repo>.git (read-only; public repos only)
+# then fetches, and points each branch that tracked a GitLab branch at the same branch on origin.
+# Uncommitted work, HEAD and stashes are fingerprinted before and after; a difference is reported.
+set -u
+DIR=.; HTTPS=false; DRY=false
+for a in "$@"; do
+  case "$a" in
+    --https) HTTPS=true ;;
+    --dry-run) DRY=true ;;
+    -*) echo "fw repoint: unknown option $a" >&2; exit 2 ;;
+    *) DIR=$a ;;
+  esac
+done
+
+fp() { { git -C "$1" status --porcelain=v1 -uall; git -C "$1" rev-parse HEAD; git -C "$1" symbolic-ref -q HEAD; git -C "$1" stash list; } 2>/dev/null | shasum | cut -c1-12; }
+
+ok=0; skipped=0; failed=0
+for d in "$DIR"/*/; do
+  d=${d%/}
+  [ -d "$d/.git" ] || continue
+  url=$(git -C "$d" remote get-url origin 2>/dev/null) || continue
+  repo=$(printf '%s' "$url" | sed -nE 's#^.*gitlab\.com[:/](PavelSafronov|fuzzy-rp)/([^/]+)/?$#\2#p' | sed 's/\.git$//')
+  [ -n "$repo" ] || continue
+  name=$(basename "$d")
+  if $HTTPS; then
+    new="https://code.fuzzyworld.net/$repo.git"
+    if ! git ls-remote --exit-code "$new" HEAD >/dev/null 2>&1; then
+      echo "skip  $name: $repo is not public on code.fuzzyworld.net"; skipped=$((skipped+1)); continue
+    fi
+  else
+    new="dh:git/$repo.git"
+  fi
+  if git -C "$d" remote get-url gitlab >/dev/null 2>&1; then
+    echo "skip  $name: already has a remote named gitlab"; skipped=$((skipped+1)); continue
+  fi
+  if $DRY; then echo "would $name: origin $url -> $new"; ok=$((ok+1)); continue; fi
+
+  before=$(fp "$d")
+  git -C "$d" remote rename origin gitlab
+  git -C "$d" remote add origin "$new"
+  if ! git -C "$d" fetch --quiet origin; then
+    echo "FAIL  $name: fetch from $new failed (remotes changed; run 'git -C $d fetch origin' to retry)"; failed=$((failed+1)); continue
+  fi
+  moved=""
+  for b in $(git -C "$d" for-each-ref --format='%(refname:short)' refs/heads); do
+    [ "$(git -C "$d" config --get "branch.$b.remote")" = gitlab ] || continue
+    m=$(git -C "$d" config --get "branch.$b.merge" | sed 's#^refs/heads/##')
+    git -C "$d" rev-parse -q --verify "refs/remotes/origin/$m" >/dev/null &&
+      git -C "$d" branch --quiet --set-upstream-to="origin/$m" "$b" && moved="$moved $b"
+  done
+  after=$(fp "$d")
+  if [ "$before" = "$after" ]; then
+    echo "ok    $name -> $new (tracking:${moved:- none})"; ok=$((ok+1))
+  else
+    echo "WARN  $name: working copy fingerprint changed; check it"; failed=$((failed+1))
+  fi
+done
+echo "repointed $ok, skipped $skipped, problems $failed$($DRY && echo ' (dry run: nothing changed)')"
diff --git a/package.json b/package.json
index 6c29f66..4749b64 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "fuzzyworld-tools",
-  "version": "1.0.4",
+  "version": "1.0.5",
   "description": "Tooling for code.fuzzyworld.net and apps.fuzzyworld.net: fw command, deploy, vite plugin.",
   "type": "module",
   "bin": {