mensa 0.9.5 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.devcontainer/devcontainer-lock.json +4 -4
- data/.devcontainer/devcontainer.json +16 -7
- data/.devcontainer/docker-compose.yml +74 -13
- data/.devcontainer/env.conf +29 -0
- data/.devcontainer/postStart.sh +7 -2
- data/.gitignore +2 -1
- data/.shipkit.yml +1 -1
- data/.worktreeinclude +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +56 -4
- data/app/components/mensa/add_filter/component.css +9 -0
- data/app/components/mensa/add_filter/component_controller.js +55 -2
- data/app/components/mensa/filter_pill/component.rb +1 -7
- data/app/components/mensa/filter_pill_list/component_controller.js +1 -6
- data/app/tables/mensa/base.rb +1 -1
- data/app/tables/mensa/config/table_dsl.rb +23 -0
- data/app/tables/mensa/config/view_dsl.rb +19 -0
- data/app/tables/mensa/filter.rb +51 -5
- data/app/views/mensa/tables/filters/show.turbo_stream.erb +15 -8
- data/bin/de +868 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/nl.yml +5 -0
- data/lib/mensa/verifier.rb +2 -0
- data/lib/mensa/version.rb +1 -1
- data/mise.toml +1 -3
- metadata +4 -2
- data/.devcontainer/initialize.sh +0 -22
data/bin/de
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# de: development environments for this repository. Run without arguments
|
|
3
|
+
# for help. Repository specific settings live in .devcontainer/env.conf, this
|
|
4
|
+
# script is generic: it comes from the de repository, whose `de init` copies
|
|
5
|
+
# it into a repository as bin/de, the same everywhere. Run from the de
|
|
6
|
+
# repository itself (e.g. linked onto the PATH as `de`), it sets repositories
|
|
7
|
+
# up and hands every other command to the bin/de of the current checkout.
|
|
8
|
+
#
|
|
9
|
+
# Every checkout (main checkout or git worktree) gets its own environment: a
|
|
10
|
+
# Docker Compose project named after the branch, labelled with
|
|
11
|
+
#
|
|
12
|
+
# dev.env.name the environment (and compose project) name
|
|
13
|
+
# dev.env.kind persistent | ephemeral
|
|
14
|
+
# dev.env.branch the branch, empty for a detached HEAD
|
|
15
|
+
# dev.env.path the checkout on the host
|
|
16
|
+
# dev.env.gitdir the repository's common .git directory on the host
|
|
17
|
+
#
|
|
18
|
+
# Cleanup selects resources by these labels, never by name.
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# Through symlinks, e.g. ~/.local/bin/de to the de repository's bin/de
|
|
22
|
+
DE_ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd -P)"
|
|
23
|
+
# Set when running from the de repository rather than a repository's copy
|
|
24
|
+
DE_TEMPLATE=""
|
|
25
|
+
DE_DOCS="https://github.com/tdegrunt/de#readme"
|
|
26
|
+
# Your own tools for every environment, see env_write_host_feature
|
|
27
|
+
DE_CONFIG="${DE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/de/config.json}"
|
|
28
|
+
# Where the folders "home" in DE_CONFIG lists are kept, shared by every environment
|
|
29
|
+
DE_HOME_STATE="${XDG_STATE_HOME:-$HOME/.local/state}/de/home"
|
|
30
|
+
# `de exec` passes a SELENIUM_URL from the caller on, see env.conf. Unset it
|
|
31
|
+
# so env.conf's value isn't exported to the commands this script runs.
|
|
32
|
+
ENV_SELENIUM_URL_GIVEN="${SELENIUM_URL:-}"
|
|
33
|
+
unset SELENIUM_URL
|
|
34
|
+
UP_COMMAND=()
|
|
35
|
+
PRIMARY_BRANCHES=(main master)
|
|
36
|
+
if [ -f "$DE_ROOT/.devcontainer/env.conf" ]; then
|
|
37
|
+
# shellcheck source=../.devcontainer/env.conf
|
|
38
|
+
source "$DE_ROOT/.devcontainer/env.conf"
|
|
39
|
+
elif [ -d "$DE_ROOT/template" ]; then
|
|
40
|
+
DE_TEMPLATE="$DE_ROOT/template"
|
|
41
|
+
else
|
|
42
|
+
printf 'error: %s has neither .devcontainer/env.conf nor template/\n' "$DE_ROOT" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
usage() {
|
|
47
|
+
cat <<EOF
|
|
48
|
+
Usage: bin/de <command> [arguments]
|
|
49
|
+
|
|
50
|
+
Every checkout of this repository, the main one and each git worktree, gets
|
|
51
|
+
its own development environment: a devcontainer with its own services,
|
|
52
|
+
volumes and network. Environments for ${PRIMARY_BRANCHES[*]} ${PERSISTENT_BRANCHES[*]} are
|
|
53
|
+
persistent and keep their data, all others are ephemeral. A primary branch
|
|
54
|
+
(${PRIMARY_BRANCHES[*]}) gets one named $ENV_REPO, the others $ENV_REPO-<branch>.
|
|
55
|
+
|
|
56
|
+
Commands:
|
|
57
|
+
new <branch> [folder] [--from <ref>] [--no-trust]
|
|
58
|
+
Create a worktree for a branch (next to the main checkout
|
|
59
|
+
as $ENV_REPO-<branch> unless a folder is given) and start
|
|
60
|
+
its environment. New branches start from the default
|
|
61
|
+
branch, or from --from. --no-trust as for up.
|
|
62
|
+
up [--rebuild] [--no-trust]
|
|
63
|
+
Create or start this checkout's environment and prepare
|
|
64
|
+
its databases. --rebuild recreates the containers after
|
|
65
|
+
changing .devcontainer/, keeping the data. Trusts the
|
|
66
|
+
checkout's mise config on the host, unless --no-trust.
|
|
67
|
+
exec <command...> Run a command in this checkout's devcontainer, in the same
|
|
68
|
+
directory relative to the checkout.
|
|
69
|
+
status Show the environment's containers, health and URLs.
|
|
70
|
+
code [<checkout>] Open a checkout in VS Code inside its devcontainer,
|
|
71
|
+
starting it first if needed. <checkout> is a branch,
|
|
72
|
+
environment name, worktree folder name or a path in
|
|
73
|
+
it, default this one.
|
|
74
|
+
down [--destroy-persistent]
|
|
75
|
+
Remove an ephemeral environment with its data. A
|
|
76
|
+
persistent one is only stopped, unless
|
|
77
|
+
--destroy-persistent is given and its name confirmed.
|
|
78
|
+
prune [--yes] Remove ephemeral environments whose worktree or branch
|
|
79
|
+
is gone. --yes skips the confirmation.
|
|
80
|
+
help Show this help.
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
bin/de new fix-login
|
|
84
|
+
bin/de exec bin/rails test
|
|
85
|
+
SELENIUM_URL=http://host.docker.internal:4444/ HEADLESS=0 bin/de exec bin/rails test:system
|
|
86
|
+
bin/de code main
|
|
87
|
+
|
|
88
|
+
Settings: .devcontainer/env.conf, your own tools: $DE_CONFIG,
|
|
89
|
+
docs: $DE_DOCS. To update bin/de, run \`de init\` from the de repository.
|
|
90
|
+
EOF
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Help when running from the de repository
|
|
94
|
+
usage_source() {
|
|
95
|
+
cat <<EOF
|
|
96
|
+
Usage: de <command> [arguments]
|
|
97
|
+
|
|
98
|
+
Development environments, one devcontainer per checkout. This is the de
|
|
99
|
+
repository's copy ($DE_ROOT): it sets repositories up, and hands every other
|
|
100
|
+
command to the bin/de of the checkout you're in.
|
|
101
|
+
|
|
102
|
+
Commands:
|
|
103
|
+
init [<repository>] [--force]
|
|
104
|
+
Set up development environments in a repository (default
|
|
105
|
+
the current one), or update its bin/de. Repository
|
|
106
|
+
specific files that already exist there are kept, unless
|
|
107
|
+
--force is given.
|
|
108
|
+
help Show this help.
|
|
109
|
+
<command> Any bin/de command, run by the current checkout's bin/de,
|
|
110
|
+
e.g. de up, de exec bin/rails test. See bin/de help there.
|
|
111
|
+
|
|
112
|
+
Your own tools for every environment: $DE_CONFIG
|
|
113
|
+
EOF
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# --- Output -------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
log() { printf '\033[1;34m==>\033[0m %s\n' "$*" >&2; }
|
|
119
|
+
warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$*" >&2; }
|
|
120
|
+
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
121
|
+
|
|
122
|
+
# Prints a command, then runs it
|
|
123
|
+
run() {
|
|
124
|
+
printf '\033[2m+' >&2
|
|
125
|
+
printf ' %q' "$@" >&2
|
|
126
|
+
printf '\033[0m\n' >&2
|
|
127
|
+
"$@"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
devcontainer_cli() {
|
|
131
|
+
if command -v devcontainer >/dev/null 2>&1; then
|
|
132
|
+
run devcontainer "$@"
|
|
133
|
+
else
|
|
134
|
+
run npx --yes "@devcontainers/cli@$DEVCONTAINER_CLI_VERSION" "$@"
|
|
135
|
+
fi
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# --- Environment identity -----------------------------------------------
|
|
139
|
+
|
|
140
|
+
# Lowercases and replaces everything but [a-z0-9-] with dashes
|
|
141
|
+
env_sanitize() {
|
|
142
|
+
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | tr -s '-' | sed 's/^-//; s/-$//'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
env_is_primary_branch() {
|
|
146
|
+
local branch
|
|
147
|
+
for branch in "${PRIMARY_BRANCHES[@]}"; do
|
|
148
|
+
[ "$1" = "$branch" ] && return 0
|
|
149
|
+
done
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Primary branches are always persistent
|
|
154
|
+
env_is_persistent_branch() {
|
|
155
|
+
local branch
|
|
156
|
+
env_is_primary_branch "$1" && return 0
|
|
157
|
+
for branch in "${PERSISTENT_BRANCHES[@]}"; do
|
|
158
|
+
[ "$1" = "$branch" ] && return 0
|
|
159
|
+
done
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Prints the environment name for a checkout directory
|
|
164
|
+
env_name_for() {
|
|
165
|
+
local dir="$1" branch name
|
|
166
|
+
branch="$(git -C "$dir" symbolic-ref --short -q HEAD || true)"
|
|
167
|
+
if [ -n "$branch" ] && env_is_primary_branch "$branch"; then
|
|
168
|
+
name="$(env_sanitize "$ENV_REPO")"
|
|
169
|
+
elif [ -n "$branch" ]; then
|
|
170
|
+
name="$(env_sanitize "$ENV_REPO-$branch")"
|
|
171
|
+
else
|
|
172
|
+
name="$(env_sanitize "$ENV_REPO-detached-$(basename "$dir")")"
|
|
173
|
+
fi
|
|
174
|
+
# Compose project names and OrbStack domain labels max out at 63 characters
|
|
175
|
+
if [ "${#name}" -gt 63 ]; then
|
|
176
|
+
name="${name:0:54}-$(printf '%s' "$name" | shasum | cut -c1-8)"
|
|
177
|
+
fi
|
|
178
|
+
printf '%s' "$name"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Sets the ENV_* variables describing the checkout that contains $1
|
|
182
|
+
env_init() {
|
|
183
|
+
local dir="${1:-$PWD}" common
|
|
184
|
+
|
|
185
|
+
ENV_PATH="$(cd "$(git -C "$dir" rev-parse --show-toplevel)" && pwd -P)" ||
|
|
186
|
+
die "$dir is not inside a git checkout"
|
|
187
|
+
common="$(git -C "$ENV_PATH" rev-parse --git-common-dir)"
|
|
188
|
+
ENV_GIT_COMMON_DIR="$(cd "$ENV_PATH" && cd "$common" && pwd -P)"
|
|
189
|
+
ENV_MAIN_CHECKOUT="$(dirname "$ENV_GIT_COMMON_DIR")"
|
|
190
|
+
ENV_BRANCH="$(git -C "$ENV_PATH" symbolic-ref --short -q HEAD || true)"
|
|
191
|
+
ENV_NAME="$(env_name_for "$ENV_PATH")"
|
|
192
|
+
|
|
193
|
+
if [ -n "$ENV_BRANCH" ] && env_is_persistent_branch "$ENV_BRANCH"; then
|
|
194
|
+
ENV_KIND=persistent
|
|
195
|
+
ENV_RESTART=unless-stopped
|
|
196
|
+
else
|
|
197
|
+
ENV_KIND=ephemeral
|
|
198
|
+
ENV_RESTART=no
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# Inside the container, both the checkout and the common .git directory are
|
|
202
|
+
# mounted at /workspaces/<host path>, so the relative paths in a worktree's
|
|
203
|
+
# .git file (worktree.useRelativePaths) resolve the same way.
|
|
204
|
+
# devcontainer.json uses the same path as its workspaceFolder.
|
|
205
|
+
ENV_PATH_IN_CONTAINER="/workspaces$ENV_PATH"
|
|
206
|
+
ENV_GIT_COMMON_DIR_IN_CONTAINER="/workspaces$ENV_GIT_COMMON_DIR"
|
|
207
|
+
# A shorter, stable path to the checkout, for interactive use
|
|
208
|
+
ENV_WORKSPACE_LINK="/workspaces/$ENV_REPO"
|
|
209
|
+
|
|
210
|
+
ENV_DOTENV="$ENV_PATH/.devcontainer/.env"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Prints where a browser driven through Selenium at $1 reaches the app's test
|
|
214
|
+
# server: by service name from the environment's own selenium container, and
|
|
215
|
+
# by OrbStack domain from a browser on the Mac
|
|
216
|
+
env_capybara_app_host() {
|
|
217
|
+
case "$1" in
|
|
218
|
+
*//selenium:*) echo "http://app:3100" ;;
|
|
219
|
+
*) echo "http://app.$ENV_NAME.orb.local:3100" ;;
|
|
220
|
+
esac
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Writes .devcontainer/.env, which docker compose reads for the project name,
|
|
224
|
+
# labels and mounts
|
|
225
|
+
env_write_dotenv() {
|
|
226
|
+
cat > "$ENV_DOTENV" <<EOF
|
|
227
|
+
# Written by bin/de, don't edit
|
|
228
|
+
COMPOSE_PROJECT_NAME=$ENV_NAME
|
|
229
|
+
ENV_NAME=$ENV_NAME
|
|
230
|
+
ENV_KIND=$ENV_KIND
|
|
231
|
+
ENV_BRANCH=$ENV_BRANCH
|
|
232
|
+
ENV_PATH=$ENV_PATH
|
|
233
|
+
ENV_GIT_COMMON_DIR=$ENV_GIT_COMMON_DIR
|
|
234
|
+
ENV_PATH_IN_CONTAINER=$ENV_PATH_IN_CONTAINER
|
|
235
|
+
ENV_GIT_COMMON_DIR_IN_CONTAINER=$ENV_GIT_COMMON_DIR_IN_CONTAINER
|
|
236
|
+
ENV_WORKSPACE_LINK=$ENV_WORKSPACE_LINK
|
|
237
|
+
ENV_RESTART=$ENV_RESTART
|
|
238
|
+
ENV_GIT_CONFIG_DIR=$HOME/.config/git
|
|
239
|
+
SELENIUM_URL=$SELENIUM_URL
|
|
240
|
+
CAPYBARA_APP_HOST=$(env_capybara_app_host "$SELENIUM_URL")
|
|
241
|
+
EOF
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# --- Docker resources ---------------------------------------------------
|
|
245
|
+
|
|
246
|
+
# Runs docker compose for this checkout's project
|
|
247
|
+
env_compose() {
|
|
248
|
+
run docker compose --project-directory "$ENV_PATH/.devcontainer" \
|
|
249
|
+
-f "$ENV_PATH/.devcontainer/docker-compose.yml" \
|
|
250
|
+
--env-file "$ENV_DOTENV" -p "$ENV_NAME" "$@"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Succeeds when the app container of an environment is running
|
|
254
|
+
env_running() {
|
|
255
|
+
[ -n "$(docker container ls -q --filter "label=dev.env.name=$1" --filter label=com.docker.compose.service=app)" ]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Prints the value of a label on any container, volume or network of an
|
|
259
|
+
# environment
|
|
260
|
+
env_label() {
|
|
261
|
+
local name="$1" label="$2" kind values
|
|
262
|
+
for kind in container volume network; do
|
|
263
|
+
values="$(docker "$kind" ls $([ "$kind" = container ] && echo -a) --filter "label=dev.env.name=$name" --format "{{.Label \"$label\"}}")"
|
|
264
|
+
if [ -n "$values" ]; then
|
|
265
|
+
head -n1 <<<"$values"
|
|
266
|
+
return
|
|
267
|
+
fi
|
|
268
|
+
done
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Prints the names of this repository's environments of a kind, one per line
|
|
272
|
+
env_list() {
|
|
273
|
+
local kind_filter="label=dev.env.kind=$1" repo_filter="label=dev.env.gitdir=$ENV_GIT_COMMON_DIR"
|
|
274
|
+
{
|
|
275
|
+
docker container ls -a --filter "$kind_filter" --filter "$repo_filter" --format '{{.Label "dev.env.name"}}'
|
|
276
|
+
docker volume ls --filter "$kind_filter" --filter "$repo_filter" --format '{{.Label "dev.env.name"}}'
|
|
277
|
+
docker network ls --filter "$kind_filter" --filter "$repo_filter" --format '{{.Label "dev.env.name"}}'
|
|
278
|
+
} | sort -u | sed '/^$/d'
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Succeeds when any resource of the environment is labelled persistent
|
|
282
|
+
env_is_persistent() {
|
|
283
|
+
[ -n "$(docker container ls -aq --filter "label=dev.env.name=$1" --filter label=dev.env.kind=persistent)$(docker volume ls -q --filter "label=dev.env.name=$1" --filter label=dev.env.kind=persistent)$(docker network ls -q --filter "label=dev.env.name=$1" --filter label=dev.env.kind=persistent)" ]
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Removes every container, volume and network of an environment. Resources are
|
|
287
|
+
# found by the dev.env.name label, plus the compose project label for the ones
|
|
288
|
+
# devcontainer features add without our labels (the docker-in-docker volume).
|
|
289
|
+
env_destroy() {
|
|
290
|
+
local name="$1" ids filter
|
|
291
|
+
for filter in "label=dev.env.name=$name" "label=com.docker.compose.project=$name"; do
|
|
292
|
+
ids="$(docker container ls -aq --filter "$filter")"
|
|
293
|
+
[ -z "$ids" ] || run docker container rm --force --volumes $ids >/dev/null
|
|
294
|
+
done
|
|
295
|
+
for filter in "label=dev.env.name=$name" "label=com.docker.compose.project=$name"; do
|
|
296
|
+
ids="$(docker volume ls -q --filter "$filter")"
|
|
297
|
+
[ -z "$ids" ] || run docker volume rm --force $ids >/dev/null
|
|
298
|
+
ids="$(docker network ls -q --filter "$filter")"
|
|
299
|
+
[ -z "$ids" ] || run docker network rm $ids >/dev/null
|
|
300
|
+
done
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Prints "<service> <health>" for every running container of an environment,
|
|
304
|
+
# where health is the healthcheck status, or "running" without a healthcheck
|
|
305
|
+
env_health() {
|
|
306
|
+
local ids
|
|
307
|
+
ids="$(docker container ls -q --filter "label=dev.env.name=$1")"
|
|
308
|
+
[ -z "$ids" ] && return
|
|
309
|
+
# shellcheck disable=SC2086
|
|
310
|
+
docker container inspect $ids --format '{{ index .Config.Labels "com.docker.compose.service" }} {{ if .State.Health }}{{ .State.Health.Status }}{{ else }}{{ .State.Status }}{{ end }}'
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Prints "<path>\t<branch>" for every checkout of the repository
|
|
314
|
+
checkouts() {
|
|
315
|
+
git -C "$ENV_PATH" worktree list --porcelain | awk '
|
|
316
|
+
/^worktree / { if (path != "") print path "\t" branch; path = substr($0, 10); branch = "" }
|
|
317
|
+
/^branch / { branch = substr($0, 8); sub("^refs/heads/", "", branch) }
|
|
318
|
+
END { if (path != "") print path "\t" branch }'
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# --- Commands -----------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
# Writes the local feature devcontainer.json includes as ./de-host, from
|
|
324
|
+
# $DE_CONFIG on the host: your own editor extensions, npm packages and
|
|
325
|
+
# features for every environment of every repository, e.g.
|
|
326
|
+
#
|
|
327
|
+
# {
|
|
328
|
+
# "extensions": ["anthropic.claude-code", "openai.chatgpt"],
|
|
329
|
+
# "npm": ["@anthropic-ai/claude-code", "@openai/codex"],
|
|
330
|
+
# "features": { "ghcr.io/devcontainers/features/python:1": {} },
|
|
331
|
+
# "home": [".claude", ".codex"],
|
|
332
|
+
# "env": { "CLAUDE_CONFIG_DIR": "/home/vscode/.claude" }
|
|
333
|
+
# }
|
|
334
|
+
#
|
|
335
|
+
# "home" lists folders in the container's home directory that every
|
|
336
|
+
# environment shares, kept on the host in $DE_HOME_STATE, e.g. logins. "env"
|
|
337
|
+
# sets environment variables in the container.
|
|
338
|
+
#
|
|
339
|
+
# A feature, so the editor installs its extensions in the container whoever
|
|
340
|
+
# opens it, and its packages are part of the image. Changes apply on the next
|
|
341
|
+
# build, bin/de up --rebuild.
|
|
342
|
+
env_write_host_feature() {
|
|
343
|
+
local dir="$ENV_PATH/.devcontainer/de-host" config='{}' path
|
|
344
|
+
if [ -f "$DE_CONFIG" ]; then
|
|
345
|
+
command -v jq >/dev/null 2>&1 || die "reading $DE_CONFIG needs jq"
|
|
346
|
+
config="$(jq -e 'if type == "object" then . else error("not an object") end' "$DE_CONFIG")" ||
|
|
347
|
+
die "$DE_CONFIG isn't a JSON object"
|
|
348
|
+
fi
|
|
349
|
+
# Docker refuses a missing bind source
|
|
350
|
+
while IFS= read -r path; do
|
|
351
|
+
case "$path" in "" | /* | *..*) die "home in $DE_CONFIG takes paths inside the home directory, not $path" ;; esac
|
|
352
|
+
mkdir -p "$DE_HOME_STATE/$path"
|
|
353
|
+
done < <([ "$config" = '{}' ] || jq -r '.home // [] | .[]' <<<"$config")
|
|
354
|
+
mkdir -p "$dir"
|
|
355
|
+
if [ "$config" = '{}' ]; then
|
|
356
|
+
printf '{ "id": "de-host", "version": "1.0.0", "name": "Nothing from %s" }\n' "$DE_CONFIG" > "$dir/devcontainer-feature.json"
|
|
357
|
+
printf '#!/bin/sh\n# Written by bin/de, %s has nothing to install\n' "$DE_CONFIG" > "$dir/install.sh"
|
|
358
|
+
else
|
|
359
|
+
jq --arg config "$DE_CONFIG" --arg state "$DE_HOME_STATE" '{
|
|
360
|
+
id: "de-host",
|
|
361
|
+
version: "1.0.0",
|
|
362
|
+
name: "Your own tools from \($config), written by bin/de",
|
|
363
|
+
installsAfter: ["ghcr.io/devcontainers/features/node"],
|
|
364
|
+
customizations: { vscode: { extensions: (.extensions // []) } }
|
|
365
|
+
}
|
|
366
|
+
+ (if (.features // {}) != {} then { dependsOn: .features } else {} end)
|
|
367
|
+
+ (if (.home // []) != [] then { mounts: [.home[] | { type: "bind", source: "\($state)/\(.)", target: "/home/vscode/\(.)" }] } else {} end)
|
|
368
|
+
+ (if (.env // {}) != {} then { containerEnv: .env } else {} end)' <<<"$config" > "$dir/devcontainer-feature.json"
|
|
369
|
+
{
|
|
370
|
+
printf '#!/bin/sh\n# Written by bin/de from %s, do not edit\nset -e\n' "$DE_CONFIG"
|
|
371
|
+
jq -r '.npm // [] | if length > 0 then "npm install --global --no-fund --no-audit " + (map(@sh) | join(" ")) else empty end' <<<"$config"
|
|
372
|
+
} > "$dir/install.sh"
|
|
373
|
+
fi
|
|
374
|
+
chmod +x "$dir/install.sh"
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Runs on the host before the containers start, see initializeCommand in
|
|
378
|
+
# devcontainer.json, so opening a checkout from an editor works too
|
|
379
|
+
# The Mac's mise refuses a checkout's config until it's trusted, also for
|
|
380
|
+
# commands this script runs there, like npx. Trust is per path, so every
|
|
381
|
+
# worktree needs it, the container trusts its own copy in postStart.sh.
|
|
382
|
+
env_trust_mise() {
|
|
383
|
+
local file
|
|
384
|
+
command -v mise >/dev/null 2>&1 || return 0
|
|
385
|
+
for file in mise.toml mise.local.toml .mise.toml .mise.local.toml; do
|
|
386
|
+
[ -f "$ENV_PATH/$file" ] || continue
|
|
387
|
+
log "Trusting $file on the host (--no-trust to skip)"
|
|
388
|
+
run mise trust --quiet "$ENV_PATH/$file"
|
|
389
|
+
done
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
cmd_initialize() {
|
|
393
|
+
# docker-compose.yml mounts it, and Docker refuses a missing bind source
|
|
394
|
+
mkdir -p "$HOME/.config/git"
|
|
395
|
+
env_write_dotenv
|
|
396
|
+
env_write_host_feature
|
|
397
|
+
log "Environment $ENV_NAME ($ENV_KIND)"
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# Copies the setup into a repository, from the de repository. bin/de is
|
|
401
|
+
# generic and always updated; the template is repository
|
|
402
|
+
# specific, a starting point that is only copied when missing, or with --force.
|
|
403
|
+
cmd_init() {
|
|
404
|
+
local target="" force=false arg rel src common repo up_command entry
|
|
405
|
+
local usage_line="usage: de init [<repository>] [--force]"
|
|
406
|
+
for arg in "$@"; do
|
|
407
|
+
case "$arg" in
|
|
408
|
+
--force) force=true ;;
|
|
409
|
+
-*) die "$usage_line" ;;
|
|
410
|
+
*) [ -z "$target" ] || die "$usage_line"; target="$arg" ;;
|
|
411
|
+
esac
|
|
412
|
+
done
|
|
413
|
+
target="${target:-$PWD}"
|
|
414
|
+
[ -d "$target" ] || die "$target doesn't exist"
|
|
415
|
+
target="$(cd "$(git -C "$target" rev-parse --show-toplevel 2>/dev/null)" && pwd -P)" ||
|
|
416
|
+
die "$target isn't a git checkout"
|
|
417
|
+
[ "$target" != "$DE_ROOT" ] || die "$target is the de repository itself"
|
|
418
|
+
|
|
419
|
+
common="$(cd "$target" && cd "$(git rev-parse --git-common-dir)" && pwd -P)"
|
|
420
|
+
repo="$(env_sanitize "$(basename "$(dirname "$common")")")"
|
|
421
|
+
if [ -d "$target/test/dummy" ] && [ -x "$target/bin/rails" ]; then
|
|
422
|
+
up_command="env RAILS_ENV=test bin/rails app:db:prepare"
|
|
423
|
+
elif [ -x "$target/bin/rails" ]; then
|
|
424
|
+
up_command="env RAILS_ENV=test bin/rails db:prepare"
|
|
425
|
+
else
|
|
426
|
+
up_command=""
|
|
427
|
+
fi
|
|
428
|
+
log "Setting up development environments in $target ($repo)"
|
|
429
|
+
|
|
430
|
+
# Generic: always the same as in the de repository
|
|
431
|
+
if cmp -s "$DE_ROOT/bin/de" "$target/bin/de"; then
|
|
432
|
+
printf ' %-36s up to date\n' bin/de >&2
|
|
433
|
+
else
|
|
434
|
+
run mkdir -p "$target/bin"
|
|
435
|
+
run cp -p "$DE_ROOT/bin/de" "$target/bin/de"
|
|
436
|
+
fi
|
|
437
|
+
# The docs used to be copied too, they live in the de repository now
|
|
438
|
+
if [ -f "$target/.devcontainer/README.md" ] && grep -q '^`bin/de` manages them' "$target/.devcontainer/README.md"; then
|
|
439
|
+
log "Removing .devcontainer/README.md, the docs are at $DE_DOCS"
|
|
440
|
+
run rm "$target/.devcontainer/README.md"
|
|
441
|
+
fi
|
|
442
|
+
|
|
443
|
+
# Repository specific: services, image, setup steps and settings
|
|
444
|
+
while IFS= read -r rel; do
|
|
445
|
+
rel="${rel#./}"
|
|
446
|
+
if [ -e "$target/$rel" ] && ! $force; then
|
|
447
|
+
printf ' %-36s exists, kept (--force to replace)\n' "$rel" >&2
|
|
448
|
+
continue
|
|
449
|
+
fi
|
|
450
|
+
run mkdir -p "$(dirname "$target/$rel")"
|
|
451
|
+
run cp -p "$DE_TEMPLATE/$rel" "$target/$rel"
|
|
452
|
+
sed -i '' -e "s|@REPO@|$repo|g" -e "s|@UP_COMMAND@|$up_command|g" "$target/$rel"
|
|
453
|
+
done < <(cd "$DE_TEMPLATE" && find . -type f | sort)
|
|
454
|
+
|
|
455
|
+
if ! grep -q '"\./de-host"' "$target/.devcontainer/devcontainer.json"; then
|
|
456
|
+
warn "add \"./de-host\": {} to the features in .devcontainer/devcontainer.json, for your own tools from $DE_CONFIG"
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
# Never commit generated files or local secrets
|
|
460
|
+
for entry in .devcontainer/.env .devcontainer/de-host/ mise.local.toml; do
|
|
461
|
+
# A file inside, as a directory entry like de-host/ only matches what's in it
|
|
462
|
+
git -C "$target" check-ignore -q --no-index -- "${entry%/}$([ "${entry%/}" = "$entry" ] || echo /x)" && continue
|
|
463
|
+
log "Adding $entry to .gitignore"
|
|
464
|
+
# Not onto the last line when the file doesn't end in a newline
|
|
465
|
+
if [ -s "$target/.gitignore" ] && [ -n "$(tail -c1 "$target/.gitignore")" ]; then
|
|
466
|
+
echo >> "$target/.gitignore"
|
|
467
|
+
fi
|
|
468
|
+
printf '%s\n' "$entry" >> "$target/.gitignore"
|
|
469
|
+
done
|
|
470
|
+
|
|
471
|
+
cat <<EOF
|
|
472
|
+
|
|
473
|
+
Done. Review the repository specific files for $repo, then start it:
|
|
474
|
+
|
|
475
|
+
.devcontainer/env.conf ENV_REPO, PERSISTENT_BRANCHES, UP_COMMAND
|
|
476
|
+
.devcontainer/docker-compose.yml services, keep the x-env-labels and mounts
|
|
477
|
+
.devcontainer/postStart.sh installing dependencies, the dev database
|
|
478
|
+
.devcontainer/devcontainer.json features, keep ./de-host and the mounts
|
|
479
|
+
.worktreeinclude ignored files new worktrees need
|
|
480
|
+
orca.yaml Orca's worktree hooks
|
|
481
|
+
|
|
482
|
+
cd $target && bin/de up
|
|
483
|
+
|
|
484
|
+
Run de init there again later to update its bin/de.
|
|
485
|
+
EOF
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
cmd_new() {
|
|
489
|
+
local branch="" folder="" from="" file dest up_args=()
|
|
490
|
+
local usage_line="usage: bin/de new <branch> [folder] [--from <ref>] [--no-trust]"
|
|
491
|
+
while [ "$#" -gt 0 ]; do
|
|
492
|
+
case "$1" in
|
|
493
|
+
--from) [ "$#" -ge 2 ] || die "$usage_line"; from="$2"; shift 2 ;;
|
|
494
|
+
--no-trust) up_args+=(--no-trust); shift ;;
|
|
495
|
+
-*) die "$usage_line" ;;
|
|
496
|
+
*)
|
|
497
|
+
if [ -z "$branch" ]; then branch="$1"
|
|
498
|
+
elif [ -z "$folder" ]; then folder="$1"
|
|
499
|
+
else die "$usage_line"
|
|
500
|
+
fi
|
|
501
|
+
shift
|
|
502
|
+
;;
|
|
503
|
+
esac
|
|
504
|
+
done
|
|
505
|
+
[ -n "$branch" ] || die "$usage_line"
|
|
506
|
+
git check-ref-format --branch "$branch" >/dev/null || die "$branch isn't a valid branch name"
|
|
507
|
+
|
|
508
|
+
[ -n "$folder" ] || folder="$(dirname "$ENV_MAIN_CHECKOUT")/$(env_sanitize "$ENV_REPO-$branch")"
|
|
509
|
+
[ -e "$folder" ] && die "$folder already exists"
|
|
510
|
+
|
|
511
|
+
log "Creating worktree $folder for $branch"
|
|
512
|
+
if git -C "$ENV_PATH" show-ref --verify --quiet "refs/heads/$branch"; then
|
|
513
|
+
run git -C "$ENV_PATH" worktree add "$folder" "$branch"
|
|
514
|
+
elif [ -n "$(git -C "$ENV_PATH" for-each-ref --format='%(refname)' "refs/remotes/*/$branch")" ]; then
|
|
515
|
+
# Creates a local branch tracking the remote one
|
|
516
|
+
run git -C "$ENV_PATH" worktree add "$folder" "$branch"
|
|
517
|
+
else
|
|
518
|
+
if [ -z "$from" ]; then
|
|
519
|
+
from="$(git -C "$ENV_PATH" symbolic-ref --quiet --short refs/remotes/origin/HEAD || echo HEAD)"
|
|
520
|
+
# Prefer the local branch when there is one, e.g. main over origin/main
|
|
521
|
+
git -C "$ENV_PATH" show-ref --verify --quiet "refs/heads/${from#origin/}" && from="${from#origin/}"
|
|
522
|
+
fi
|
|
523
|
+
run git -C "$ENV_PATH" worktree add -b "$branch" "$folder" "$from"
|
|
524
|
+
fi
|
|
525
|
+
folder="$(cd "$folder" && pwd -P)"
|
|
526
|
+
|
|
527
|
+
# Branches that predate the environment setup get it from this checkout,
|
|
528
|
+
# with mise.toml: its SELENIUM_URL would override the environment's
|
|
529
|
+
if [ ! -x "$folder/bin/de" ]; then
|
|
530
|
+
warn "$branch has no environment setup, copying it from $ENV_PATH (uncommitted there)"
|
|
531
|
+
for file in "$ENV_PATH"/.devcontainer/* "$ENV_PATH/bin/de" "$ENV_PATH/mise.toml"; do
|
|
532
|
+
[ -e "$file" ] || continue
|
|
533
|
+
dest="$folder/${file#"$ENV_PATH"/}"
|
|
534
|
+
run mkdir -p "$(dirname "$dest")"
|
|
535
|
+
run cp -p "$file" "$dest"
|
|
536
|
+
done
|
|
537
|
+
fi
|
|
538
|
+
|
|
539
|
+
# A branch's older bin/de may not know --no-trust
|
|
540
|
+
if [ "${#up_args[@]}" -gt 0 ] && ! grep -q -- '--no-trust' "$folder/bin/de"; then
|
|
541
|
+
warn "$branch's bin/de doesn't support --no-trust, it won't trust mise config anyway"
|
|
542
|
+
up_args=()
|
|
543
|
+
fi
|
|
544
|
+
run "$folder/bin/de" up ${up_args[@]+"${up_args[@]}"}
|
|
545
|
+
|
|
546
|
+
cat <<EOF
|
|
547
|
+
|
|
548
|
+
Worktree $folder is ready:
|
|
549
|
+
|
|
550
|
+
cd $folder
|
|
551
|
+
bin/de exec bin/rails test
|
|
552
|
+
bin/de code # open in VS Code
|
|
553
|
+
bin/de down # when done, then: git worktree remove $folder
|
|
554
|
+
EOF
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
cmd_up() {
|
|
558
|
+
local up_args=() trust=true arg other entry health deadline
|
|
559
|
+
for arg in "$@"; do
|
|
560
|
+
case "$arg" in
|
|
561
|
+
--rebuild) up_args=(--remove-existing-container) ;;
|
|
562
|
+
--no-trust) trust=false ;;
|
|
563
|
+
*) die "usage: bin/de up [--rebuild] [--no-trust]" ;;
|
|
564
|
+
esac
|
|
565
|
+
done
|
|
566
|
+
|
|
567
|
+
log "Environment $ENV_NAME ($ENV_KIND, branch ${ENV_BRANCH:-detached}) for $ENV_PATH"
|
|
568
|
+
|
|
569
|
+
# Share nothing: refuse to adopt another checkout's resources
|
|
570
|
+
other="$(env_label "$ENV_NAME" dev.env.path)"
|
|
571
|
+
if [ -n "$other" ] && [ "$other" != "$ENV_PATH" ]; then
|
|
572
|
+
die "$ENV_NAME already belongs to $other, remove that environment first"
|
|
573
|
+
fi
|
|
574
|
+
|
|
575
|
+
# Worktree .git files point to the main repository by relative path, so the
|
|
576
|
+
# link also resolves inside the container
|
|
577
|
+
if [ "$(git -C "$ENV_PATH" config --get worktree.useRelativePaths || true)" != true ]; then
|
|
578
|
+
log "Enabling relative worktree paths"
|
|
579
|
+
run git -C "$ENV_PATH" config worktree.useRelativePaths true
|
|
580
|
+
fi
|
|
581
|
+
if [ "$ENV_PATH" != "$ENV_MAIN_CHECKOUT" ]; then
|
|
582
|
+
run git -C "$ENV_MAIN_CHECKOUT" worktree repair --relative-paths "$ENV_PATH"
|
|
583
|
+
|
|
584
|
+
# Copy (never link) the ignored local files a worktree needs, from the
|
|
585
|
+
# same list other worktree tools use
|
|
586
|
+
if [ -f "$ENV_MAIN_CHECKOUT/.worktreeinclude" ]; then
|
|
587
|
+
while IFS= read -r entry || [ -n "$entry" ]; do
|
|
588
|
+
case "$entry" in "" | "#"*) continue ;; esac
|
|
589
|
+
[ -e "$ENV_MAIN_CHECKOUT/$entry" ] || continue
|
|
590
|
+
[ -e "$ENV_PATH/$entry" ] && continue
|
|
591
|
+
git -C "$ENV_MAIN_CHECKOUT" check-ignore -q -- "$entry" || continue
|
|
592
|
+
log "Copying $entry from the main checkout"
|
|
593
|
+
run mkdir -p "$(dirname "$ENV_PATH/$entry")"
|
|
594
|
+
run cp -pR "$ENV_MAIN_CHECKOUT/$entry" "$ENV_PATH/$entry"
|
|
595
|
+
done < "$ENV_MAIN_CHECKOUT/.worktreeinclude"
|
|
596
|
+
fi
|
|
597
|
+
fi
|
|
598
|
+
|
|
599
|
+
if $trust; then
|
|
600
|
+
env_trust_mise
|
|
601
|
+
fi
|
|
602
|
+
|
|
603
|
+
log "Writing $ENV_DOTENV"
|
|
604
|
+
env_write_dotenv
|
|
605
|
+
env_write_host_feature
|
|
606
|
+
|
|
607
|
+
log "Building and starting the devcontainer"
|
|
608
|
+
devcontainer_cli up --workspace-folder "$ENV_PATH" ${up_args[@]+"${up_args[@]}"}
|
|
609
|
+
|
|
610
|
+
log "Waiting for health checks"
|
|
611
|
+
deadline=$((SECONDS + 300))
|
|
612
|
+
while :; do
|
|
613
|
+
health="$(env_health "$ENV_NAME")"
|
|
614
|
+
if [ -n "$health" ] && ! grep -qvE ' (healthy|running)$' <<<"$health"; then
|
|
615
|
+
break
|
|
616
|
+
fi
|
|
617
|
+
[ "$SECONDS" -lt "$deadline" ] || die "services not healthy after 5 minutes:"$'\n'"$health"
|
|
618
|
+
sleep 2
|
|
619
|
+
done
|
|
620
|
+
sed 's/^/ /' <<<"$health" >&2
|
|
621
|
+
|
|
622
|
+
if [ "${#UP_COMMAND[@]}" -gt 0 ]; then
|
|
623
|
+
log "Running UP_COMMAND from env.conf"
|
|
624
|
+
(cd "$ENV_PATH" && cmd_exec "${UP_COMMAND[@]}")
|
|
625
|
+
fi
|
|
626
|
+
|
|
627
|
+
cmd_status
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
cmd_exec() {
|
|
631
|
+
local cwd dir remote_env=()
|
|
632
|
+
[ "$#" -gt 0 ] || die "usage: bin/de exec <command> [arguments]"
|
|
633
|
+
env_running "$ENV_NAME" || die "environment $ENV_NAME isn't running, start it with bin/de up"
|
|
634
|
+
|
|
635
|
+
cwd="$(pwd -P)"
|
|
636
|
+
case "$cwd/" in
|
|
637
|
+
"$ENV_PATH/"*) dir="$ENV_PATH_IN_CONTAINER${cwd#"$ENV_PATH"}" ;;
|
|
638
|
+
*) dir="$ENV_PATH_IN_CONTAINER" ;;
|
|
639
|
+
esac
|
|
640
|
+
|
|
641
|
+
if [ -n "$ENV_SELENIUM_URL_GIVEN" ]; then
|
|
642
|
+
log "Using Selenium at $ENV_SELENIUM_URL_GIVEN"
|
|
643
|
+
remote_env+=(--remote-env "SELENIUM_URL=$ENV_SELENIUM_URL_GIVEN")
|
|
644
|
+
remote_env+=(--remote-env "CAPYBARA_APP_HOST=$(env_capybara_app_host "$ENV_SELENIUM_URL_GIVEN")")
|
|
645
|
+
fi
|
|
646
|
+
if [ -n "${HEADLESS:-}" ]; then
|
|
647
|
+
remote_env+=(--remote-env "HEADLESS=$HEADLESS")
|
|
648
|
+
fi
|
|
649
|
+
|
|
650
|
+
devcontainer_cli exec --workspace-folder "$ENV_PATH" ${remote_env[@]+"${remote_env[@]}"} \
|
|
651
|
+
bash -c 'cd "$1" && shift && exec "$HOME/.local/bin/mise" exec -- "$@"' de-exec "$dir" "$@"
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
cmd_status() {
|
|
655
|
+
local ids id service port
|
|
656
|
+
[ "$#" -eq 0 ] || die "usage: bin/de status"
|
|
657
|
+
|
|
658
|
+
cat <<EOF
|
|
659
|
+
Environment $ENV_NAME
|
|
660
|
+
Kind $ENV_KIND
|
|
661
|
+
Branch ${ENV_BRANCH:-(detached)}
|
|
662
|
+
Checkout $ENV_PATH
|
|
663
|
+
In container $ENV_WORKSPACE_LINK
|
|
664
|
+
|
|
665
|
+
EOF
|
|
666
|
+
|
|
667
|
+
log "Containers"
|
|
668
|
+
run docker container ls -a --filter "label=dev.env.name=$ENV_NAME" \
|
|
669
|
+
--format 'table {{.Label "com.docker.compose.service"}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'
|
|
670
|
+
|
|
671
|
+
ids="$(docker container ls -q --filter "label=dev.env.name=$ENV_NAME")"
|
|
672
|
+
if [ -n "$ids" ]; then
|
|
673
|
+
echo
|
|
674
|
+
log "Reachable from the Mac (OrbStack domains)"
|
|
675
|
+
for id in $ids; do
|
|
676
|
+
service="$(docker container inspect "$id" --format '{{ index .Config.Labels "com.docker.compose.service" }}')"
|
|
677
|
+
for port in $(docker container inspect "$id" --format '{{ range $p, $_ := .Config.ExposedPorts }}{{ $p }} {{ end }}'); do
|
|
678
|
+
printf ' %-10s %s.%s.orb.local:%s\n' "$service" "$service" "$ENV_NAME" "${port%/*}"
|
|
679
|
+
done
|
|
680
|
+
# Only when a host port was published, Docker picks the number
|
|
681
|
+
docker port "$id" | sed "s/^/ $service published /"
|
|
682
|
+
done
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
echo
|
|
686
|
+
log "Volumes"
|
|
687
|
+
run docker volume ls --filter "label=com.docker.compose.project=$ENV_NAME"
|
|
688
|
+
echo
|
|
689
|
+
log "Networks"
|
|
690
|
+
run docker network ls --filter "label=dev.env.name=$ENV_NAME"
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
# Runs `code` (CODE to override, e.g. CODE=cursor) with the dev container URI
|
|
694
|
+
# itself: vscli resolves ${localWorkspaceFolder} in devcontainer.json to
|
|
695
|
+
# /workspaces/<folder name> instead of the host path.
|
|
696
|
+
cmd_code() {
|
|
697
|
+
local target="$ENV_PATH" path branch name host_hex
|
|
698
|
+
if [ "$#" -gt 1 ]; then
|
|
699
|
+
die "usage: bin/de code [branch | environment | folder | path]"
|
|
700
|
+
elif [ "$#" -eq 1 ]; then
|
|
701
|
+
target=""
|
|
702
|
+
while IFS=$'\t' read -r path branch; do
|
|
703
|
+
[ -d "$path" ] || continue
|
|
704
|
+
if [ "$1" = "$branch" ] || [ "$1" = "$(basename "$path")" ] || [ "$1" = "$(env_name_for "$path")" ]; then
|
|
705
|
+
target="$path"
|
|
706
|
+
break
|
|
707
|
+
fi
|
|
708
|
+
done < <(checkouts)
|
|
709
|
+
# Or any folder in a checkout, e.g. the absolute path Orca's "Open in" adds
|
|
710
|
+
if [ -z "$target" ] && [ -d "$1" ]; then
|
|
711
|
+
target="$(cd "$(git -C "$1" rev-parse --show-toplevel)" && pwd -P)" ||
|
|
712
|
+
die "$1 is not inside a git checkout"
|
|
713
|
+
fi
|
|
714
|
+
|
|
715
|
+
if [ -z "$target" ]; then
|
|
716
|
+
warn "no checkout of $ENV_REPO matches $1, these exist:"
|
|
717
|
+
while IFS=$'\t' read -r path branch; do
|
|
718
|
+
[ -d "$path" ] && printf ' %-40s %-30s %s\n' "$(env_name_for "$path")" "${branch:-(detached)}" "$path" >&2
|
|
719
|
+
done < <(checkouts)
|
|
720
|
+
die "create one with: bin/de new $1"
|
|
721
|
+
fi
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
[ -x "$target/bin/de" ] || die "$target has no bin/de, it predates the environment setup"
|
|
725
|
+
|
|
726
|
+
name="$(env_name_for "$target")"
|
|
727
|
+
if ! env_running "$name"; then
|
|
728
|
+
log "Environment $name isn't running, starting it"
|
|
729
|
+
if ! run "$target/bin/de" up; then
|
|
730
|
+
# Still open it when only a setup step failed, the editor helps fix it
|
|
731
|
+
env_running "$name" || die "bin/de up failed and $name isn't running"
|
|
732
|
+
warn "bin/de up failed, see above, opening $name anyway"
|
|
733
|
+
fi
|
|
734
|
+
fi
|
|
735
|
+
|
|
736
|
+
log "Opening $target ($name) in VS Code"
|
|
737
|
+
# The authority is the host folder, hex encoded, the path is
|
|
738
|
+
# devcontainer.json's workspaceFolder: /workspaces/<host path>
|
|
739
|
+
host_hex="$(printf '%s' "$target" | xxd -p | tr -d '\n')"
|
|
740
|
+
run "${CODE:-code}" --folder-uri "vscode-remote://dev-container+$host_hex/workspaces$target"
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
cmd_down() {
|
|
744
|
+
local destroy_persistent=false arg answer
|
|
745
|
+
for arg in "$@"; do
|
|
746
|
+
case "$arg" in
|
|
747
|
+
--destroy-persistent) destroy_persistent=true ;;
|
|
748
|
+
*) die "usage: bin/de down [--destroy-persistent]" ;;
|
|
749
|
+
esac
|
|
750
|
+
done
|
|
751
|
+
|
|
752
|
+
env_write_dotenv
|
|
753
|
+
|
|
754
|
+
if [ "$ENV_KIND" = persistent ] || env_is_persistent "$ENV_NAME"; then
|
|
755
|
+
if ! $destroy_persistent; then
|
|
756
|
+
log "Stopping persistent environment $ENV_NAME, its volumes are kept"
|
|
757
|
+
env_compose stop
|
|
758
|
+
return
|
|
759
|
+
fi
|
|
760
|
+
|
|
761
|
+
[ -t 0 ] || die "--destroy-persistent needs a terminal to confirm"
|
|
762
|
+
warn "This deletes $ENV_NAME and all of its data:"
|
|
763
|
+
docker volume ls --filter "label=com.docker.compose.project=$ENV_NAME" >&2
|
|
764
|
+
read -r -p "Type the environment name to confirm: " answer
|
|
765
|
+
[ "$answer" = "$ENV_NAME" ] || die "name didn't match, nothing removed"
|
|
766
|
+
else
|
|
767
|
+
log "Removing ephemeral environment $ENV_NAME"
|
|
768
|
+
fi
|
|
769
|
+
|
|
770
|
+
env_compose down --volumes --remove-orphans
|
|
771
|
+
env_destroy "$ENV_NAME"
|
|
772
|
+
log "Removed $ENV_NAME"
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
cmd_prune() {
|
|
776
|
+
local yes=false arg name path branch reason answer orphans=()
|
|
777
|
+
for arg in "$@"; do
|
|
778
|
+
case "$arg" in
|
|
779
|
+
--yes | -y) yes=true ;;
|
|
780
|
+
*) die "usage: bin/de prune [--yes]" ;;
|
|
781
|
+
esac
|
|
782
|
+
done
|
|
783
|
+
|
|
784
|
+
log "Looking for ephemeral environments of $ENV_GIT_COMMON_DIR"
|
|
785
|
+
for name in $(env_list ephemeral); do
|
|
786
|
+
if env_is_persistent "$name"; then
|
|
787
|
+
warn "$name has persistent resources, skipping"
|
|
788
|
+
continue
|
|
789
|
+
fi
|
|
790
|
+
|
|
791
|
+
path="$(env_label "$name" dev.env.path)"
|
|
792
|
+
branch="$(env_label "$name" dev.env.branch)"
|
|
793
|
+
reason=""
|
|
794
|
+
if [ -z "$path" ] || [ ! -d "$path" ]; then
|
|
795
|
+
reason="checkout ${path:-?} no longer exists"
|
|
796
|
+
elif ! git -C "$path" rev-parse --git-dir >/dev/null 2>&1; then
|
|
797
|
+
reason="$path is no longer a git checkout"
|
|
798
|
+
elif [ -n "$branch" ] && ! git -C "$path" show-ref --verify --quiet "refs/heads/$branch"; then
|
|
799
|
+
reason="branch $branch is gone"
|
|
800
|
+
elif [ "$(env_name_for "$path")" != "$name" ]; then
|
|
801
|
+
reason="$path is now on another branch"
|
|
802
|
+
fi
|
|
803
|
+
|
|
804
|
+
if [ -n "$reason" ]; then
|
|
805
|
+
orphans+=("$name")
|
|
806
|
+
printf ' %-50s %s\n' "$name" "$reason"
|
|
807
|
+
else
|
|
808
|
+
printf ' %-50s in use by %s, keeping\n' "$name" "$path"
|
|
809
|
+
fi
|
|
810
|
+
done
|
|
811
|
+
|
|
812
|
+
if [ "${#orphans[@]}" -eq 0 ]; then
|
|
813
|
+
log "Nothing to prune"
|
|
814
|
+
return
|
|
815
|
+
fi
|
|
816
|
+
|
|
817
|
+
echo
|
|
818
|
+
log "These resources will be removed:"
|
|
819
|
+
for name in "${orphans[@]}"; do
|
|
820
|
+
docker container ls -a --filter "label=com.docker.compose.project=$name" --format ' container {{.Names}}'
|
|
821
|
+
docker volume ls --filter "label=com.docker.compose.project=$name" --format ' volume {{.Name}}'
|
|
822
|
+
docker network ls --filter "label=com.docker.compose.project=$name" --format ' network {{.Name}}'
|
|
823
|
+
done
|
|
824
|
+
|
|
825
|
+
if ! $yes; then
|
|
826
|
+
[ -t 0 ] || die "pass --yes to prune without a terminal"
|
|
827
|
+
read -r -p "Remove ${#orphans[@]} environment(s)? [y/N] " answer
|
|
828
|
+
[ "$answer" = y ] || [ "$answer" = Y ] || die "nothing removed"
|
|
829
|
+
fi
|
|
830
|
+
|
|
831
|
+
for name in "${orphans[@]}"; do
|
|
832
|
+
log "Removing $name"
|
|
833
|
+
env_destroy "$name"
|
|
834
|
+
done
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
# --- Main ---------------------------------------------------------------
|
|
838
|
+
|
|
839
|
+
command="${1:-help}"
|
|
840
|
+
[ "$#" -gt 0 ] && shift
|
|
841
|
+
|
|
842
|
+
if [ -n "$DE_TEMPLATE" ]; then
|
|
843
|
+
case "$command" in
|
|
844
|
+
help | -h | --help) usage_source; exit ;;
|
|
845
|
+
init) cmd_init "$@"; exit ;;
|
|
846
|
+
esac
|
|
847
|
+
# `code <folder>` (Orca's "Open in" runs it from /) opens the checkout that
|
|
848
|
+
# folder is in, from inside it, so a checkout's older bin/de handles it too
|
|
849
|
+
if [ "$command" = code ] && [ "$#" -eq 1 ] && [ -d "$1" ]; then
|
|
850
|
+
cd "$1"
|
|
851
|
+
shift
|
|
852
|
+
fi
|
|
853
|
+
# Everything else is for the checkout the current directory is in
|
|
854
|
+
checkout="$(git rev-parse --show-toplevel 2>/dev/null)" || die "not inside a git checkout"
|
|
855
|
+
[ -f "$checkout/.devcontainer/env.conf" ] && [ -x "$checkout/bin/de" ] ||
|
|
856
|
+
die "$checkout has no development environments, set them up with: de init"
|
|
857
|
+
exec "$checkout/bin/de" "$command" "$@"
|
|
858
|
+
fi
|
|
859
|
+
|
|
860
|
+
case "$command" in
|
|
861
|
+
help | -h | --help) usage; exit ;;
|
|
862
|
+
initialize | new | up | exec | status | code | down | prune) ;;
|
|
863
|
+
init) die "run de init from the de repository, which bin/de comes from" ;;
|
|
864
|
+
*) usage >&2; die "unknown command: $command" ;;
|
|
865
|
+
esac
|
|
866
|
+
|
|
867
|
+
env_init "$DE_ROOT"
|
|
868
|
+
"cmd_$command" "$@"
|