gitbroker 0.7.0 → 0.8.0
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/lib/gitbroker/agents/claude.rb +10 -2
- data/lib/gitbroker/cli.rb +3 -1
- data/lib/gitbroker/config.rb +6 -1
- data/lib/gitbroker/daemon.rb +26 -11
- data/lib/gitbroker/process_scanner.rb +16 -15
- data/lib/gitbroker/reaper.rb +111 -0
- data/lib/gitbroker/repo_scanner.rb +13 -7
- data/lib/gitbroker/task_runner.rb +5 -1
- data/lib/gitbroker/version.rb +1 -1
- data/lib/gitbroker.rb +1 -0
- data/plugin/.claude-plugin/plugin.json +1 -1
- 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: b20e2186e4b0e888878382bcced5b98530f92b642b69a1e20cab0add06301e58
|
|
4
|
+
data.tar.gz: 0da49468966d94932958c778386e432dd00b4feb9c0ee3e0432380100f472bf4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4515180233432eabd09be57c026794ca06c1141f82bb41da8097cc8b76d80e47464c6aaf6bd27216dcd3f580d3872b0287d333a9dea1dadcb0b86592eea745df
|
|
7
|
+
data.tar.gz: '09ec99fb2e88657b6b80a0eaccd37f62be01d84cdd3c4f1fd4814a2fb27425ee1610cae031bd37805b009c65ee731599f2b00dd5ca8d4fb653b6ef7ed6d6f431'
|
|
@@ -6,9 +6,17 @@ module Gitbroker
|
|
|
6
6
|
# credit pool pays for it (spec §2). Terminal: interactive `claude`.
|
|
7
7
|
class Claude < Base
|
|
8
8
|
PLUGIN_DIR = File.join(Gitbroker::ROOT, "plugin")
|
|
9
|
+
# A bare `Read` in --allowedTools means every file on the machine, so a prompt-injected agent could read
|
|
10
|
+
# ~/.aws/credentials. `Read(./**)` is the worktree only (Grep and Glob follow Read rules), the settings flag makes
|
|
11
|
+
# the file tools refuse anything outside the working directory in every mode (a symlink out of the worktree
|
|
12
|
+
# included), and the deny rules name the usual secrets. Either of the first two alone was verified to block
|
|
13
|
+
# reads outside the worktree, in Read, Grep and Glob.
|
|
14
|
+
SECRET_PATHS = %w[Read(~/.ssh/**) Read(//**/.aws/**) Read(~/.gnupg/**) Read(~/.config/**) Read(~/.netrc)
|
|
15
|
+
Read(~/.git-credentials) Read(//**/.env)].freeze
|
|
9
16
|
READ_ONLY_ARGS = [ "--permission-mode", "dontAsk", "--permission-prompts", "none",
|
|
10
|
-
"--allowedTools", "Read,
|
|
11
|
-
"--disallowedTools",
|
|
17
|
+
"--allowedTools", "Read(./**),mcp__gitbroker__*",
|
|
18
|
+
"--disallowedTools", [ *%w[Bash Edit Write NotebookEdit WebFetch WebSearch], *SECRET_PATHS ].join(","),
|
|
19
|
+
"--settings", { permissions: { blockReadsOutsideWorkingDirectories: true } }.to_json ].freeze
|
|
12
20
|
READS = %w[Read NotebookRead].freeze
|
|
13
21
|
EDITS = %w[Edit MultiEdit Write NotebookEdit].freeze
|
|
14
22
|
|
data/lib/gitbroker/cli.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Gitbroker
|
|
|
26
26
|
"Start companion" handler (`start` does this when needed)
|
|
27
27
|
install-skill Install the gitbroker skills into ~/.claude/skills
|
|
28
28
|
status Show pairing, configuration and the repositories found
|
|
29
|
-
clean Remove finished agent worktrees (keeps
|
|
29
|
+
clean Remove finished agent worktrees and stop their leftover processes (keeps worktrees with uncommitted work)
|
|
30
30
|
install-hooks [--yes] Show your own Claude Code sessions on your cards (asks first)
|
|
31
31
|
uninstall-hooks Remove those hooks
|
|
32
32
|
help Show this help
|
|
@@ -261,6 +261,8 @@ module Gitbroker
|
|
|
261
261
|
# `git worktree remove` without --force refuses a worktree with modified or untracked files, so work is never lost.
|
|
262
262
|
def clean
|
|
263
263
|
worktree = Worktree.new(root: config.worktree_root)
|
|
264
|
+
stopped = Reaper.new(root: config.worktree_root).sweep!
|
|
265
|
+
@out.puts "Stopped #{stopped} leftover process#{"es" unless stopped == 1} (rubocop --server and the like)." if stopped.positive?
|
|
264
266
|
worktree.list.each do |dir|
|
|
265
267
|
git_dir = GitContext.git_dir(dir)
|
|
266
268
|
repo_path = git_dir && File.dirname(GitContext.common_dir(git_dir))
|
data/lib/gitbroker/config.rb
CHANGED
|
@@ -63,8 +63,13 @@ module Gitbroker
|
|
|
63
63
|
def cable_url = "#{server.sub(/\Ahttp/, "ws")}/cable"
|
|
64
64
|
# `roots` in config.yml and the folders added in Git Broker (`device_roots`) add to the usual folders; the checkouts Claude Code and Codex know come in through `repo_scanner`.
|
|
65
65
|
def device_roots = self.class.clean_device_roots(data["device_roots"], home:)
|
|
66
|
+
def ignored_roots = self.class.clean_device_roots(data["ignored_roots"], home:)
|
|
66
67
|
def roots = (self.class.common_roots(home) + Array(data["roots"]).map { File.expand_path(_1.to_s.sub(/\A~/, home)) } + device_roots).uniq
|
|
67
|
-
|
|
68
|
+
# Folders the user removed in Git Broker (`ignored_roots`) are skipped everywhere except inside a folder they added.
|
|
69
|
+
def repo_scanner
|
|
70
|
+
RepoScanner.new(roots: roots - device_roots, explicit_roots: device_roots, depth: scan_depth, exclude: [ worktree_root ],
|
|
71
|
+
ignored: ignored_roots, hints: AgentProjects.paths(home:), home:)
|
|
72
|
+
end
|
|
68
73
|
def scan_depth = Integer(data["scan_depth"] || 3)
|
|
69
74
|
def terminal = (data["terminal"] || (@platform == "macos" ? "terminal" : "tmux")).to_s
|
|
70
75
|
def worktree_root = File.expand_path(data["worktree_root"] || File.join(home, ".local/share/gitbroker/worktrees"))
|
data/lib/gitbroker/daemon.rb
CHANGED
|
@@ -23,18 +23,18 @@ module Gitbroker
|
|
|
23
23
|
process_scanner: build_process_scanner(config))
|
|
24
24
|
raise Error, "This machine is not paired. Run `gitbroker start` in a terminal to pair it." unless config.paired?
|
|
25
25
|
|
|
26
|
-
new(scanner: config.repo_scanner, roots_changed: roots_changed(config), device_roots: config.device_roots,
|
|
26
|
+
new(scanner: config.repo_scanner, roots_changed: roots_changed(config), device_roots: config.device_roots, ignored_roots: config.ignored_roots,
|
|
27
27
|
client_factory: -> { CableClient.new(url: config.cable_url, token: config.device_token, origin: config.server) },
|
|
28
|
-
task_runner_factory: ->(report) { TaskRunner.new(config:, report:, out:) }, out:, relay:, process_scanner:,
|
|
29
|
-
lifecycle: Lifecycle.new(dir: config.dir, server: config.server))
|
|
28
|
+
task_runner_factory: ->(report) { TaskRunner.new(config:, report:, out:) }, out:, relay:, process_scanner:, agent: config.agent,
|
|
29
|
+
lifecycle: Lifecycle.new(dir: config.dir, server: config.server), reaper: Reaper.new(root: config.worktree_root))
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# The server's "roots" message: remember the folders in config.yml (so they survive a restart without the server) and
|
|
33
33
|
# scan with them from now on. Returns the new scanner.
|
|
34
34
|
def self.roots_changed(config)
|
|
35
35
|
current = config
|
|
36
|
-
lambda do |roots|
|
|
37
|
-
current = current.save!("device_roots" => roots.empty? ? nil : roots)
|
|
36
|
+
lambda do |roots, ignored|
|
|
37
|
+
current = current.save!("device_roots" => roots.empty? ? nil : roots, "ignored_roots" => ignored.empty? ? nil : ignored)
|
|
38
38
|
current.repo_scanner
|
|
39
39
|
end
|
|
40
40
|
end
|
|
@@ -54,9 +54,10 @@ module Gitbroker
|
|
|
54
54
|
|
|
55
55
|
def initialize(scanner:, client_factory:, task_runner_factory:, out: $stdout, sleeper: ->(seconds) { sleep(seconds) },
|
|
56
56
|
clock: -> { Time.now }, spawner: ->(&block) { Thread.new(&block) }, relay: nil, process_scanner: nil, lifecycle: nil,
|
|
57
|
-
maintenance: Maintenance.new, roots_changed: nil, device_roots: [])
|
|
57
|
+
maintenance: Maintenance.new, roots_changed: nil, device_roots: [], ignored_roots: [], agent: "claude", reaper: nil)
|
|
58
58
|
@scanner = scanner
|
|
59
59
|
@device_roots = device_roots
|
|
60
|
+
@ignored_roots = ignored_roots
|
|
60
61
|
@roots_changed = roots_changed
|
|
61
62
|
@client_factory = client_factory
|
|
62
63
|
@task_runner = task_runner_factory.call(method(:report))
|
|
@@ -68,6 +69,8 @@ module Gitbroker
|
|
|
68
69
|
@process_scanner = process_scanner
|
|
69
70
|
@lifecycle = lifecycle
|
|
70
71
|
@maintenance = maintenance
|
|
72
|
+
@agent = agent
|
|
73
|
+
@reaper = reaper
|
|
71
74
|
@repositories = []
|
|
72
75
|
@running = Set.new
|
|
73
76
|
@running_lock = Mutex.new
|
|
@@ -78,6 +81,7 @@ module Gitbroker
|
|
|
78
81
|
@rejections = 0
|
|
79
82
|
@relay&.start
|
|
80
83
|
@lifecycle&.write_pid
|
|
84
|
+
sweep_strays
|
|
81
85
|
until @stopping
|
|
82
86
|
begin
|
|
83
87
|
@client = @client_factory.call
|
|
@@ -120,7 +124,7 @@ module Gitbroker
|
|
|
120
124
|
@rejections = 0
|
|
121
125
|
rescan(client)
|
|
122
126
|
client.perform("hello", "version" => VERSION, "platform" => Gitbroker.platform,
|
|
123
|
-
"service" => @lifecycle ? @lifecycle.service_installed? : false)
|
|
127
|
+
"service" => @lifecycle ? @lifecycle.service_installed? : false, "agent" => @agent)
|
|
124
128
|
@out.puts "Connected. #{@repositories.size} repositories reported."
|
|
125
129
|
end
|
|
126
130
|
client.on_tick do
|
|
@@ -221,7 +225,7 @@ module Gitbroker
|
|
|
221
225
|
|
|
222
226
|
def handle(client, message)
|
|
223
227
|
return control(message["action"]) if message["type"] == "control"
|
|
224
|
-
return apply_roots(client, message["roots"]) if message["type"] == "roots"
|
|
228
|
+
return apply_roots(client, message["roots"], message["ignored"]) if message["type"] == "roots"
|
|
225
229
|
|
|
226
230
|
id = message["task_id"]
|
|
227
231
|
return unless id.is_a?(String)
|
|
@@ -234,14 +238,16 @@ module Gitbroker
|
|
|
234
238
|
end
|
|
235
239
|
|
|
236
240
|
# Sent on every connection and whenever the user changes the list. Nothing happens when it is what we already scan.
|
|
237
|
-
def apply_roots(client, roots)
|
|
241
|
+
def apply_roots(client, roots, ignored)
|
|
238
242
|
return unless @roots_changed
|
|
239
243
|
|
|
240
244
|
clean = Config.clean_device_roots(roots, home: Dir.home)
|
|
241
|
-
|
|
245
|
+
clean_ignored = Config.clean_device_roots(ignored, home: Dir.home)
|
|
246
|
+
return if clean == @device_roots && clean_ignored == @ignored_roots
|
|
242
247
|
|
|
243
248
|
@device_roots = clean
|
|
244
|
-
@
|
|
249
|
+
@ignored_roots = clean_ignored
|
|
250
|
+
@scanner = @roots_changed.call(clean, clean_ignored)
|
|
245
251
|
rescan(client)
|
|
246
252
|
end
|
|
247
253
|
|
|
@@ -256,7 +262,16 @@ module Gitbroker
|
|
|
256
262
|
ensure
|
|
257
263
|
upgrade_now = @running_lock.synchronize { @running.delete(id) && @pending_upgrade && @running.empty? }
|
|
258
264
|
upgrade if upgrade_now
|
|
265
|
+
sweep_strays
|
|
259
266
|
end
|
|
260
267
|
end
|
|
268
|
+
|
|
269
|
+
# Helpers an agent left in worktrees nobody works in (rubocop --server and the like). Only while no task runs here.
|
|
270
|
+
def sweep_strays
|
|
271
|
+
return unless @reaper && @running_lock.synchronize { @running.empty? }
|
|
272
|
+
|
|
273
|
+
stopped = @reaper.sweep!
|
|
274
|
+
@out.puts "Stopped #{stopped} leftover process#{"es" unless stopped == 1} in finished task worktrees." if stopped.positive?
|
|
275
|
+
end
|
|
261
276
|
end
|
|
262
277
|
end
|
|
@@ -44,6 +44,22 @@ module Gitbroker
|
|
|
44
44
|
running + exited
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# pid => cwd for the given pids (lsof on macOS, /proc on Linux); a pid that vanished is left out.
|
|
48
|
+
def working_dirs(pids)
|
|
49
|
+
return {} if pids.empty?
|
|
50
|
+
return pids.to_h { [ _1, proc_cwd(_1) ] }.compact if @platform == "linux"
|
|
51
|
+
|
|
52
|
+
cwds = {}
|
|
53
|
+
pid = nil
|
|
54
|
+
@runner.capture("lsof", "-a", "-d", "cwd", "-Fn", "-p", pids.sort.join(",")).each_line do |line|
|
|
55
|
+
case line[0]
|
|
56
|
+
when "p" then pid = line[1..].to_i
|
|
57
|
+
when "n" then cwds[pid] = line[1..].chomp if pid
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
cwds
|
|
61
|
+
end
|
|
62
|
+
|
|
47
63
|
private
|
|
48
64
|
|
|
49
65
|
# { "proc-<pid>" => { agent:, context: } }, one per (agent, cwd), only in GitHub checkouts outside `exclude`.
|
|
@@ -79,21 +95,6 @@ module Gitbroker
|
|
|
79
95
|
{}
|
|
80
96
|
end
|
|
81
97
|
|
|
82
|
-
def working_dirs(pids)
|
|
83
|
-
return {} if pids.empty?
|
|
84
|
-
return pids.to_h { [ _1, proc_cwd(_1) ] }.compact if @platform == "linux"
|
|
85
|
-
|
|
86
|
-
cwds = {}
|
|
87
|
-
pid = nil
|
|
88
|
-
@runner.capture("lsof", "-a", "-d", "cwd", "-Fn", "-p", pids.sort.join(",")).each_line do |line|
|
|
89
|
-
case line[0]
|
|
90
|
-
when "p" then pid = line[1..].to_i
|
|
91
|
-
when "n" then cwds[pid] = line[1..].chomp if pid
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
cwds
|
|
95
|
-
end
|
|
96
|
-
|
|
97
98
|
def proc_cwd(pid)
|
|
98
99
|
File.readlink(File.join(@proc_root, pid.to_s, "cwd"))
|
|
99
100
|
rescue SystemCallError
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitbroker
|
|
4
|
+
# Stops the processes an agent leaves behind in a task worktree. Tools such as `rubocop --server`, spring or a language
|
|
5
|
+
# server detach from the agent (their parent becomes launchd/init), so they outlive it and its process group, keep the
|
|
6
|
+
# worktree as their working directory and pile up for days. Only processes whose working directory is inside the
|
|
7
|
+
# worktree root are ever touched, and only by pid: a process is never found by name or killed by group.
|
|
8
|
+
class Reaper
|
|
9
|
+
GRACE = 3
|
|
10
|
+
POLL = 0.1
|
|
11
|
+
INIT = /\A(?:\S*\/)?(?:systemd|launchd)\b/
|
|
12
|
+
|
|
13
|
+
def initialize(root:, runner: Runner.new, platform: Gitbroker.platform, proc_root: "/proc", grace: GRACE,
|
|
14
|
+
signal: Process.method(:kill), sleeper: ->(seconds) { sleep(seconds) })
|
|
15
|
+
@root = real(root)
|
|
16
|
+
@runner = runner
|
|
17
|
+
@scanner = ProcessScanner.new(runner:, platform:, proc_root:)
|
|
18
|
+
@grace = grace
|
|
19
|
+
@signal = signal
|
|
20
|
+
@sleeper = sleeper
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A task ended in `dir`: whatever still runs there is a stray. Returns how many processes were stopped.
|
|
24
|
+
def reap!(dir)
|
|
25
|
+
dir = real(dir)
|
|
26
|
+
return 0 unless inside?(dir, @root)
|
|
27
|
+
|
|
28
|
+
stop(processes.select { inside?(_1[:cwd], dir) }.map { _1[:pid] })
|
|
29
|
+
rescue Error, SystemCallError
|
|
30
|
+
0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Worktrees nobody works in (no agent process inside): their orphaned helpers are strays. A worktree with a live
|
|
34
|
+
# agent (a terminal run the user is still in) is left alone. Call it only when no task is running here.
|
|
35
|
+
def sweep!
|
|
36
|
+
pids = processes.group_by { worktree_of(_1[:cwd]) }.flat_map do |dir, inside|
|
|
37
|
+
next [] if dir.nil? || inside.any? { ProcessScanner.agent_for(_1[:args]) }
|
|
38
|
+
|
|
39
|
+
inside.select { _1[:orphan] }.map { _1[:pid] }
|
|
40
|
+
end
|
|
41
|
+
stop(pids)
|
|
42
|
+
rescue Error, SystemCallError
|
|
43
|
+
0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# [{ pid:, args:, cwd:, orphan: }] for every process with a working directory under the worktree root.
|
|
49
|
+
def processes
|
|
50
|
+
table = @runner.run!("ps", "-Ao", "pid=,ppid=,args=").lines.filter_map do |line|
|
|
51
|
+
pid, ppid, args = line.strip.split(" ", 3)
|
|
52
|
+
next unless pid.to_s.match?(/\A\d+\z/) && ppid.to_s.match?(/\A\d+\z/) && pid.to_i > 1 && pid.to_i != Process.pid
|
|
53
|
+
|
|
54
|
+
{ pid: pid.to_i, ppid: ppid.to_i, args: args.to_s }
|
|
55
|
+
end
|
|
56
|
+
args_of = table.to_h { [ _1[:pid], _1[:args] ] }
|
|
57
|
+
cwds = @scanner.working_dirs(table.map { _1[:pid] })
|
|
58
|
+
table.filter_map do |process|
|
|
59
|
+
cwd = cwds[process[:pid]]
|
|
60
|
+
next unless cwd && inside?(cwd, @root)
|
|
61
|
+
|
|
62
|
+
process.merge(cwd:, orphan: process[:ppid] == 1 || args_of[process[:ppid]].to_s.match?(INIT))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# <root>/<owner>-<repo>/pr-<n>-<kind>, whatever depth the process is working at.
|
|
67
|
+
def worktree_of(cwd)
|
|
68
|
+
return unless cwd.start_with?("#{@root}/")
|
|
69
|
+
|
|
70
|
+
owner_repo, pr = cwd.delete_prefix("#{@root}/").split("/", 3).first(2)
|
|
71
|
+
File.join(@root, owner_repo, pr) if pr
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def stop(pids)
|
|
75
|
+
pids = pids.uniq.select { deliver("TERM", _1) }
|
|
76
|
+
return 0 if pids.empty?
|
|
77
|
+
|
|
78
|
+
(@grace / POLL).ceil.times do
|
|
79
|
+
break if pids.none? { alive?(_1) }
|
|
80
|
+
|
|
81
|
+
@sleeper.call(POLL)
|
|
82
|
+
end
|
|
83
|
+
pids.each { deliver("KILL", _1) if alive?(_1) }
|
|
84
|
+
pids.size
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def deliver(name, pid)
|
|
88
|
+
@signal.call(name, pid)
|
|
89
|
+
true
|
|
90
|
+
rescue SystemCallError
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def alive?(pid)
|
|
95
|
+
@signal.call(0, pid)
|
|
96
|
+
true
|
|
97
|
+
rescue Errno::EPERM
|
|
98
|
+
true
|
|
99
|
+
rescue SystemCallError
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def inside?(path, dir) = path == dir || path.start_with?("#{dir}/")
|
|
104
|
+
|
|
105
|
+
def real(path)
|
|
106
|
+
File.realpath(path)
|
|
107
|
+
rescue SystemCallError
|
|
108
|
+
File.expand_path(path)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -19,19 +19,25 @@ module Gitbroker
|
|
|
19
19
|
HINT_LEVELS = 4
|
|
20
20
|
HINT_PARENT_DEPTH = 2
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
# `exclude` (the task worktrees) is skipped everywhere. `ignored` (folders the user removed) is skipped by everything
|
|
23
|
+
# found by itself; `explicit_roots` (folders the user added) are scanned even inside an ignored folder.
|
|
24
|
+
def initialize(roots:, depth: 3, exclude: [], hints: [], home: Dir.home, explicit_roots: [], ignored: [])
|
|
23
25
|
@roots = roots
|
|
26
|
+
@explicit_roots = explicit_roots
|
|
27
|
+
@ignored = ignored.map { File.expand_path(_1) }
|
|
24
28
|
@hints = hints
|
|
25
29
|
@home = File.expand_path(home)
|
|
26
30
|
@depth = depth
|
|
27
31
|
@exclude = exclude.map { File.expand_path(_1) }
|
|
32
|
+
@skip = @exclude + @ignored
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def scan
|
|
31
36
|
found = {}
|
|
32
|
-
@roots.each { |root| walk(File.expand_path(root), 0, found) if File.directory?(root) }
|
|
37
|
+
@roots.each { |root| walk(File.expand_path(root), 0, found, @depth, @skip) if File.directory?(root) }
|
|
38
|
+
@explicit_roots.each { |root| walk(File.expand_path(root), 0, found, @depth, @exclude) if File.directory?(root) }
|
|
33
39
|
parents = @hints.filter_map { |hint| add_hint(File.expand_path(hint), found) }.map { File.dirname(_1) }.uniq
|
|
34
|
-
parents.each { |parent| walk(parent, 0, found, HINT_PARENT_DEPTH) if scannable_parent?(parent) }
|
|
40
|
+
parents.each { |parent| walk(parent, 0, found, HINT_PARENT_DEPTH, @skip) if scannable_parent?(parent) }
|
|
35
41
|
found.sort.map { |full_name, path| { "full_name" => full_name, "path" => path } }
|
|
36
42
|
end
|
|
37
43
|
|
|
@@ -40,7 +46,7 @@ module Gitbroker
|
|
|
40
46
|
# Records the checkout a hint is in and returns its directory (nil when it is not in one).
|
|
41
47
|
def add_hint(dir, found)
|
|
42
48
|
HINT_LEVELS.times do
|
|
43
|
-
return nil if @
|
|
49
|
+
return nil if @skip.any? { |skipped| dir == skipped || dir.start_with?("#{skipped}/") }
|
|
44
50
|
return nil unless File.directory?(dir)
|
|
45
51
|
|
|
46
52
|
if File.exist?(File.join(dir, ".git"))
|
|
@@ -58,8 +64,8 @@ module Gitbroker
|
|
|
58
64
|
!(dir == @home || dir == "/" || @home.start_with?("#{dir}/"))
|
|
59
65
|
end
|
|
60
66
|
|
|
61
|
-
def walk(dir, level, found, limit
|
|
62
|
-
return if
|
|
67
|
+
def walk(dir, level, found, limit, skip)
|
|
68
|
+
return if skip.any? { |skipped| dir == skipped || dir.start_with?("#{skipped}/") }
|
|
63
69
|
|
|
64
70
|
if File.exist?(File.join(dir, ".git"))
|
|
65
71
|
remote_names(dir).each { |name| found[name] = dir if found[name].nil? || dir.length < found[name].length }
|
|
@@ -71,7 +77,7 @@ module Gitbroker
|
|
|
71
77
|
next if child.start_with?(".") || SKIP.include?(child)
|
|
72
78
|
|
|
73
79
|
path = File.join(dir, child)
|
|
74
|
-
walk(path, level + 1, found, limit) if File.directory?(path) && !File.symlink?(path)
|
|
80
|
+
walk(path, level + 1, found, limit, skip) if File.directory?(path) && !File.symlink?(path)
|
|
75
81
|
end
|
|
76
82
|
rescue Errno::EACCES, Errno::ENOENT, Errno::ENOTDIR
|
|
77
83
|
nil
|
|
@@ -16,7 +16,8 @@ module Gitbroker
|
|
|
16
16
|
|
|
17
17
|
def initialize(config:, report:, worktree: Worktree.new(root: config.worktree_root), neutralizer: Neutralizer.new,
|
|
18
18
|
gh_check: GhCheck.new, launcher: Launcher.new(name: config.terminal), out: $stdout, grace: CANCEL_GRACE,
|
|
19
|
-
progress_interval: PROGRESS_INTERVAL, run_script: RunScript.new(state_dir: config.state_dir)
|
|
19
|
+
progress_interval: PROGRESS_INTERVAL, run_script: RunScript.new(state_dir: config.state_dir),
|
|
20
|
+
reaper: Reaper.new(root: config.worktree_root))
|
|
20
21
|
@config = config
|
|
21
22
|
@report = report
|
|
22
23
|
@worktree = worktree
|
|
@@ -27,6 +28,7 @@ module Gitbroker
|
|
|
27
28
|
@grace = grace
|
|
28
29
|
@progress_interval = progress_interval
|
|
29
30
|
@run_script = run_script
|
|
31
|
+
@reaper = reaper
|
|
30
32
|
@handles = {}
|
|
31
33
|
@running = Set.new
|
|
32
34
|
@cancelled = Set.new
|
|
@@ -74,6 +76,8 @@ module Gitbroker
|
|
|
74
76
|
ensure
|
|
75
77
|
files.cleanup!
|
|
76
78
|
end
|
|
79
|
+
stopped = @reaper.reap!(dir)
|
|
80
|
+
say(id, "Stopped #{stopped} leftover process#{"es" unless stopped == 1} in #{dir}") if stopped.positive?
|
|
77
81
|
if @worktree.remove!(dir, repo_path:)
|
|
78
82
|
say(id, "Removed worktree #{dir}")
|
|
79
83
|
else
|
data/lib/gitbroker/version.rb
CHANGED
data/lib/gitbroker.rb
CHANGED
|
@@ -34,6 +34,7 @@ module Gitbroker
|
|
|
34
34
|
autoload :Login, File.expand_path("gitbroker/login", __dir__)
|
|
35
35
|
autoload :Neutralizer, File.expand_path("gitbroker/neutralizer", __dir__)
|
|
36
36
|
autoload :ProcessScanner, File.expand_path("gitbroker/process_scanner", __dir__)
|
|
37
|
+
autoload :Reaper, File.expand_path("gitbroker/reaper", __dir__)
|
|
37
38
|
autoload :RepoScanner, File.expand_path("gitbroker/repo_scanner", __dir__)
|
|
38
39
|
autoload :RunScript, File.expand_path("gitbroker/run_script", __dir__)
|
|
39
40
|
autoload :Runner, File.expand_path("gitbroker/runner", __dir__)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitbroker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yuri Sidorov (@newstler)
|
|
@@ -58,6 +58,7 @@ files:
|
|
|
58
58
|
- lib/gitbroker/maintenance.rb
|
|
59
59
|
- lib/gitbroker/neutralizer.rb
|
|
60
60
|
- lib/gitbroker/process_scanner.rb
|
|
61
|
+
- lib/gitbroker/reaper.rb
|
|
61
62
|
- lib/gitbroker/repo_scanner.rb
|
|
62
63
|
- lib/gitbroker/run_script.rb
|
|
63
64
|
- lib/gitbroker/runner.rb
|