Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .delphi/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# MINOR something new that existing clones need nothing for
# MAJOR clones need cleanup (MINOR while 0.x): add .delphi/migrations/<version>-<slug>.sh, which
# setup.sh runs once per workspace in each clone (rules in Delphi's CLAUDE.md)
version: 0.7.0
version: 0.8.0
files: # repo-relative paths; a trailing / means everything under it
- .delphi/manifest.yml
- .delphi/.gitattributes
Expand All @@ -18,5 +18,6 @@ files: # repo-relative paths; a trailing / means everythi
- .delphi/plugins.sh
- .delphi/switch.sh
- .delphi/park.sh
- .delphi/names.sh
- .delphi/migrations/
- .github/workflows/delphi.yml
52 changes: 52 additions & 0 deletions .delphi/migrations/0.8.0-dotted-names.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# 0.8.0: a workspace's name is its folders' short names and its own, joined by dots (.delphi/names.sh:
# s.app.argos.argos-dev, branch ws/s.app.argos.argos-dev; was its folder's path without workspaces/,
# software/application-software/argos/argos-dev), and `workspaces:` entries (`- <name>` or `- main`)
# are copied to linked/<name>. Moves what this clone keeps under old names to the new ones, for every
# workspace on main: parked slots (.delphi/parked/<old> to .delphi/parked/<name>), read-only copies
# (linked/ws/<old> to linked/<name>, here and in each parked slot), and version records
# (delphi.<old>.* in git config). Then, if this clone is on ws/<old>, renames that branch ws/<name>
# tracking origin/ws/<name> (sync pushed both to the same commit); a local ws/<name> already there is
# replaced only if this branch has all its commits. Only renames; a name taken at both places is
# reported and left.
set -euo pipefail
git fetch -q origin 2>/dev/null || echo "migrate: could not fetch; using the last fetched" >&2
git rev-parse -q --verify refs/remotes/origin/main >/dev/null ||
{ echo "migrate: no origin/main here; git fetch origin main, then re-run setup" >&2 && exit 1; }
pairs=$(bash .delphi/names.sh refs/remotes/origin/main | awk '{ sub("/workspaces/", "/", $2); print $2 "|" $1 }') # <old>|<name>
move() { # <from> <to>
[ -e "$1" ] || return 0
if [ -e "$2" ]; then echo "migrate: $1 and $2 both exist; left $1 (move what you need by hand)" >&2 && return 0; fi
mkdir -p "$(dirname "$2")" && mv "$1" "$2" && echo "migrate: moved $1 to $2"
}
for p in $pairs; do
old=${p%|*} n=${p#*|}
move ".delphi/parked/$old" ".delphi/parked/$n"
(cd .delphi/parked 2>/dev/null && rmdir -p "${old%/*}" 2>/dev/null) || true # the old name's emptied folders
if git config --get-regexp "^delphi\\.$old\\." >/dev/null; then
if git config "delphi.$n.version" >/dev/null; then git config --remove-section "delphi.$old"
else git config --rename-section "delphi.$old" "delphi.$n"; fi
fi
done
for slot in . $(for p in $pairs; do [ ! -d ".delphi/parked/${p#*|}" ] || echo ".delphi/parked/${p#*|}"; done); do
for p in $pairs; do
old=${p%|*}
move "$slot/linked/ws/$old" "$slot/linked/${p#*|}"
(cd "$slot/linked" 2>/dev/null && rmdir -p "ws/${old%/*}" 2>/dev/null) || true
done
done

cur=$(git branch --show-current)
for p in $pairs; do
n=${p#*|}
[ "$cur" = "ws/${p%|*}" ] || continue
if git rev-parse -q --verify "refs/heads/ws/$n" >/dev/null; then
git merge-base --is-ancestor "ws/$n" HEAD ||
{ echo "migrate: local ws/$n has commits $cur doesn't; merge or delete it (git branch -D ws/$n), then re-run setup" >&2 && exit 1; }
fi
git branch -M "$cur" "ws/$n"
if git rev-parse -q --verify "refs/remotes/origin/ws/$n" >/dev/null; then
git branch -q -u "origin/ws/$n" "ws/$n"
echo "migrate: renamed this branch $cur to ws/$n, tracking origin/ws/$n"
else echo "migrate: renamed this branch $cur to ws/$n; once CI creates origin/ws/$n: git branch -u origin/ws/$n"; fi
done
27 changes: 27 additions & 0 deletions .delphi/names.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# .delphi/names.sh [<rev>]: print `<name> <folder>` for each workspace folder software/**/workspaces/<leaf>/
# in <rev> (default: the working tree here, a main checkout), sorted by folder. A workspace's name is
# the short name of each folder from software/ down to its project folder, then <leaf>, joined by
# dots (software/application-software/argos/workspaces/argos-dev -> s.app.argos.argos-dev; branch
# ws/<name>). A folder's short name is the one line in its `short-name` file, else its own name.
# Prints nothing where there's no software/ (a ws/* branch). Bash 3.2 and Git Bash safe.
set -euo pipefail
rev=${1:-}
folders() {
if [ -n "$rev" ]; then git ls-tree -r --name-only "$rev" -- software
else find software -type f -name workspace.yml 2>/dev/null; fi | sed -nE 's#^(software/(.+/)?workspaces/[^/]+)/workspace\.yml$#\1#p' | LC_ALL=C sort
}
short() { # <folder>: its short name
local s
if [ -n "$rev" ]; then s=$(git show "$rev:$1/short-name" 2>/dev/null || true); else s=$(cat "$1/short-name" 2>/dev/null || true); fi
s=$(printf '%s' "$s" | tr -d '[:space:]')
echo "${s:-${1##*/}}"
}
for f in $(folders); do
n="" p=""
for part in $(echo "${f%/workspaces/*}" | tr / ' '); do
p=${p:+$p/}$part
n="$n$(short "$p")."
done
echo "$n${f##*/} $f"
done
14 changes: 8 additions & 6 deletions .delphi/park.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@
# .delphi/park.sh <prev> <new> <branch-flag>: run by the post-checkout hook .delphi/setup.sh writes,
# from the worktree that checked out. .delphi/ is on every branch, main included, so the hook always
# finds it, the version just checked out. When a checkout moves this clone to another workspace
# (the `name:` in workspace.yml, its place in the tree; none on main), it parks the old workspace's
# (the `name:` in workspace.yml; none on main), it parks the old workspace's
# root folders (the ones .delphi/paths.sh names, .journal/, and those under `park:` in the workspace.yml
# of the branch it leaves) at .delphi/parked/<name>/ (git-ignored by .delphi/.gitignore), and brings
# back everything parked for the new one. A name from before workspaces were named by
# path (argos-dev) is the same workspace as the path ending in it, so moving onto the renamed branch
# moves nothing. Moves are renames, so every path is the same again once its workspace is checked
# back everything parked for the new one. A workspace's dotted name and its old one, its folder's
# path (s.app.argos.argos-dev, software/application-software/argos/argos-dev), are taken to be the
# same workspace when their last parts match, so moving onto the renamed branch moves nothing. Moves are renames, so every path is the same again once its workspace is checked
# out. Runs on bash 3.2 and Git Bash.
set -euo pipefail
[ "${3:-0}" = 1 ] || exit 0
name() { git show "$1:workspace.yml" 2>/dev/null | awk '/^name:/ { print $2; exit }' || true; }
old=$(name "$1") new=$(name "$2")
[ "$old" != "$new" ] || exit 0
case "$old" in */*) ;; ?*) [ "$old" != "${new##*/}" ] || exit 0 ;; esac # renamed by path: still the same workspace
case "$new" in */*) ;; ?*) [ "$new" != "${old##*/}" ] || exit 0 ;; esac
last() { local n=${1##*/}; echo "${n##*.}"; } # a name's last part, after its last / or .
slashed() { case "$1" in */*) echo 1 ;; esac; } # an old name, its folder's path
[ -z "$old" ] || [ -z "$new" ] || [ "$(slashed "$old")" = "$(slashed "$new")" ] || [ "$(last "$old")" != "$(last "$new")" ] ||
exit 0 # its old name and its new one: still the same workspace
top=$(git rev-parse --show-toplevel)
park=$top/.delphi/parked
parks() { # <commit>: the root folders its workspace parks
Expand Down
2 changes: 1 addition & 1 deletion .delphi/paths.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DELPHI_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)
DELPHI_REPOS=$DELPHI_ROOT/repos # bare stores: repos/<repo>
DELPHI_WORKTREES=$DELPHI_ROOT/worktrees # checkouts: worktrees/<repo>/<branch>
DELPHI_REFERENCES=$DELPHI_ROOT/references # read-only dependency sources: references/<name>
DELPHI_LINKED=$DELPHI_ROOT/linked # read-only Delphi copies: linked/<branch>
DELPHI_LINKED=$DELPHI_ROOT/linked # read-only Delphi copies: linked/<workspace>, linked/main
export DELPHI_ROOT DELPHI_REPOS DELPHI_WORKTREES DELPHI_REFERENCES DELPHI_LINKED
[ "${BASH_SOURCE[0]:-}" != "$0" ] || printf 'export DELPHI_ROOT=%q DELPHI_REPOS=%q DELPHI_WORKTREES=%q DELPHI_REFERENCES=%q DELPHI_LINKED=%q\n' \
"$DELPHI_ROOT" "$DELPHI_REPOS" "$DELPHI_WORKTREES" "$DELPHI_REFERENCES" "$DELPHI_LINKED"
23 changes: 13 additions & 10 deletions .delphi/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,32 @@
# out its default branch as the first worktree, worktrees/<name>/<default-branch>. For each repo under
# `references-git:` (read-only dependency sources, `<name>: <git-url> [<ref>]`), checks out a shallow,
# detached copy of <ref> (default: origin's default branch) at references/<name>, refreshed on every
# run, with pushes disabled. Each entry under `workspaces:` (`- ws/<name>` or `- main`, a workspace
# being named by its place in the tree) gets the same kind of copy of that branch of this clone's
# origin (Delphi), at linked/<branch>; to edit one, switch this clone to it (.delphi/switch.sh). Adds the
# run, with pushes disabled. Each entry under `workspaces:` (`- <name>`, a workspace's dotted name
# from .delphi/names.sh, or `- main`) gets the same kind of copy of that branch of this clone's
# origin (Delphi, ws/<name> or main), at linked/<name>; to edit one, switch this clone to it (.delphi/switch.sh). Adds the
# folders park.sh parks and macOS's .DS_Store to this clone's .git/info/exclude, and writes its post-checkout hook, which runs .delphi/park.sh (on
# every branch) as just checked out in the worktree that checked out (unless another hook is there), so switching
# to another workspace or main parks those folders. First
# it runs the .delphi/migrations/<version>-*.sh this workspace hasn't (oldest first; the last one done
# is `git config delphi.<name>.version`, 0.0.0 if unset), stopping at one that fails, and warns if
# is `git config delphi.<name>.version`, else the record under its old name, its folder's path without
# workspaces/, from origin/main; 0.0.0 if neither), stopping at one that fails, and warns if
# .delphi/ isn't exactly the version it says (.delphi/version.sh). Last, it brings this machine's Delphi plugins in
# step with the workspace (.delphi/plugins.sh: installs what it enables, uninstalls local ones it doesn't and what the marketplace dropped). Safe to re-run. Keep it bash 3.2
# (macOS) and Git Bash safe: no bash-4 features, POSIX awk only.
set -euo pipefail
cd "$(dirname "$0")/.."
[ -f workspace.yml ] || { echo "setup: no workspace.yml here (on main?); nothing to set up" >&2 && exit 0; }
self=$(awk '/^name:/ { print $2; exit }' workspace.yml)
case "$self" in "" | [-/]* | */ | *//* | *[!a-z0-9/-]*) echo "setup: workspace.yml needs a valid name:" >&2 && exit 1 ;; esac
case "$self" in "" | [-.]* | *. | *..* | *[!a-z0-9.-]*) echo "setup: workspace.yml needs a valid name:" >&2 && exit 1 ;; esac

older() { # <a> <b>: a is a lower MAJOR.MINOR.PATCH than b
[ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | head -n 1)" = "$1" ]
}
here=$(bash .delphi/version.sh) released=$(grep -v '^#' .delphi/versions | grep . | tail -n 1 | cut -d' ' -f1,2 || true)
[ "$here" = "$released" ] ||
echo "setup: .delphi/ is not Delphi ${released%% *} as released (edited here? change it only through main)" >&2
have=$(git config "delphi.$self.version" || echo 0.0.0)
old=$(bash .delphi/names.sh refs/remotes/origin/main 2>/dev/null | awk -v n="$self" '$1 == n { sub("/workspaces/", "/", $2); print $2 }' || true)
have=$(git config "delphi.$self.version" || { [ -n "$old" ] && git config "delphi.$old.version"; } || echo 0.0.0)
pending=$(for f in .delphi/migrations/*.sh; do [ ! -e "$f" ] || echo "${f##*/}"; done | sort -t. -k1,1n -k2,2n -k3,3n)
while IFS= read -r f; do
v=${f%%-*}
Expand Down Expand Up @@ -66,10 +68,11 @@ readonly_copy() { # <dir> <url> [<ref>]: shallow, detached, push-disabled checko
fi
}

link() { # ws/<name>|main: a read-only copy of that branch of Delphi at linked/<branch>
case "$1" in main | ws/[a-z0-9]*) ;; *) echo "setup: skipping bad workspaces: entry '$1'" >&2 && return ;; esac
case "$1" in "ws/$self") return ;; */ | *//* | *[!a-z0-9/-]*) echo "setup: skipping bad workspaces: entry '$1'" >&2 && return ;; esac
readonly_copy "linked/$1" "$(git remote get-url origin)" "$1"
link() { # <name>|main: a read-only copy of that workspace's branch of Delphi (ws/<name>), or main, at linked/<name>
case "$1" in "$self") return ;; "" | [-.]* | *. | *..* | *[!a-z0-9.-]*)
echo "setup: skipping bad workspaces: entry '$1' (want a workspace's name, like s.app.argos.argos-dev, or main)" >&2 && return ;;
esac
readonly_copy "linked/$1" "$(git remote get-url origin)" "$([ "$1" = main ] && echo main || echo "ws/$1")"
}

