brunch 0.0.1 → 0.8.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0446f4c84c8e70a7178c9de33468d90d7747c12b
4
- data.tar.gz: 4f08030c58640bbbeea3a2078083bca77744461e
2
+ SHA256:
3
+ metadata.gz: 318ff646dd610a9647612d8ea0fcc446e5122215f4517d665fc639b5bfca740d
4
+ data.tar.gz: e4aeddec12c82d9b0d33caddf6a366840e3db8badd45fec4114f57bd034c3d86
5
5
  SHA512:
6
- metadata.gz: 0dc5753a95e229da0bf221648a291b8c745194e4798674fa3ab377f8cce5d7969843780791db8dd12d0767b4f6001ed5ac0f3a58c6b9c12abdeb7e9ea5217ff2
7
- data.tar.gz: 2e2a24eb7ce8c920cef40c55bc744c0399c81653872eca813aa25aa395a6ca86ef42abf0f022123a7bd9e4a876a6528409121ff39954ebe742af04210d185202
6
+ metadata.gz: d334e9cc379f39f4866626e8801adbdc9cbe8aace2d0249b8b4a6a81020b34ec35eca8b8dcf742622e06545d58086966ed087784b077460bdc7e56e5ec150f1c
7
+ data.tar.gz: b34f7f1baf8dbbf6a56669ccc310846eab6aa23d891fa05e4adbe541e1ab1ccea954bfdb46c087fba7a47e3601f45f967efb1de8ff93d7678cb361abaec6eb1c
data/CHANGELOG.md ADDED
@@ -0,0 +1,75 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.8.1 - 2026-09-29
6
+
7
+ - Reorganize the README into a step-by-step guide for branches, parallel
8
+ worktrees, managers, and everyday commands.
9
+
10
+ ## 0.8.0 - 2026-09-29
11
+
12
+ - Use one worktree-based lifecycle for both branch switches and parallel
13
+ worktrees. Branches within a worktree reuse its port; separate worktrees
14
+ receive distinct host ports.
15
+ - Run applications from the live worktree and replace mode-specific port
16
+ settings with an optional `preferred_port`.
17
+ - Document installation and everyday use, and add a compact illustration to
18
+ the README.
19
+ - Make CI lint only project files, install the `ghe` alias, and initialize the
20
+ test repository on `main`.
21
+ - Detect running Podman Compose containers by their project label.
22
+
23
+ ## 0.7.1 - 2026-09-29
24
+
25
+ - Use a compatible Podman Compose provider and CLI arguments in integration
26
+ tests and at runtime.
27
+
28
+ ## 0.7.0 - 2026-09-29
29
+
30
+ - Add parallel worktree mode with per-worktree ports, live source directories,
31
+ isolated projects and volumes, and branch-specific environments.
32
+ - Handle git-hooks-ext worktree create, remove, move, prune, and repair events;
33
+ reconcile plain Git worktree operations during cleanup.
34
+ - Preserve control files for teardown after a worktree disappears, and use
35
+ short repository locks plus per-worktree lifecycle locks.
36
+
37
+ ## 0.6.0 - 2026-09-29
38
+
39
+ - Add shared and unique port modes; shared port `3000` is now the default.
40
+
41
+ ## 0.5.0 - 2026-09-28
42
+
43
+ - Add `ports` and script-friendly `port` commands.
44
+
45
+ ## 0.4.1 - 2026-09-28
46
+
47
+ - Strengthen `doctor` hook and port validation.
48
+
49
+ ## 0.4.0 - 2026-09-28
50
+
51
+ - Add `doctor`, `exec`, and argument forwarding for `logs`.
52
+ - Serialize commands with a repository-local lock, save state atomically, and
53
+ recover interrupted environment setup on the next activation.
54
+ - Add conditional Docker Compose and Podman Compose integration tests to CI.
55
+
56
+ ## 0.3.0 - 2026-09-28
57
+
58
+ - Add a native `local_process` manager for `bin/dev`-style commands, with PID
59
+ tracking, process-group shutdown, and snapshot-local logs.
60
+
61
+ ## 0.2.0 - 2026-09-28
62
+
63
+ - Add pluggable environment managers. Docker Compose remains the default and a
64
+ command-based manager supports other project-specific runtimes.
65
+ - Add an optional `create` lifecycle command for provisioning custom-manager
66
+ environments before they start.
67
+ - Add the Podman Compose manager, configurable `switch_only` and `active_only`
68
+ lifecycle modes, and `status`, `stop`, `restart`, and `logs` commands.
69
+ - Validate `brunch.yml` and expose manager status and health checks.
70
+
71
+ ## 0.1.0 - 2026-09-28
72
+
73
+ - Initial public release.
74
+ - Isolated Docker Compose environments activated by Git branch checkout.
75
+ - `git-hooks-ext` integration and stale-environment cleanup.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maciej Ciemborowicz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,355 @@
1
+ # Brunch
2
+
3
+ ![Brunch — container per branch and worktree](brunch.webp)
4
+
5
+ Brunch runs isolated development environments for Git branches and worktrees.
6
+
7
+ Each branch gets its own environment. With Compose, that means a separate project, network, containers, and named volumes. Branches checked out in the same worktree share its host port, while additional worktrees get different ports and can run in parallel.
8
+
9
+ Docker Compose is the default manager. Podman Compose, local processes, and custom project commands are also supported.
10
+
11
+ ## Requirements
12
+
13
+ - Ruby 3.1+
14
+ - Git 2.28+
15
+ - Docker Compose, Podman Compose, or another supported manager
16
+ - [git-hooks-ext](https://github.com/ciembor/git-hooks-ext)
17
+
18
+ ## Installation
19
+
20
+ Install [git-hooks-ext](https://github.com/ciembor/git-hooks-ext) so that `ghe` is available on your `PATH`, then install Brunch:
21
+
22
+ ```bash
23
+ gem install brunch
24
+ ```
25
+
26
+ Inside each repository you want Brunch to manage:
27
+
28
+ ```bash
29
+ brunch install
30
+ ```
31
+
32
+ Brunch installs its hooks through `git-hooks-ext` and will not overwrite hooks owned by another tool.
33
+
34
+ ## Configuration
35
+
36
+ Add `brunch.yml` to the repository root.
37
+
38
+ For Docker Compose:
39
+
40
+ ```yaml
41
+ compose_file: compose.yaml
42
+ ```
43
+
44
+ Expose the application through `BRUNCH_PORT`:
45
+
46
+ ```yaml
47
+ services:
48
+ web:
49
+ ports:
50
+ - "127.0.0.1:${BRUNCH_PORT}:3000"
51
+ ```
52
+
53
+ The first activated worktree prefers port `3000`. Additional worktrees receive another free port.
54
+
55
+ To prefer a different starting port:
56
+
57
+ ```yaml
58
+ preferred_port: 4000
59
+ ```
60
+
61
+ Port assignments are persisted per worktree in `.git/brunch/state.json`.
62
+
63
+ ## Getting started
64
+
65
+ Commit `brunch.yml` and the application configuration first. The repository must contain at least one commit.
66
+
67
+ Then run:
68
+
69
+ ```bash
70
+ brunch install
71
+ brunch doctor
72
+ brunch activate
73
+ brunch status
74
+ ```
75
+
76
+ `brunch activate` starts the environment for the currently checked-out branch.
77
+
78
+ To print only the current worktree's port:
79
+
80
+ ```bash
81
+ brunch port
82
+ ```
83
+
84
+ Open the application on the printed port, for example:
85
+
86
+ ```text
87
+ http://127.0.0.1:3000
88
+ ```
89
+
90
+ With Compose, the application must listen on the container port mapped in the Compose file.
91
+
92
+ ## Branch switching
93
+
94
+ Once the hooks are installed, normal Git branch switches automatically stop the previous branch environment and start the new one:
95
+
96
+ ```bash
97
+ git switch -c feature/login
98
+ git switch main
99
+ ```
100
+
101
+ Branches checked out in the same worktree reuse that worktree's host port.
102
+
103
+ With Compose, each branch keeps its own Compose project and named volumes, so persistent resources remain isolated between branches.
104
+
105
+ To manually start the environment after `brunch stop`:
106
+
107
+ ```bash
108
+ brunch activate
109
+ ```
110
+
111
+ To rebuild and restart it:
112
+
113
+ ```bash
114
+ brunch restart
115
+ ```
116
+
117
+ Compose builds use files from the live worktree, including uncommitted changes.
118
+
119
+ If you want source edits to appear in an already running container without rebuilding, configure a bind mount or another reload mechanism in your Compose setup.
120
+
121
+ ## Parallel worktrees
122
+
123
+ Additional worktrees run independently on different host ports.
124
+
125
+ Create them with `git-hooks-ext`:
126
+
127
+ ```bash
128
+ ghe worktree add -b feature-a ../feature-a
129
+ ghe worktree add -b feature-b ../feature-b
130
+ ```
131
+
132
+ Then:
133
+
134
+ ```bash
135
+ cd ../feature-a
136
+
137
+ brunch port
138
+ brunch ports
139
+ ```
140
+
141
+ `brunch port` prints the current worktree's port.
142
+
143
+ `brunch ports` lists ports assigned to all worktrees.
144
+
145
+ Switching branches inside one worktree does not affect environments running in other worktrees.
146
+
147
+ If you create a worktree with plain Git:
148
+
149
+ ```bash
150
+ git worktree add -b feature-c ../feature-c
151
+ ```
152
+
153
+ activate Brunch manually inside it:
154
+
155
+ ```bash
156
+ cd ../feature-c
157
+ brunch activate
158
+ ```
159
+
160
+ After moving or removing worktrees with plain Git, run:
161
+
162
+ ```bash
163
+ brunch cleanup
164
+ ```
165
+
166
+ Brunch never deletes the worktree source directory.
167
+
168
+ ## Managers
169
+
170
+ ### Docker Compose
171
+
172
+ Docker Compose is the default manager:
173
+
174
+ ```yaml
175
+ compose_file: compose.yaml
176
+ ```
177
+
178
+ ### Podman Compose
179
+
180
+ Podman Compose uses the same Compose file contract:
181
+
182
+ ```yaml
183
+ manager: podman_compose
184
+ compose_file: compose.yaml
185
+ ```
186
+
187
+ ### Local process
188
+
189
+ Use `local_process` for a development command such as `bin/dev`:
190
+
191
+ ```yaml
192
+ manager: local_process
193
+ command: bin/dev
194
+ ```
195
+
196
+ Brunch starts and stops the process together with the branch environment and stores its output under `.git/brunch/controls`.
197
+
198
+ ### Custom commands
199
+
200
+ Use the `command` manager to integrate Brunch with project-specific tooling:
201
+
202
+ ```yaml
203
+ manager: command
204
+ commands:
205
+ create: bin/environment create
206
+ start: bin/environment start
207
+ stop: bin/environment stop
208
+ remove: bin/environment remove
209
+ ```
210
+
211
+ Brunch runs these commands from the live worktree with:
212
+
213
+ ```text
214
+ BRUNCH_REF
215
+ BRUNCH_PORT
216
+ BRUNCH_PROJECT
217
+ BRUNCH_SNAPSHOT
218
+ ```
219
+
220
+ `BRUNCH_SNAPSHOT` points to the live worktree for compatibility with manager adapters.
221
+
222
+ `create` is optional.
223
+
224
+ `start` must return after launching the environment.
225
+
226
+ `stop` is called when switching away from the active branch.
227
+
228
+ `remove` is called when the corresponding worktree environment is deleted and should remove manager-owned persistent resources.
229
+
230
+ Optional commands:
231
+
232
+ ```yaml
233
+ commands:
234
+ status: bin/environment status
235
+ health: bin/environment health
236
+ logs: bin/environment logs
237
+ ```
238
+
239
+ These power the corresponding Brunch operations.
240
+
241
+ ## Worktree lifecycle
242
+
243
+ A branch checkout stops the previous environment in that worktree and starts the new one on the same host port.
244
+
245
+ Other worktrees continue running.
246
+
247
+ Removing a worktree removes its Brunch environments. Brunch keeps the information needed to shut down manager resources under `.git/brunch/controls`, so cleanup can still run after the worktree itself is gone.
248
+
249
+ If a custom `remove` command depends on project files, those files must be committed because cleanup after worktree removal uses the last committed state.
250
+
251
+ `brunch cleanup` removes environments belonging to worktrees that no longer exist.
252
+
253
+ Git does not expose every branch deletion workflow reliably to hooks, so stopped branch environments may remain until their worktree is removed.
254
+
255
+ ## Commands
256
+
257
+ ```bash
258
+ brunch install
259
+ ```
260
+
261
+ Install Brunch hooks for the repository.
262
+
263
+ ```bash
264
+ brunch activate
265
+ ```
266
+
267
+ Start the environment for the current branch.
268
+
269
+ ```bash
270
+ brunch status
271
+ ```
272
+
273
+ Show the current environment, manager status, and port.
274
+
275
+ ```bash
276
+ brunch port
277
+ ```
278
+
279
+ Print the current worktree's port.
280
+
281
+ ```bash
282
+ brunch ports
283
+ ```
284
+
285
+ List ports assigned to all worktrees.
286
+
287
+ ```bash
288
+ brunch stop
289
+ ```
290
+
291
+ Stop the active environment without removing it.
292
+
293
+ ```bash
294
+ brunch restart
295
+ ```
296
+
297
+ Recreate and start the active environment.
298
+
299
+ ```bash
300
+ brunch logs
301
+ brunch logs --follow
302
+ ```
303
+
304
+ Show environment logs.
305
+
306
+ ```bash
307
+ brunch run -- bin/rails console
308
+ ```
309
+
310
+ Run a command on the host from the live worktree.
311
+
312
+ ```bash
313
+ brunch doctor
314
+ ```
315
+
316
+ Check Git, installed hooks, configuration, manager availability, and ports.
317
+
318
+ ```bash
319
+ brunch cleanup
320
+ ```
321
+
322
+ Remove environments belonging to deleted worktrees.
323
+
324
+ ## Upgrading from older configurations
325
+
326
+ Before upgrading from an older branch-only configuration, stop its running environment and clean up its legacy state.
327
+
328
+ Brunch will not reinterpret an existing non-empty branch-only state as worktree state.
329
+
330
+ ## Development
331
+
332
+ ```bash
333
+ bundle install
334
+ bin/install-pre-commit
335
+ bundle exec rake quality
336
+ gem build brunch.gemspec
337
+ ```
338
+
339
+ Run container integration tests with Docker Compose:
340
+
341
+ ```bash
342
+ BRUNCH_INTEGRATION_MANAGER=docker_compose bundle exec rake test
343
+ ```
344
+
345
+ Or Podman Compose:
346
+
347
+ ```bash
348
+ BRUNCH_INTEGRATION_MANAGER=podman_compose bundle exec rake test
349
+ ```
350
+
351
+ Without `BRUNCH_INTEGRATION_MANAGER`, container integration tests are skipped by the local quality check.
352
+
353
+ The pre-commit hook runs RuboCop with automatic corrections, Reek, and the full test suite. SimpleCov requires 100% line and branch coverage for `lib/**/*.rb`.
354
+
355
+ If RuboCop modifies a file, review and stage the changes before committing.
data/brunch.webp ADDED
Binary file
data/exe/brunch ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/brunch"
5
+
6
+ exit Brunch::CLI.start(ARGV)
data/lib/brunch/cli.rb ADDED
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "open3"
6
+ require "yaml"
7
+
8
+ module Brunch
9
+ class CLI
10
+ include WorktreeMode
11
+
12
+ PORT_RANGE = (1024..49_151)
13
+ CONFIGURATION_KEYS = %w[manager compose_file command commands preferred_port].freeze
14
+ COMMAND_KEYS = %w[create start stop remove status health logs].freeze
15
+
16
+ class ConfigurationError < StandardError; end
17
+ class OperationError < StandardError; end
18
+
19
+ def self.start(arguments) = new.start(arguments)
20
+
21
+ def start(arguments)
22
+ Dir.chdir(repo_root) unless Dir.pwd == repo_root
23
+ case arguments
24
+ in ["install"] then Hooks.install
25
+ in ["version"] | ["--version"] | ["-v"] then puts Brunch::VERSION
26
+ else return worktree_command(arguments)
27
+ end
28
+ 0
29
+ rescue ConfigurationError => e
30
+ warn e.message
31
+ 64
32
+ rescue OperationError => e
33
+ warn e.message
34
+ 1
35
+ end
36
+
37
+ private
38
+
39
+ def git_output(*arguments)
40
+ output, status = Open3.capture2("git", *arguments)
41
+ abort "Git command failed: git #{arguments.join(' ')}" unless status.success?
42
+
43
+ output.strip
44
+ end
45
+
46
+ def repo_root
47
+ @repo_root ||= git_output("rev-parse", "--show-toplevel")
48
+ end
49
+
50
+ def state_root
51
+ common_dir = git_output("rev-parse", "--git-common-dir")
52
+ @state_root ||= File.join(File.expand_path(common_dir, repo_root), "brunch")
53
+ end
54
+
55
+ def state_path = File.join(state_root, "state.json")
56
+
57
+ def state
58
+ return { "worktrees" => {} } unless File.file?(state_path)
59
+
60
+ data = JSON.parse(File.read(state_path))
61
+ raise ConfigurationError, "Invalid Brunch state: expected a JSON object." unless data.is_a?(Hash)
62
+ if data.fetch("environments", {}).any? || data.key?("active_ref")
63
+ raise ConfigurationError, "Legacy branch state found in .git/brunch/state.json; stop old environments before upgrading."
64
+ end
65
+
66
+ data
67
+ rescue JSON::ParserError => e
68
+ raise ConfigurationError, "Invalid Brunch state: #{e.message}"
69
+ end
70
+
71
+ def save_state(value)
72
+ FileUtils.mkdir_p(state_root)
73
+ temporary_path = "#{state_path}.#{Process.pid}.tmp"
74
+ File.open(temporary_path, "w", 0o600) do |file|
75
+ file.write(JSON.pretty_generate(value))
76
+ file.flush
77
+ file.fsync
78
+ end
79
+ File.rename(temporary_path, state_path)
80
+ ensure
81
+ FileUtils.rm_f(temporary_path) if temporary_path
82
+ end
83
+
84
+ def configuration(root: repo_root)
85
+ path = File.join(root, "brunch.yml")
86
+ configured = File.file?(path) ? YAML.safe_load_file(path, permitted_classes: [], aliases: false) || {} : {}
87
+ validate_configuration!(configured)
88
+ { "manager" => "docker_compose", "compose_file" => "compose.yaml", "preferred_port" => 3000 }.merge(configured)
89
+ rescue Psych::Exception => e
90
+ raise ConfigurationError, "Invalid brunch.yml: #{e.message}"
91
+ end
92
+
93
+ def validate_configuration!(configured)
94
+ raise ConfigurationError, "brunch.yml must contain a mapping." unless configured.is_a?(Hash)
95
+
96
+ unknown = configured.keys - CONFIGURATION_KEYS
97
+ raise ConfigurationError, "Unknown brunch.yml key(s): #{unknown.join(', ')}." unless unknown.empty?
98
+
99
+ value = { "manager" => "docker_compose", "compose_file" => "compose.yaml", "preferred_port" => 3000 }.merge(configured)
100
+ unless %w[docker_compose podman_compose local_process command].include?(value["manager"])
101
+ raise ConfigurationError, "Unknown Brunch manager: #{value['manager']}."
102
+ end
103
+ unless value["preferred_port"].is_a?(Integer) && PORT_RANGE.cover?(value["preferred_port"])
104
+ raise ConfigurationError, "preferred_port must be a number from #{PORT_RANGE.begin} to #{PORT_RANGE.end}."
105
+ end
106
+ if %w[docker_compose podman_compose].include?(value["manager"]) && (!value["compose_file"].is_a?(String) || value["compose_file"].empty?)
107
+ raise ConfigurationError, "compose_file must be a non-empty string."
108
+ end
109
+ if value["manager"] == "local_process" && (!value["command"].is_a?(String) || value["command"].empty?)
110
+ raise ConfigurationError, "local_process manager requires a non-empty command."
111
+ end
112
+ return unless value["manager"] == "command"
113
+
114
+ commands = value["commands"]
115
+ raise ConfigurationError, "command manager requires a commands mapping." unless commands.is_a?(Hash)
116
+
117
+ unknown = commands.keys - COMMAND_KEYS
118
+ raise ConfigurationError, "Unknown commands key(s): #{unknown.join(', ')}." unless unknown.empty?
119
+
120
+ %w[start stop remove].each do |action|
121
+ raise ConfigurationError, "command manager requires commands.#{action}." unless commands[action].is_a?(String) && !commands[action].empty?
122
+ end
123
+ %w[create status health logs].each do |action|
124
+ raise ConfigurationError, "commands.#{action} must be a string." if commands.key?(action) && !commands[action].is_a?(String)
125
+ end
126
+ end
127
+
128
+ def preferred_port = configuration.fetch("preferred_port")
129
+
130
+ def archive_ref(ref, destination)
131
+ FileUtils.rm_rf(destination)
132
+ FileUtils.mkdir_p(destination)
133
+ environment = { "LC_ALL" => "C", "LANG" => "C" }
134
+ statuses = Open3.pipeline([environment, "git", "archive", "--format=tar", ref],
135
+ [environment, "tar", "-x", "-C", destination])
136
+ abort "Could not create a control copy for #{ref}." unless statuses.all?(&:success?)
137
+ end
138
+
139
+ def color(text, code)
140
+ return text unless $stdout.tty?
141
+
142
+ "\e[#{code}m#{text}\e[0m"
143
+ end
144
+ end
145
+ end