brunch 0.9.1 → 0.9.2
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/CHANGELOG.md +6 -1
- data/README.md +14 -9
- data/lib/brunch/branch_settlement.rb +67 -0
- data/lib/brunch/cli.rb +1 -1
- data/lib/brunch/hooks.rb +12 -1
- data/lib/brunch/version.rb +1 -1
- data/lib/brunch/worktree_mode.rb +42 -6
- data/lib/brunch.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f7a05f3aae716ba9e82f616156c85380fad4437a5d0d440056a834999830fc6
|
|
4
|
+
data.tar.gz: dccd1027c1fb0a185807427cfe0ffbe06bc94362f656de08a064f52786c39ee2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 550ae4c541eabe5196634628561c094e28039a578ccbb795c97ee41bb67b20fd48a58d4742bd685b877da57654784daa6a57004697422ad5b15f633199e9dc33
|
|
7
|
+
data.tar.gz: b6e4d92569d518d6ba19a8eb2b326996d0351a00cae53a03e4eecd23f0653a3cf5a6247a0db199b34050a47c49d8434e3ff6148860136dc99856fe8616f97ddc
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.9.2 - 2026-10-05
|
|
4
|
+
|
|
5
|
+
- Settle branch deletions automatically after the Git process exits, preserving
|
|
6
|
+
environments across `git branch -m` without requiring another Brunch command.
|
|
7
|
+
Handle `git branch -M` when the destination already has an environment, and
|
|
8
|
+
keep pending state if the background worker cannot start.
|
|
4
9
|
|
|
5
10
|
## 0.9.1 - 2026-10-03
|
|
6
11
|
|
data/README.md
CHANGED
|
@@ -301,15 +301,20 @@ If a custom `remove` command depends on project files, those files must be commi
|
|
|
301
301
|
`brunch cleanup` removes environments belonging to worktrees that no longer exist.
|
|
302
302
|
|
|
303
303
|
With `git-hooks-ext` 0.6.0+, deleting a local branch queues its Brunch
|
|
304
|
-
environments for removal.
|
|
305
|
-
|
|
306
|
-
the
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
304
|
+
environments for removal. A detached worker waits for the Git process that
|
|
305
|
+
triggered the hook to exit, then checks the final refs and reflogs. A rename
|
|
306
|
+
keeps its environment under the new name; a deletion removes the managed
|
|
307
|
+
resources, including Compose containers and volumes. A Brunch command run
|
|
308
|
+
while the identified Git process is still working leaves the queued environment
|
|
309
|
+
alone. If the process cannot be identified or the worker cannot start, the next
|
|
310
|
+
regular Brunch command after Git exits, such as `brunch status` or
|
|
311
|
+
`brunch cleanup`, performs the check. Brunch also checks for
|
|
312
|
+
missing branches when Git emits no deletion event, on the next regular command.
|
|
313
|
+
If Git reflogs are unavailable and Brunch cannot distinguish a rename from a
|
|
314
|
+
deletion, it keeps the environment. A later `brunch restart` recreates the
|
|
315
|
+
container in the same Compose project, preserving named volumes.
|
|
316
|
+
For `git branch -M old target`, when `target` already has a Brunch environment,
|
|
317
|
+
Brunch removes that overwritten environment and moves `old` to `target`.
|
|
313
318
|
|
|
314
319
|
## Commands
|
|
315
320
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Brunch
|
|
7
|
+
module BranchSettlement
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def git_ancestor(pid = Process.ppid)
|
|
11
|
+
32.times do
|
|
12
|
+
process = process_info(pid)
|
|
13
|
+
break unless process
|
|
14
|
+
|
|
15
|
+
return { "pid" => pid, "started" => process.fetch(:started) } if File.basename(process.fetch(:command)) == "git"
|
|
16
|
+
|
|
17
|
+
pid = process.fetch(:parent)
|
|
18
|
+
break if pid <= 1
|
|
19
|
+
end
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def running?(identity)
|
|
24
|
+
return false unless identity.is_a?(Hash) && identity["pid"].is_a?(Integer) && identity["started"].is_a?(String)
|
|
25
|
+
|
|
26
|
+
process = process_info(identity.fetch("pid"))
|
|
27
|
+
return process_exists?(identity.fetch("pid")) unless process
|
|
28
|
+
|
|
29
|
+
process.fetch(:started) == identity.fetch("started") && !process.fetch(:state).start_with?("Z")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def process_exists?(pid)
|
|
33
|
+
Process.kill(0, pid)
|
|
34
|
+
true
|
|
35
|
+
rescue Errno::ESRCH
|
|
36
|
+
false
|
|
37
|
+
rescue Errno::EPERM
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def token = SecureRandom.hex(16)
|
|
42
|
+
|
|
43
|
+
def spawn_worker(root, ref, token)
|
|
44
|
+
executable = File.expand_path("../../exe/brunch", __dir__)
|
|
45
|
+
pid = Process.spawn(Gem.ruby, executable, "branch-settled", ref, token,
|
|
46
|
+
chdir: root, in: File::NULL, out: File::NULL, err: File::NULL,
|
|
47
|
+
pgroup: true, close_others: true)
|
|
48
|
+
Process.detach(pid)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def wait_for(identity)
|
|
52
|
+
sleep 0.1 while running?(identity)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def process_info(pid)
|
|
56
|
+
output, status = Open3.capture2("ps", "-p", pid.to_s, "-o", "ppid=,lstart=,stat=,comm=", err: File::NULL)
|
|
57
|
+
return unless status.success?
|
|
58
|
+
|
|
59
|
+
fields = output.strip.split(/\s+/, 8)
|
|
60
|
+
return unless fields.length == 8 && fields[0].match?(/\A\d+\z/)
|
|
61
|
+
|
|
62
|
+
{ parent: fields[0].to_i, started: fields[1, 5].join(" "), state: fields[6], command: fields[7] }
|
|
63
|
+
rescue Errno::ENOENT
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/brunch/cli.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Brunch
|
|
|
24
24
|
in ["install"] then Hooks.install
|
|
25
25
|
in ["version"] | ["--version"] | ["-v"] then puts Brunch::VERSION
|
|
26
26
|
else
|
|
27
|
-
unless arguments.first
|
|
27
|
+
unless %w[branch-deleted branch-settled].include?(arguments.first)
|
|
28
28
|
reconcile_deleted_branches
|
|
29
29
|
reconcile_missing_branches
|
|
30
30
|
end
|
data/lib/brunch/hooks.rb
CHANGED
|
@@ -27,7 +27,9 @@ module Brunch
|
|
|
27
27
|
when "branch-created", "remote-branch-created", "remote-branch-updated"
|
|
28
28
|
CLI.start(["register", arguments.fetch(0)])
|
|
29
29
|
when "branch-deleted"
|
|
30
|
-
|
|
30
|
+
with_branch_deletion_hook do
|
|
31
|
+
CLI.start(["branch-deleted", arguments.fetch(0), arguments.fetch(2)])
|
|
32
|
+
end
|
|
31
33
|
when "worktree-created", "worktree-removed", "worktree-moved", "worktree-pruned", "worktree-repaired"
|
|
32
34
|
CLI.start(["worktree-event", event, *arguments])
|
|
33
35
|
else
|
|
@@ -43,6 +45,15 @@ module Brunch
|
|
|
43
45
|
end
|
|
44
46
|
private_class_method :git_output
|
|
45
47
|
|
|
48
|
+
def with_branch_deletion_hook
|
|
49
|
+
previous = ENV.fetch("BRUNCH_BRANCH_DELETION_HOOK", nil)
|
|
50
|
+
ENV["BRUNCH_BRANCH_DELETION_HOOK"] = "1"
|
|
51
|
+
yield
|
|
52
|
+
ensure
|
|
53
|
+
previous ? ENV["BRUNCH_BRANCH_DELETION_HOOK"] = previous : ENV.delete("BRUNCH_BRANCH_DELETION_HOOK")
|
|
54
|
+
end
|
|
55
|
+
private_class_method :with_branch_deletion_hook
|
|
56
|
+
|
|
46
57
|
def install_hook(path, event)
|
|
47
58
|
contents = hook_contents(event)
|
|
48
59
|
abort "Refusing to replace existing hook: #{path}" if File.exist?(path) && File.read(path) != contents && !brunch_hook?(path)
|
data/lib/brunch/version.rb
CHANGED
data/lib/brunch/worktree_mode.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Brunch
|
|
|
21
21
|
worktree_activate(repo_root)
|
|
22
22
|
in ["stop"] then worktree_stop(repo_root)
|
|
23
23
|
in ["branch-deleted", ref, oid] then record_deleted_branch(ref, oid)
|
|
24
|
+
in ["branch-settled", ref, token] then settle_deleted_branch(ref, token)
|
|
24
25
|
in ["cleanup"] | ["register", _] then worktree_cleanup
|
|
25
26
|
in ["status"] then worktree_status
|
|
26
27
|
in ["list"] then worktree_ports
|
|
@@ -261,27 +262,58 @@ module Brunch
|
|
|
261
262
|
end
|
|
262
263
|
|
|
263
264
|
def record_deleted_branch(ref, oid)
|
|
265
|
+
git = BranchSettlement.git_ancestor if ENV["BRUNCH_BRANCH_DELETION_HOOK"] == "1"
|
|
266
|
+
token = BranchSettlement.token if git
|
|
267
|
+
recorded = false
|
|
264
268
|
with_state_lock do |data|
|
|
265
269
|
records = data.fetch("worktrees", {}).values
|
|
266
270
|
next unless records.any? { |record| record.fetch("environments", {}).key?(ref) }
|
|
267
271
|
|
|
268
|
-
(data["deleted_branches"] ||= {})[ref] = oid
|
|
272
|
+
(data["deleted_branches"] ||= {})[ref] = git ? { "oid" => oid, "git" => git, "token" => token } : oid
|
|
273
|
+
recorded = true
|
|
269
274
|
end
|
|
275
|
+
BranchSettlement.spawn_worker(repo_root, ref, token) if recorded && git
|
|
276
|
+
rescue SystemCallError => e
|
|
277
|
+
warn "Could not start branch cleanup worker: #{e.message}; cleanup will run on the next Brunch command."
|
|
270
278
|
end
|
|
271
279
|
|
|
272
280
|
def reconcile_deleted_branches
|
|
273
281
|
pending = state.fetch("deleted_branches", {})
|
|
274
282
|
return if pending.empty?
|
|
275
283
|
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
pending.each_key { |ref| settle_deleted_branch(ref) }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def settle_deleted_branch(ref, expected_token = nil)
|
|
288
|
+
if expected_token
|
|
289
|
+
queued = state.fetch("deleted_branches", {})[ref]
|
|
290
|
+
return unless queued.is_a?(Hash) && queued["token"] == expected_token
|
|
291
|
+
|
|
292
|
+
BranchSettlement.wait_for(queued.fetch("git"))
|
|
293
|
+
end
|
|
294
|
+
directory = File.join(state_root, "branch-locks")
|
|
295
|
+
FileUtils.mkdir_p(directory)
|
|
296
|
+
File.open(File.join(directory, Digest::SHA256.hexdigest(ref)), "w") do |lock|
|
|
297
|
+
lock.flock(File::LOCK_EX)
|
|
298
|
+
pending = state.fetch("deleted_branches", {})[ref]
|
|
299
|
+
next unless pending
|
|
300
|
+
next if expected_token && (!pending.is_a?(Hash) || pending["token"] != expected_token)
|
|
301
|
+
|
|
302
|
+
identity = pending["git"] if pending.is_a?(Hash)
|
|
303
|
+
next if identity && BranchSettlement.running?(identity)
|
|
304
|
+
|
|
305
|
+
oid = pending.is_a?(Hash) ? pending.fetch("oid") : pending
|
|
306
|
+
refs = local_branch_refs
|
|
278
307
|
unless refs.include?("refs/heads/#{ref}")
|
|
279
308
|
renamed = renamed_branch(ref, oid, refs)
|
|
280
309
|
next if renamed == :unknown
|
|
281
310
|
|
|
282
|
-
renamed ? rename_branch_environments(ref, renamed) : remove_branch_environments(ref)
|
|
311
|
+
renamed ? rename_branch_environments(ref, renamed, replace_existing: pending.is_a?(Hash)) : remove_branch_environments(ref)
|
|
312
|
+
end
|
|
313
|
+
with_state_lock do |data|
|
|
314
|
+
current = data.fetch("deleted_branches", {})[ref]
|
|
315
|
+
data.fetch("deleted_branches").delete(ref) if current == pending
|
|
283
316
|
end
|
|
284
|
-
with_state_lock { |data| data.fetch("deleted_branches").delete(ref) }
|
|
285
317
|
end
|
|
286
318
|
end
|
|
287
319
|
|
|
@@ -325,7 +357,11 @@ module Brunch
|
|
|
325
357
|
:unknown if missing_reflog
|
|
326
358
|
end
|
|
327
359
|
|
|
328
|
-
def rename_branch_environments(old_ref, new_ref)
|
|
360
|
+
def rename_branch_environments(old_ref, new_ref, replace_existing: false)
|
|
361
|
+
records = state.fetch("worktrees", {}).values
|
|
362
|
+
return unless records.any? { |record| record.fetch("environments", {}).key?(old_ref) }
|
|
363
|
+
|
|
364
|
+
remove_branch_environments(new_ref) if replace_existing
|
|
329
365
|
state.fetch("worktrees", {}).each_key do |id|
|
|
330
366
|
with_worktree_lock(id) do
|
|
331
367
|
with_state_lock do |data|
|
data/lib/brunch.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brunch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maciej Ciemborowicz
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- brunch.webp
|
|
93
93
|
- exe/brunch
|
|
94
94
|
- lib/brunch.rb
|
|
95
|
+
- lib/brunch/branch_settlement.rb
|
|
95
96
|
- lib/brunch/cli.rb
|
|
96
97
|
- lib/brunch/hooks.rb
|
|
97
98
|
- lib/brunch/managers.rb
|