while read -r kind name url ref; do
Expand Down
34 changes: 14 additions & 20 deletions .delphi/switch.sh
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
#!/usr/bin/env bash
# .delphi/switch.sh <workspace>|main: move this clone to Delphi's ws/<workspace> (or main). A workspace
# is named by its place in the tree (software/application-software/argos/argos-dev; ws/ in front is
# fine), or by its last part alone (argos-dev) if only one workspace ends in it. Refuses if
# tracked files have changes (they would follow the switch), fetches the branch, switches to it (the
# local branch, else a new one tracking origin), fast-forwards it to origin, then runs the new
# workspace's .delphi/setup.sh (none for main). The post-checkout hook (.delphi/park.sh) parks this workspace's
# repos/, worktrees/, references/ and linked/ and restores the new one's. Runs on bash 3.2 and Git Bash.
# .delphi/switch.sh --list: fetch the ws/* branches, then print main and each workspace's name, one a
# line, `* ` before this clone's and two spaces before the others (the delphi mod's /ws-delphi pane reads it).
# is named by .delphi/names.sh (s.app.argos.argos-dev; ws/ in front is fine). Refuses if tracked files have changes (they would follow the
# switch), fetches the branch, switches to it (the local branch, else a new one tracking origin),
# fast-forwards it to origin, then runs the new workspace's .delphi/setup.sh (none for main). The
# post-checkout hook (.delphi/park.sh) parks this workspace's repos/, worktrees/, references/ and
# linked/ and restores the new one's. Runs on bash 3.2 and Git Bash.
# .delphi/switch.sh --list: fetch main and the ws/* branches, then print main and each workspace on
# main that has its branch, one a line, as `<name> <its project folder>`, `* ` before this clone's
# and two spaces before the others (the delphi mod's /ws-delphi pane reads it).
set -euo pipefail
{ # one block, so bash has read all of it before the switch replaces this file
cd "$(dirname "$0")/.."
if [ "${1:-}" = --list ]; then
git fetch -q --prune origin '+refs/heads/ws/*:refs/remotes/origin/ws/*' ||
echo "switch: could not fetch the ws/* branches; listing the ones last fetched" >&2
{ echo main && git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ws/; } |
awk -v here="$(git branch --show-current)" '$0 == "main" || /^ws\/.*\// { # not ws/<leaf>: an old name
n = $0; sub(/^ws\//, "", n); print ($0 == here ? "* " : " ") n }'
git fetch -q --prune origin '+refs/heads/main:refs/remotes/origin/main' '+refs/heads/ws/*:refs/remotes/origin/ws/*' ||
echo "switch: could not fetch main and the ws/* branches; listing the ones last fetched" >&2
{ echo main && bash .delphi/names.sh refs/remotes/origin/main | while read -r n f; do
! git rev-parse -q --verify "refs/remotes/origin/ws/$n" >/dev/null || echo "$n ${f%/workspaces/*}"
done; } | awk -v here="$(git branch --show-current)" '{ print (($1 == "main" ? "main" : "ws/" $1) == here ? "* " : " ") $0 }'
exit
fi
name=${1:?usage: switch.sh <workspace>|main}
name=${name#ws/} ref=ws/${name#ws/}
case "$name" in main) ref=main ;; "" | [-/]* | */ | *//* | *[!a-z0-9/-]*) echo "switch: bad workspace name '$1'" >&2 && exit 1 ;; esac
if [ "$ref" != main ] && [ "$name" = "${name##*/}" ]; then # its last part alone: find the one workspace it names
git fetch -q origin '+refs/heads/ws/*:refs/remotes/origin/ws/*' || { echo "switch: could not fetch the ws/* branches" >&2 && exit 1; }
ref=$(git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ws/ | grep "^ws/.*/$name\$" || true) # not ws/<name>: an old name
[ -n "$ref" ] && [ "$(echo "$ref" | wc -l)" -eq 1 ] ||
{ echo "switch: no one workspace ends in '$name' (found: $(echo "${ref:-none}" | tr '\n' ' ')); give its full name" >&2 && exit 1; }
fi
case "$name" in main) ref=main ;; "" | [-.]* | *. | *..* | *[!a-z0-9.-]*) echo "switch: bad workspace name '$1'" >&2 && exit 1 ;; esac
git diff --quiet HEAD -- || { echo "switch: commit or set aside your changes first (git status)" >&2 && exit 1; }
git fetch -q origin "+refs/heads/$ref:refs/remotes/origin/$ref" || { echo "switch: could not fetch $ref" >&2 && exit 1; }
git switch -q "$ref"
Expand Down
1 change: 1 addition & 0 deletions .delphi/versions
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
0.5.1 3fd96f222b77 the delphi mod's command is /ws-delphi (was /ws); switch.sh's comment says so
0.6.0 c65ffc578bfa setup.sh (.delphi/plugins.sh) makes each clone's @delphi plugins what its workspace enables (installs them, uninstalls local ones it doesn't) and uninstalls ones the marketplace dropped, instead of warning; plugin changes need no migration
0.7.0 84df5ce802db park.sh also parks .journal/ and the root folders under a workspace's park: in workspace.yml (each workspace its own), restores everything parked for a workspace, and skips bad entries; setup.sh excludes them
0.8.0 05d4c954a946 workspaces are named by their folders' short names and leaf, joined by dots (.delphi/names.sh, short-name files; ws/s.app.argos.argos-dev, was ws/software/…/argos-dev); workspaces: entries are - <name> | - main, copied to linked/<name>; sync carries ws/<old> over once (migration)
Loading
Loading