ctree 0.5.3
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 +7 -0
- data/CHANGELOG.md +63 -0
- data/LICENSE +21 -0
- data/README.md +491 -0
- data/bin/ctree +7 -0
- data/lib/ctree/cli.rb +402 -0
- data/lib/ctree/compose_config_cmd.rb +136 -0
- data/lib/ctree/config.rb +242 -0
- data/lib/ctree/config.yml +111 -0
- data/lib/ctree/create.rb +714 -0
- data/lib/ctree/delete.rb +313 -0
- data/lib/ctree/domain.rb +158 -0
- data/lib/ctree/env_cmd.rb +123 -0
- data/lib/ctree/free.rb +79 -0
- data/lib/ctree/list.rb +101 -0
- data/lib/ctree/rebase.rb +221 -0
- data/lib/ctree/sync.rb +17 -0
- data/lib/ctree/update.rb +354 -0
- data/lib/ctree/version.rb +5 -0
- data/lib/ctree.rb +935 -0
- metadata +78 -0
data/lib/ctree/cli.rb
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ctree
|
|
4
|
+
module CLI
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
COMMANDS = [
|
|
8
|
+
["create", "<worktree_name> <branch_name>", "create a sibling worktree on a branch"],
|
|
9
|
+
["delete", "<worktree_name>", "remove a worktree and its Docker resources"],
|
|
10
|
+
["list", "[all | free | used]", "list worktrees for the current source repo"],
|
|
11
|
+
["rebase", "", "rebase the worktree onto the source repo's master"],
|
|
12
|
+
["update", "", "sync images, volumes, and files from source"],
|
|
13
|
+
["free", "", "reset the worktree to a free placeholder branch"],
|
|
14
|
+
["env", "[list | check | fix]", "manage the worktree's .env against the source"],
|
|
15
|
+
["version", "", "print the installed ctree version"],
|
|
16
|
+
["domain", "[list | [add | delete] <tld>]", "configure local DNS resolution for a TLD (macOS only)"],
|
|
17
|
+
["config", "[list | add | delete]", "manage the per-repo .ctree/config.yml"],
|
|
18
|
+
["compose-config", "[list | check | fix]", "manage shared-volume references in the compose override"],
|
|
19
|
+
["sync", "", "sync the worktree by rebasing and updating"],
|
|
20
|
+
["help", "[command]", "show detailed help for a command"]
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
COMMAND_SIGNATURES = COMMANDS.map { |name, args, _| args.empty? ? name : "#{name} #{args}" }.freeze
|
|
24
|
+
|
|
25
|
+
COMMAND_SIGNATURE_WIDTH = COMMAND_SIGNATURES.map(&:length).max + 2
|
|
26
|
+
|
|
27
|
+
COMMANDS_TABLE = COMMAND_SIGNATURES.zip(COMMANDS).map { |sig, (_, _, desc)|
|
|
28
|
+
" #{sig.ljust(COMMAND_SIGNATURE_WIDTH)}#{desc}"
|
|
29
|
+
}.join("\n").freeze
|
|
30
|
+
|
|
31
|
+
USAGE = <<~USAGE
|
|
32
|
+
Replicate a Docker Compose dev tree as a sibling git worktree with its own branch, volumes, and .env.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
ctree [command]
|
|
36
|
+
|
|
37
|
+
Available Commands:
|
|
38
|
+
#{COMMANDS_TABLE}
|
|
39
|
+
|
|
40
|
+
Most commands run from the source repository. `update`, `rebase`, `free`,
|
|
41
|
+
`env`, and `compose-config` run from inside a worktree. Use "ctree help
|
|
42
|
+
<command>" for more information about a command and its options.
|
|
43
|
+
USAGE
|
|
44
|
+
|
|
45
|
+
HELP = {
|
|
46
|
+
"create" => <<~HELP,
|
|
47
|
+
Usage:
|
|
48
|
+
ctree create <worktree_name> <branch_name>
|
|
49
|
+
|
|
50
|
+
Creates a sibling worktree at ../<worktree_name> on <branch_name>. The entire
|
|
51
|
+
source directory is cloned using copy-on-write (clonefile on macOS, cp --reflink
|
|
52
|
+
on Linux). Docker volumes are replicated under the new compose project name and
|
|
53
|
+
a tailored .env is written — prompting you to accept or change each value.
|
|
54
|
+
|
|
55
|
+
--config <path> replaces the repo's .ctree/config.yml with a custom config file
|
|
56
|
+
for this worktree. The custom file is merged over the shipped defaults and
|
|
57
|
+
persisted into the worktree's .ctree/config.yml so that later commands
|
|
58
|
+
(update, rebase, etc.) continue to use it.
|
|
59
|
+
|
|
60
|
+
--force skips all confirmation prompts (assuming the bracket default, or
|
|
61
|
+
the shown value for per-var .env prompts) so create runs non-interactively.
|
|
62
|
+
|
|
63
|
+
--log-file <path> writes full output to <path> instead of the console,
|
|
64
|
+
appending if it already exists (creating it otherwise). Implies
|
|
65
|
+
--force (prompts are skipped; defaults are assumed).
|
|
66
|
+
HELP
|
|
67
|
+
"delete" => <<~HELP,
|
|
68
|
+
Usage:
|
|
69
|
+
ctree delete <worktree_name>
|
|
70
|
+
|
|
71
|
+
Removes a worktree: lists everything to be deleted, requires explicit "yes"
|
|
72
|
+
confirmation, then tears down the directory, git worktree registration,
|
|
73
|
+
per-project Docker volumes, and any running compose stack. Branches are
|
|
74
|
+
always preserved.
|
|
75
|
+
|
|
76
|
+
--force skips the confirmation prompt and proceeds with deletion.
|
|
77
|
+
|
|
78
|
+
--log-file <path> writes full output to <path> instead of the console,
|
|
79
|
+
appending if it already exists (creating it otherwise). Implies
|
|
80
|
+
--force (prompts are skipped; defaults are assumed).
|
|
81
|
+
HELP
|
|
82
|
+
"list" => <<~HELP,
|
|
83
|
+
Usage:
|
|
84
|
+
ctree list [all | free | used]
|
|
85
|
+
|
|
86
|
+
Lists all worktrees for the current source repo. "free" shows only worktrees
|
|
87
|
+
on a placeholder branch (prefix configured via free_branch_prefix in
|
|
88
|
+
.ctree/config.yml, default "FREE-"); "used" shows only worktrees on a
|
|
89
|
+
non-free branch. Omitting the filter (or passing "all") lists everything.
|
|
90
|
+
|
|
91
|
+
--no-current suppresses the 'current' tag in the list output
|
|
92
|
+
HELP
|
|
93
|
+
"update" => <<~HELP,
|
|
94
|
+
Usage:
|
|
95
|
+
ctree update
|
|
96
|
+
|
|
97
|
+
Run from inside a worktree. Re-tags source images to the worktree's project
|
|
98
|
+
name, rsyncs allowlisted volumes (update_volumes), and copies paths listed
|
|
99
|
+
in the update config key. Warns and prompts before running if the source repo
|
|
100
|
+
is on a non-default branch.
|
|
101
|
+
|
|
102
|
+
--force assumes the shown defaults for the prompts: the branch-mismatch
|
|
103
|
+
prompt defaults to *not* proceeding — so --force with a non-default source
|
|
104
|
+
branch will abort the update rather than forcing it through.
|
|
105
|
+
|
|
106
|
+
--log-file <path> writes full output to <path> instead of the console,
|
|
107
|
+
appending if it already exists (creating it otherwise). Implies
|
|
108
|
+
--force (prompts are skipped; defaults are assumed).
|
|
109
|
+
HELP
|
|
110
|
+
"rebase" => <<~HELP,
|
|
111
|
+
Usage:
|
|
112
|
+
ctree rebase
|
|
113
|
+
|
|
114
|
+
Run from inside a worktree. Does two things in order:
|
|
115
|
+
1. Rebases the worktree's current branch onto the source repo's master.
|
|
116
|
+
Skips if master is already an ancestor. Aborts if there are conflicts.
|
|
117
|
+
2. Rebases each embedded git repo (directories listed under `rebase` in
|
|
118
|
+
.ctree/config.yml, e.g. gems/plugins) from its source counterpart.
|
|
119
|
+
|
|
120
|
+
All operations are local — no network calls. Update the source repo first,
|
|
121
|
+
then run `ctree rebase` from the worktree to catch it up.
|
|
122
|
+
|
|
123
|
+
--force assumes the shown default for the uncommitted-changes prompt,
|
|
124
|
+
which defaults to *not* proceeding — so --force will abort a rebase run
|
|
125
|
+
against a dirty worktree rather than forcing it through.
|
|
126
|
+
|
|
127
|
+
--log-file <path> writes full output to <path> instead of the console,
|
|
128
|
+
appending if it already exists (creating it otherwise). Implies
|
|
129
|
+
--force (prompts are skipped; defaults are assumed).
|
|
130
|
+
HELP
|
|
131
|
+
"free" => <<~HELP,
|
|
132
|
+
Usage:
|
|
133
|
+
ctree free
|
|
134
|
+
|
|
135
|
+
Run from inside a worktree. Resets the worktree to a free placeholder branch
|
|
136
|
+
so it can be reused. Checks out the first available branch not currently
|
|
137
|
+
checked out in any worktree whose name starts with free_branch_prefix (default
|
|
138
|
+
"FREE-"). If all are occupied, creates the next sequential one, filling gaps
|
|
139
|
+
(FREE-001 + FREE-003 occupied → creates FREE-002). The prefix is configurable
|
|
140
|
+
via free_branch_prefix in .ctree/config.yml.
|
|
141
|
+
|
|
142
|
+
HELP
|
|
143
|
+
"env" => <<~HELP,
|
|
144
|
+
Usage:
|
|
145
|
+
ctree env [list | check | fix]
|
|
146
|
+
|
|
147
|
+
Run from inside a worktree. Manages the worktree's .env relative to the
|
|
148
|
+
source repo's .env.
|
|
149
|
+
|
|
150
|
+
"list" Prints all key=value pairs from the worktree .env.
|
|
151
|
+
|
|
152
|
+
"check" Reports discrepancies between source and worktree .env:
|
|
153
|
+
[missing] KEY — in source but absent from worktree
|
|
154
|
+
[extra] KEY — in worktree but absent from source
|
|
155
|
+
Exits 1 if any issues found. ctree-managed keys
|
|
156
|
+
(COMPOSE_PROJECT_NAME, HOST_NAME, etc.) and keys in
|
|
157
|
+
skip_env_keys (.ctree/config.yml) are never reported as extra.
|
|
158
|
+
|
|
159
|
+
"fix" Resolves discrepancies interactively:
|
|
160
|
+
Missing keys — prompted with source value as default
|
|
161
|
+
Extra keys — offered for deletion (y/N)
|
|
162
|
+
Keys already present in both are left untouched. ctree-managed
|
|
163
|
+
keys and skip_env_keys are never offered for deletion.
|
|
164
|
+
|
|
165
|
+
--force keeps source values for missing keys (no prompt) and
|
|
166
|
+
assumes the deletion default ("no") for extra keys.
|
|
167
|
+
HELP
|
|
168
|
+
"compose-config" => <<~HELP,
|
|
169
|
+
Usage:
|
|
170
|
+
ctree compose-config [list | check | fix]
|
|
171
|
+
|
|
172
|
+
Run from inside a worktree. Manages shared-volume external references in the
|
|
173
|
+
compose override file. "list" shows current state; "check" reports missing or
|
|
174
|
+
incorrect entries; "fix" writes the corrections automatically.
|
|
175
|
+
HELP
|
|
176
|
+
"config" => <<~HELP,
|
|
177
|
+
Usage:
|
|
178
|
+
ctree config [list | add | delete]
|
|
179
|
+
|
|
180
|
+
Manages the per-repo .ctree/config.yml. "list" prints the resolved config
|
|
181
|
+
(defaults merged with repo overrides); "add" scaffolds a new config file with
|
|
182
|
+
annotated defaults; "delete" removes the file and its directory.
|
|
183
|
+
|
|
184
|
+
--force skips confirmation: "add" resets an existing config file (default
|
|
185
|
+
no, so it won't under --force) and "delete" proceeds with removal (default
|
|
186
|
+
yes for explicit-typing prompts).
|
|
187
|
+
HELP
|
|
188
|
+
"domain" => <<~HELP,
|
|
189
|
+
Usage:
|
|
190
|
+
ctree domain [list | [add | delete] <tld>]
|
|
191
|
+
|
|
192
|
+
macOS only. Configures local DNS resolution for a TLD. "add <tld>" adds a
|
|
193
|
+
wildcard dnsmasq rule resolving *.tld to 127.0.0.1 and writes a macOS
|
|
194
|
+
resolver file at /etc/resolver/<tld> (requires sudo). "delete <tld>" removes
|
|
195
|
+
both. "list" shows TLDs currently configured in dnsmasq and whether their
|
|
196
|
+
resolver files are present.
|
|
197
|
+
HELP
|
|
198
|
+
"version" => <<~HELP,
|
|
199
|
+
Usage:
|
|
200
|
+
ctree version
|
|
201
|
+
|
|
202
|
+
Prints the installed ctree version.
|
|
203
|
+
HELP
|
|
204
|
+
"sync" => <<~HELP,
|
|
205
|
+
Usage:
|
|
206
|
+
ctree sync
|
|
207
|
+
|
|
208
|
+
Run from inside a worktree. Equivalent to running rebase and update
|
|
209
|
+
commands. All operations are local — no network calls. Update the source
|
|
210
|
+
repo first, then run `ctree sync` from the worktree to catch it up.
|
|
211
|
+
|
|
212
|
+
--force skips confirmations and assumes the default values.
|
|
213
|
+
|
|
214
|
+
--log-file <path> writes full output to <path> instead of the console,
|
|
215
|
+
appending if it already exists (creating it otherwise). Implies
|
|
216
|
+
--force (prompts are skipped; defaults are assumed).
|
|
217
|
+
HELP
|
|
218
|
+
}.freeze
|
|
219
|
+
|
|
220
|
+
def usage_and_exit
|
|
221
|
+
puts USAGE
|
|
222
|
+
exit 1
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Worktree names double as Docker Compose project names, which Docker
|
|
226
|
+
# requires to be lowercase letters/digits/'-'/'_'. If the user passed
|
|
227
|
+
# something close, suggest the sanitized form.
|
|
228
|
+
def invalid_name_message(name)
|
|
229
|
+
base = "invalid name '#{name}'; must be lowercase letters, digits, '-', or '_', " \
|
|
230
|
+
"starting with a letter or digit (Docker Compose project name constraint)"
|
|
231
|
+
suggestion = Naming.sanitize_compose_project_name(name)
|
|
232
|
+
return base if suggestion.empty? || suggestion == name || suggestion !~ NAME_PATTERN
|
|
233
|
+
"#{base} — try '#{suggestion}'"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
LOG_FILE_COMMANDS = %w[create delete rebase update sync].freeze
|
|
237
|
+
|
|
238
|
+
# Wraps create/delete/rebase/update invocation when --log-file is given:
|
|
239
|
+
# redirects output to the file and shows status on the console. On failure,
|
|
240
|
+
# stays silent (matches no-log-file behavior). A no-op if log_file is nil.
|
|
241
|
+
def with_logging(log_file, progress_message, done_message)
|
|
242
|
+
return yield unless log_file
|
|
243
|
+
|
|
244
|
+
LogFile.configure(log_file)
|
|
245
|
+
Scroller.start(progress_message)
|
|
246
|
+
begin
|
|
247
|
+
yield
|
|
248
|
+
ensure
|
|
249
|
+
status = $!.is_a?(SystemExit) ? $!.status : ($! ? 1 : 0)
|
|
250
|
+
Scroller.stop(status.zero? ? done_message : nil)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def run(argv)
|
|
255
|
+
usage_and_exit if argv.empty?
|
|
256
|
+
|
|
257
|
+
force = argv.include?("--force")
|
|
258
|
+
argv = argv.reject { |a| a == "--force" }
|
|
259
|
+
|
|
260
|
+
log_file = nil
|
|
261
|
+
argv = argv.reject do |a|
|
|
262
|
+
if a.start_with?("--log-file=")
|
|
263
|
+
log_file = a.sub("--log-file=", "")
|
|
264
|
+
true
|
|
265
|
+
elsif a == "--log-file" && argv.index(a) + 1 < argv.length && !argv[argv.index(a) + 1].start_with?("-")
|
|
266
|
+
# Extract the next element as the path and remove both --log-file and the path from argv
|
|
267
|
+
log_file = argv[argv.index(a) + 1]
|
|
268
|
+
argv.delete_at(argv.index(a) + 1)
|
|
269
|
+
true
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# --log-file implies non-interactive: with no console visible while logging
|
|
274
|
+
# there's nowhere to show a live prompt, so it always runs like --force.
|
|
275
|
+
force = true if log_file
|
|
276
|
+
|
|
277
|
+
verb = argv[0]
|
|
278
|
+
|
|
279
|
+
if log_file
|
|
280
|
+
Log.die "--log-file requires a path, e.g. --log-file /tmp/ctree.log" if log_file.empty?
|
|
281
|
+
unless LOG_FILE_COMMANDS.include?(verb)
|
|
282
|
+
Log.die "--log-file is only supported for #{LOG_FILE_COMMANDS.join(", ")}"
|
|
283
|
+
end
|
|
284
|
+
parent = File.dirname(File.expand_path(log_file))
|
|
285
|
+
unless File.directory?(parent) && File.writable?(parent)
|
|
286
|
+
Log.die "--log-file parent directory does not exist or is not writable: #{parent}"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
case verb
|
|
291
|
+
when "version"
|
|
292
|
+
puts "ctree #{VERSION}"
|
|
293
|
+
when "create"
|
|
294
|
+
if argv.length == 3
|
|
295
|
+
config_path = nil
|
|
296
|
+
elsif (idx = argv.index { |a| a.start_with?("--config=") })
|
|
297
|
+
config_path = argv[idx].sub("--config=", "")
|
|
298
|
+
argv.delete_at(idx) # Clean up argv
|
|
299
|
+
elsif argv.length == 5 && argv[3] == "--config"
|
|
300
|
+
config_path = argv[4]
|
|
301
|
+
else
|
|
302
|
+
usage_and_exit
|
|
303
|
+
end
|
|
304
|
+
name = argv[1]
|
|
305
|
+
branch_arg = argv[2]
|
|
306
|
+
Log.die invalid_name_message(name) unless name =~ NAME_PATTERN
|
|
307
|
+
if branch_arg !~ BRANCH_NAME_PATTERN
|
|
308
|
+
Log.die "invalid branch name '#{branch_arg}'; must match #{BRANCH_NAME_PATTERN.inspect}"
|
|
309
|
+
end
|
|
310
|
+
with_logging(log_file, "creating worktree #{name}", "created worktree #{name}") do
|
|
311
|
+
Create.run(name: name, branch: branch_arg, config_path: config_path, force: force)
|
|
312
|
+
end
|
|
313
|
+
when "delete"
|
|
314
|
+
usage_and_exit if argv.length != 2
|
|
315
|
+
name = argv[1]
|
|
316
|
+
Log.die invalid_name_message(name) unless name =~ NAME_PATTERN
|
|
317
|
+
with_logging(log_file, "deleting worktree #{name}", "deleted worktree #{name}") do
|
|
318
|
+
Delete.run(name: name, force: force)
|
|
319
|
+
end
|
|
320
|
+
when "update"
|
|
321
|
+
usage_and_exit unless argv.length == 1
|
|
322
|
+
with_logging(log_file, "updating worktree", "updated worktree") do
|
|
323
|
+
Update.run(force: force)
|
|
324
|
+
end
|
|
325
|
+
when "free"
|
|
326
|
+
usage_and_exit unless argv.length == 1
|
|
327
|
+
Free.run
|
|
328
|
+
when "env"
|
|
329
|
+
subcommand = argv[1]
|
|
330
|
+
usage_and_exit unless %w[list check fix].include?(subcommand)
|
|
331
|
+
usage_and_exit unless argv.length == 2
|
|
332
|
+
EnvCmd.run(subcommand, force: force)
|
|
333
|
+
when "compose-config"
|
|
334
|
+
subcommand = argv[1]
|
|
335
|
+
usage_and_exit unless %w[list check fix].include?(subcommand)
|
|
336
|
+
usage_and_exit unless argv.length == 2
|
|
337
|
+
ComposeConfigCmd.run(subcommand)
|
|
338
|
+
when "rebase"
|
|
339
|
+
usage_and_exit unless argv.length == 1
|
|
340
|
+
with_logging(log_file, "rebasing worktree", "rebased worktree") do
|
|
341
|
+
Rebase.run(force: force)
|
|
342
|
+
end
|
|
343
|
+
when "sync"
|
|
344
|
+
usage_and_exit unless argv.length == 1
|
|
345
|
+
with_logging(log_file, "syncing worktree", "synced worktree") do
|
|
346
|
+
Sync.run(force: force)
|
|
347
|
+
end
|
|
348
|
+
when "list"
|
|
349
|
+
show_current = !argv.include?("--no-current")
|
|
350
|
+
argv.delete("--no-current") # Remove flag from argv to clean up parsing
|
|
351
|
+
case argv[1..]
|
|
352
|
+
when [], ["all"]
|
|
353
|
+
List.run(show_current: show_current)
|
|
354
|
+
when ["free"]
|
|
355
|
+
List.run(filter: :free, show_current: show_current)
|
|
356
|
+
when ["used"]
|
|
357
|
+
List.run(filter: :used, show_current: show_current)
|
|
358
|
+
else
|
|
359
|
+
usage_and_exit
|
|
360
|
+
end
|
|
361
|
+
when "domain"
|
|
362
|
+
subcommand = argv[1]
|
|
363
|
+
case subcommand
|
|
364
|
+
when "add"
|
|
365
|
+
usage_and_exit unless argv.length == 3
|
|
366
|
+
Domain.add(tld: argv[2])
|
|
367
|
+
when "delete"
|
|
368
|
+
usage_and_exit unless argv.length == 3
|
|
369
|
+
Domain.remove(tld: argv[2])
|
|
370
|
+
when "list"
|
|
371
|
+
usage_and_exit unless argv.length == 2
|
|
372
|
+
Domain.list
|
|
373
|
+
else
|
|
374
|
+
usage_and_exit
|
|
375
|
+
end
|
|
376
|
+
when "config"
|
|
377
|
+
case argv[1..]
|
|
378
|
+
when []
|
|
379
|
+
usage_and_exit
|
|
380
|
+
when ["list"]
|
|
381
|
+
Config.print_resolved!
|
|
382
|
+
when ["add"]
|
|
383
|
+
Config.add_local!(force: force)
|
|
384
|
+
when ["delete"]
|
|
385
|
+
Config.remove_local!(force: force)
|
|
386
|
+
else
|
|
387
|
+
usage_and_exit
|
|
388
|
+
end
|
|
389
|
+
when "help"
|
|
390
|
+
if argv.length == 1
|
|
391
|
+
puts USAGE
|
|
392
|
+
elsif (text = HELP[argv[1]])
|
|
393
|
+
print "\n#{text}\n"
|
|
394
|
+
else
|
|
395
|
+
Log.die "unknown command '#{argv[1]}'; available: #{HELP.keys.sort.join(", ")}"
|
|
396
|
+
end
|
|
397
|
+
else
|
|
398
|
+
usage_and_exit
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ctree
|
|
4
|
+
module ComposeConfigCmd
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def run(subcommand)
|
|
8
|
+
target_path, source_root = resolve_worktree!
|
|
9
|
+
config = Config.load(target_path)
|
|
10
|
+
Log.log_prefix = config[:log_prefix]
|
|
11
|
+
override_rel = config[:compose_override_file].to_s
|
|
12
|
+
share_volumes = config[:share_volumes]
|
|
13
|
+
env_filename = config[:env_filename]
|
|
14
|
+
|
|
15
|
+
case subcommand
|
|
16
|
+
when "list"
|
|
17
|
+
cmd_list(target_path, override_rel)
|
|
18
|
+
when "check"
|
|
19
|
+
source_project, source_volumes = resolve_source!(source_root, env_filename)
|
|
20
|
+
cmd_check(target_path, override_rel, share_volumes, source_project, source_volumes)
|
|
21
|
+
when "fix"
|
|
22
|
+
source_project, source_volumes = resolve_source!(source_root, env_filename)
|
|
23
|
+
cmd_fix(target_path, override_rel, share_volumes, source_project, source_volumes)
|
|
24
|
+
else
|
|
25
|
+
Log.die "unknown subcommand '#{subcommand}'; use: list, check, or fix"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve_worktree!
|
|
30
|
+
target_path = Pathname.pwd
|
|
31
|
+
toplevel_out, _, st = Sh.capture3("git", "-C", target_path.to_s, "rev-parse", "--show-toplevel")
|
|
32
|
+
Log.die "not inside a git repository" unless st.success?
|
|
33
|
+
git_top = Pathname.new(toplevel_out.strip)
|
|
34
|
+
unless target_path.realpath == git_top.realpath
|
|
35
|
+
Log.die "must be invoked from the worktree top-level"
|
|
36
|
+
end
|
|
37
|
+
common_out, _, c_st = Sh.capture3("git", "-C", target_path.to_s, "rev-parse", "--git-common-dir")
|
|
38
|
+
Log.die "could not resolve git common dir" unless c_st.success?
|
|
39
|
+
common = Pathname.new(common_out.strip)
|
|
40
|
+
common = (target_path / common) unless common.absolute?
|
|
41
|
+
source_root = common.parent.realpath
|
|
42
|
+
if source_root == target_path.realpath
|
|
43
|
+
Log.die "already in the source project; run ctree compose-config from inside a worktree"
|
|
44
|
+
end
|
|
45
|
+
[target_path.realpath, source_root]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resolve_source!(source_root, env_filename)
|
|
49
|
+
source_env = EnvFile.parse(source_root / env_filename)
|
|
50
|
+
env_cpn = source_env["COMPOSE_PROJECT_NAME"]
|
|
51
|
+
source_project = (env_cpn && !env_cpn.empty?) ? env_cpn :
|
|
52
|
+
Naming.sanitize_compose_project_name(source_root.basename.to_s)
|
|
53
|
+
|
|
54
|
+
docker_ok = Sh.system("docker", "info", out: File::NULL, err: File::NULL)
|
|
55
|
+
Log.die "docker not reachable" unless docker_ok
|
|
56
|
+
|
|
57
|
+
vols_out, _, st = Sh.capture3("docker", "volume", "ls", "--format", "{{.Name}}")
|
|
58
|
+
Log.die "docker volume ls failed" unless st.success?
|
|
59
|
+
prefix_re = /\A#{Regexp.escape(source_project)}_/
|
|
60
|
+
source_volumes = vols_out.lines.map(&:strip).reject(&:empty?).select { |v| v =~ prefix_re }
|
|
61
|
+
[source_project, source_volumes]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def cmd_list(target_path, override_rel)
|
|
65
|
+
Log.die "compose_override_file not configured" if override_rel.empty?
|
|
66
|
+
override_path = target_path / override_rel
|
|
67
|
+
Log.die "#{override_rel} not found" unless override_path.file?
|
|
68
|
+
puts override_path.read
|
|
69
|
+
exit 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cmd_check(target_path, override_rel, share_volumes, source_project, source_volumes)
|
|
73
|
+
Log.die "compose_override_file not configured" if override_rel.empty?
|
|
74
|
+
|
|
75
|
+
if share_volumes.empty?
|
|
76
|
+
Log.info "no share_volumes configured"
|
|
77
|
+
exit 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
override_path = target_path / override_rel
|
|
81
|
+
unless override_path.file?
|
|
82
|
+
Log.warn_ "#{override_rel} not found in worktree"
|
|
83
|
+
exit 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
result = ComposeOverride.audit(
|
|
87
|
+
target_path: target_path, override_rel: override_rel,
|
|
88
|
+
share_volumes: share_volumes, source_project: source_project,
|
|
89
|
+
source_volumes: source_volumes
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
total = result[:fixable].size + result[:unfixable].size
|
|
93
|
+
if total > 0
|
|
94
|
+
if result[:fixable].any?
|
|
95
|
+
Log.warn_ "#{total} issue#{total == 1 ? "" : "s"} found — run `ctree compose-config fix` to fix"
|
|
96
|
+
else
|
|
97
|
+
Log.warn_ "#{total} issue#{total == 1 ? "" : "s"} found — source volumes missing, repair not possible"
|
|
98
|
+
end
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Log.info "#{override_rel} is valid"
|
|
103
|
+
exit 0
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def cmd_fix(target_path, override_rel, share_volumes, source_project, source_volumes)
|
|
107
|
+
Log.die "compose_override_file not configured" if override_rel.empty?
|
|
108
|
+
|
|
109
|
+
if share_volumes.empty?
|
|
110
|
+
Log.info "no share_volumes configured"
|
|
111
|
+
exit 0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
result = ComposeOverride.audit(
|
|
115
|
+
target_path: target_path, override_rel: override_rel,
|
|
116
|
+
share_volumes: share_volumes, source_project: source_project,
|
|
117
|
+
source_volumes: source_volumes
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if result[:fixable].empty? && result[:unfixable].empty?
|
|
121
|
+
Log.info "#{override_rel} is already correct"
|
|
122
|
+
exit 0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
ComposeOverride.fix(
|
|
126
|
+
target_path: target_path, override_rel: override_rel,
|
|
127
|
+
share_volumes: share_volumes, source_project: source_project,
|
|
128
|
+
source_volumes: source_volumes
|
|
129
|
+
)
|
|
130
|
+
exit 0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private_class_method :resolve_worktree!, :resolve_source!,
|
|
134
|
+
:cmd_list, :cmd_check, :cmd_fix
|
|
135
|
+
end
|
|
136
|
+
end
|