bonchi 0.6.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/bonchi/cli.rb +16 -9
- data/lib/bonchi/config.rb +13 -5
- data/lib/bonchi/git.rb +14 -0
- data/lib/bonchi/port_pool.rb +17 -1
- data/lib/bonchi/setup.rb +62 -38
- data/lib/bonchi/version.rb +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '053922f8049bc9788caaf72af772fcd9058945e2964183228c339922109029f4'
|
|
4
|
+
data.tar.gz: b2058d5ed48143f1eb54c42ecb614087bd4e3a23a461a31d0c749146366454cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24cdf46ea9f71a1bfec30f23d2765b51a1dc036e06059693f14379b192822fd850a28ab667e8439a65ed20cb2cd5b4fd1c818213c5d5c34e46ff9c03d3cbdaa5
|
|
7
|
+
data.tar.gz: a512b18c8ba6e77f5cbbeaa8584738c1f41a3754fc08ffaa9f6a1a04d76a753194ec55837ce413545b351f59936b1640c1c53d91ac68bc9d40f75a604b04c096
|
data/lib/bonchi/cli.rb
CHANGED
|
@@ -97,6 +97,7 @@ module Bonchi
|
|
|
97
97
|
|
|
98
98
|
Git.fetch_pr(pr_number)
|
|
99
99
|
Git.worktree_add(path, branch)
|
|
100
|
+
Git.set_pr_upstream(pr_number)
|
|
100
101
|
puts "PR ##{pr_number} checked out at: #{path}"
|
|
101
102
|
|
|
102
103
|
signal_cd(path)
|
|
@@ -114,7 +115,9 @@ module Bonchi
|
|
|
114
115
|
which ports to allocate, and what setup command to run.
|
|
115
116
|
DESC
|
|
116
117
|
def init
|
|
117
|
-
|
|
118
|
+
root = Git.toplevel
|
|
119
|
+
abort "Error: not inside a git repository" unless root
|
|
120
|
+
path = File.join(root, ".worktree.yml")
|
|
118
121
|
if File.exist?(path)
|
|
119
122
|
abort "Error: .worktree.yml already exists"
|
|
120
123
|
end
|
|
@@ -281,17 +284,21 @@ module Bonchi
|
|
|
281
284
|
# ports:
|
|
282
285
|
# - PORT
|
|
283
286
|
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
#
|
|
287
|
+
# Edits applied to files. Env vars ($VAR) are expanded.
|
|
288
|
+
# Three actions: regex replace, append, upsert (replace-or-append).
|
|
289
|
+
# edit:
|
|
287
290
|
# .env.local:
|
|
291
|
+
# # Replace — short form
|
|
288
292
|
# - "^PORT=.*": "PORT=$PORT"
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
# - match: "^PORT=.*"
|
|
293
|
-
# with: "PORT=$PORT"
|
|
293
|
+
# # Replace — full form (with optional missing: warn, default: halt)
|
|
294
|
+
# - match: "^DATABASE_URL=.*"
|
|
295
|
+
# with: "DATABASE_URL=postgres:///myapp_$WORKTREE_BRANCH_SLUG"
|
|
294
296
|
# missing: warn
|
|
297
|
+
# # Append a line unconditionally
|
|
298
|
+
# - append: "FOO=bar"
|
|
299
|
+
# # Replace if pattern matches, otherwise append
|
|
300
|
+
# - upsert: "^DEBUG="
|
|
301
|
+
# with: "DEBUG=1"
|
|
295
302
|
|
|
296
303
|
# Commands to run before the setup command (port env vars are available).
|
|
297
304
|
# pre_setup:
|
data/lib/bonchi/config.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Bonchi
|
|
|
4
4
|
class Config
|
|
5
5
|
include Colors
|
|
6
6
|
|
|
7
|
-
KNOWN_KEYS = %w[min_version copy link ports replace pre_setup setup].freeze
|
|
7
|
+
KNOWN_KEYS = %w[min_version copy link ports replace edit pre_setup setup].freeze
|
|
8
8
|
|
|
9
9
|
attr_reader :copy, :link, :ports, :replace, :pre_setup, :setup
|
|
10
10
|
|
|
@@ -16,10 +16,14 @@ module Bonchi
|
|
|
16
16
|
|
|
17
17
|
check_min_version!(data["min_version"]) if data["min_version"]
|
|
18
18
|
|
|
19
|
+
if data.key?("replace") && data.key?("edit")
|
|
20
|
+
abort "#{color(:red)}Error:#{reset} both 'edit' and 'replace' set in .worktree.yml — use 'edit' (preferred)"
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
@copy = Array(data["copy"])
|
|
20
24
|
@link = Array(data["link"])
|
|
21
25
|
@ports = Array(data["ports"])
|
|
22
|
-
@replace = data["replace"] || {}
|
|
26
|
+
@replace = data["edit"] || data["replace"] || {}
|
|
23
27
|
@pre_setup = Array(data["pre_setup"])
|
|
24
28
|
@setup = data["setup"] || "bin/setup"
|
|
25
29
|
|
|
@@ -49,17 +53,21 @@ module Bonchi
|
|
|
49
53
|
|
|
50
54
|
def validate!
|
|
51
55
|
unless @replace.is_a?(Hash)
|
|
52
|
-
abort "#{color(:red)}Error:#{reset} '
|
|
56
|
+
abort "#{color(:red)}Error:#{reset} 'edit' must be a mapping of filename to list of edits"
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
@replace.each do |file, entries|
|
|
56
60
|
unless entries.is_a?(Array)
|
|
57
|
-
abort "#{color(:red)}Error:#{reset} '
|
|
61
|
+
abort "#{color(:red)}Error:#{reset} 'edit.#{file}' must be a list of edits"
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
entries.each do |entry|
|
|
61
65
|
unless entry.is_a?(Hash)
|
|
62
|
-
abort "#{color(:red)}Error:#{reset} each
|
|
66
|
+
abort "#{color(:red)}Error:#{reset} each edit in 'edit.#{file}' must be a mapping"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if entry.key?("upsert") && !entry.key?("with")
|
|
70
|
+
abort "#{color(:red)}Error:#{reset} 'upsert' in 'edit.#{file}' requires a 'with' value"
|
|
63
71
|
end
|
|
64
72
|
end
|
|
65
73
|
end
|
data/lib/bonchi/git.rb
CHANGED
|
@@ -24,6 +24,11 @@ module Bonchi
|
|
|
24
24
|
`git worktree list --porcelain`.lines.first.sub("worktree ", "").strip
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def toplevel
|
|
28
|
+
root = `git rev-parse --show-toplevel 2>/dev/null`.strip
|
|
29
|
+
root.empty? ? nil : root
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
def worktree_list
|
|
28
33
|
`git worktree list`.lines.map(&:strip).reject(&:empty?)
|
|
29
34
|
end
|
|
@@ -87,6 +92,15 @@ module Bonchi
|
|
|
87
92
|
system("git", "fetch", "origin", "pull/#{pr_number}/head:pr-#{pr_number}")
|
|
88
93
|
end
|
|
89
94
|
|
|
95
|
+
def set_pr_upstream(pr_number)
|
|
96
|
+
head_ref = `gh pr view #{pr_number} --json headRefName -q .headRefName`.strip
|
|
97
|
+
return if head_ref.empty?
|
|
98
|
+
|
|
99
|
+
if system("git", "branch", "--set-upstream-to", "origin/#{head_ref}", "pr-#{pr_number}")
|
|
100
|
+
puts "Upstream set to origin/#{head_ref}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
90
104
|
def worktree_dir(branch)
|
|
91
105
|
File.join(GlobalConfig.new.worktree_root, repo_name, branch)
|
|
92
106
|
end
|
data/lib/bonchi/port_pool.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "set"
|
|
1
2
|
require "yaml"
|
|
2
3
|
require "socket"
|
|
3
4
|
|
|
@@ -6,6 +7,21 @@ module Bonchi
|
|
|
6
7
|
DEFAULT_MIN = 4000
|
|
7
8
|
DEFAULT_MAX = 5000
|
|
8
9
|
|
|
10
|
+
# Ports browsers refuse to connect to (Chrome: ERR_UNSAFE_PORT, Firefox: a
|
|
11
|
+
# similar block). Allocating one of these for a dev web server makes it
|
|
12
|
+
# unreachable in the browser even though the server binds fine. This is
|
|
13
|
+
# Chromium's kRestrictedPorts list (net/base/port_util.cc); ports outside
|
|
14
|
+
# the pool range are harmless to keep listed. 4045 (lockd) and 4190 (sieve)
|
|
15
|
+
# fall inside the default 4000..5000 range.
|
|
16
|
+
UNSAFE_PORTS = [
|
|
17
|
+
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77,
|
|
18
|
+
79, 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123,
|
|
19
|
+
135, 137, 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526,
|
|
20
|
+
530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
|
|
21
|
+
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566,
|
|
22
|
+
6665, 6666, 6667, 6668, 6669, 6697, 10080
|
|
23
|
+
].to_set.freeze
|
|
24
|
+
|
|
9
25
|
def initialize
|
|
10
26
|
@path = GlobalConfig.config_path
|
|
11
27
|
load_config
|
|
@@ -26,7 +42,7 @@ module Bonchi
|
|
|
26
42
|
|
|
27
43
|
new_ports = {}
|
|
28
44
|
port_names.each do |name|
|
|
29
|
-
port = (@min..@max).find { |p| !used.include?(p) && port_available?(p) }
|
|
45
|
+
port = (@min..@max).find { |p| !used.include?(p) && !UNSAFE_PORTS.include?(p) && port_available?(p) }
|
|
30
46
|
abort "Error: no available port for #{name}" unless port
|
|
31
47
|
used << port
|
|
32
48
|
new_ports[name] = port
|
data/lib/bonchi/setup.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Bonchi
|
|
|
6
6
|
include Colors
|
|
7
7
|
|
|
8
8
|
def initialize(worktree: nil)
|
|
9
|
-
@worktree = worktree || Dir.pwd
|
|
9
|
+
@worktree = worktree || Git.toplevel || Dir.pwd
|
|
10
10
|
@main_worktree = Git.main_worktree
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -21,8 +21,13 @@ module Bonchi
|
|
|
21
21
|
abort "#{color(:red)}Error:#{reset} already in the main worktree"
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
config = Config.
|
|
25
|
-
|
|
24
|
+
config = Config.from_worktree(@worktree)
|
|
25
|
+
if config
|
|
26
|
+
puts "Using .worktree.yml from linked worktree"
|
|
27
|
+
else
|
|
28
|
+
config = Config.from_main_worktree
|
|
29
|
+
abort "#{color(:red)}Error:#{reset} .worktree.yml not found in main worktree" unless config
|
|
30
|
+
end
|
|
26
31
|
|
|
27
32
|
last_step = upto || STEPS.last
|
|
28
33
|
run_steps = STEPS[0..STEPS.index(last_step)]
|
|
@@ -37,14 +42,6 @@ module Bonchi
|
|
|
37
42
|
|
|
38
43
|
copy_files(config.copy) if run_steps.include?("copy")
|
|
39
44
|
link_files(config.link) if run_steps.include?("link")
|
|
40
|
-
|
|
41
|
-
# Prefer linked worktree's .worktree.yml if it was copied or already exists
|
|
42
|
-
linked_config = Config.from_worktree(@worktree)
|
|
43
|
-
if linked_config
|
|
44
|
-
puts "Using .worktree.yml from linked worktree"
|
|
45
|
-
config = linked_config
|
|
46
|
-
end
|
|
47
|
-
|
|
48
45
|
allocate_ports(config.ports) if run_steps.include?("ports") && config.ports.any?
|
|
49
46
|
replace_in_files(config.replace) if run_steps.include?("replace") && config.replace.any?
|
|
50
47
|
run_pre_setup(config.pre_setup) if run_steps.include?("pre_setup")
|
|
@@ -96,37 +93,64 @@ module Bonchi
|
|
|
96
93
|
abort "#{color(:red)}Error:#{reset} #{file} not found" unless File.exist?(path)
|
|
97
94
|
|
|
98
95
|
content = File.read(path)
|
|
99
|
-
entries.each
|
|
100
|
-
if entry.is_a?(Hash) && entry.key?("match")
|
|
101
|
-
pattern = entry["match"]
|
|
102
|
-
replacement = entry["with"]
|
|
103
|
-
missing = entry["missing"] || "halt"
|
|
104
|
-
elsif entry.is_a?(Hash)
|
|
105
|
-
pattern, replacement = entry.first
|
|
106
|
-
missing = "halt"
|
|
107
|
-
else
|
|
108
|
-
abort "#{color(:red)}Error:#{reset} invalid replace entry in #{file}: #{entry.inspect}"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
expanded = replacement.gsub(/\$(\w+)/) { ENV[$1] || abort("#{color(:red)}Error:#{reset} $#{$1} not set") }
|
|
112
|
-
regex = Regexp.new(pattern)
|
|
113
|
-
|
|
114
|
-
unless content.match?(regex)
|
|
115
|
-
if missing == "warn"
|
|
116
|
-
puts "#{color(:yellow)}Warning:#{reset} pattern #{pattern} not found in #{file}, skipping"
|
|
117
|
-
next
|
|
118
|
-
else
|
|
119
|
-
abort "#{color(:red)}Error:#{reset} pattern #{pattern} not found in #{file}"
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
content = content.gsub(regex, expanded)
|
|
124
|
-
puts "Replaced #{pattern} in #{file}"
|
|
125
|
-
end
|
|
96
|
+
entries.each { |entry| content = apply_edit(entry, content, file) }
|
|
126
97
|
File.write(path, content)
|
|
127
98
|
end
|
|
128
99
|
end
|
|
129
100
|
|
|
101
|
+
def apply_edit(entry, content, file)
|
|
102
|
+
unless entry.is_a?(Hash)
|
|
103
|
+
abort "#{color(:red)}Error:#{reset} invalid edit entry in #{file}: #{entry.inspect}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if entry.key?("append")
|
|
107
|
+
line = expand(entry["append"])
|
|
108
|
+
puts "Appended to #{file}"
|
|
109
|
+
ensure_trailing_newline(content) + line + "\n"
|
|
110
|
+
elsif entry.key?("upsert")
|
|
111
|
+
unless entry.key?("with")
|
|
112
|
+
abort "#{color(:red)}Error:#{reset} 'upsert' requires 'with' in #{file}"
|
|
113
|
+
end
|
|
114
|
+
pattern = entry["upsert"]
|
|
115
|
+
replacement = expand(entry["with"])
|
|
116
|
+
regex = Regexp.new(pattern)
|
|
117
|
+
if content.match?(regex)
|
|
118
|
+
puts "Upserted #{pattern} in #{file} (matched)"
|
|
119
|
+
content.gsub(regex, replacement)
|
|
120
|
+
else
|
|
121
|
+
puts "Upserted #{pattern} in #{file} (appended)"
|
|
122
|
+
ensure_trailing_newline(content) + replacement + "\n"
|
|
123
|
+
end
|
|
124
|
+
elsif entry.key?("match")
|
|
125
|
+
replace(content, entry["match"], expand(entry["with"]), entry["missing"] || "halt", file)
|
|
126
|
+
else
|
|
127
|
+
pattern, replacement = entry.first
|
|
128
|
+
replace(content, pattern, expand(replacement), "halt", file)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def replace(content, pattern, replacement, missing, file)
|
|
133
|
+
regex = Regexp.new(pattern)
|
|
134
|
+
unless content.match?(regex)
|
|
135
|
+
if missing == "warn"
|
|
136
|
+
puts "#{color(:yellow)}Warning:#{reset} pattern #{pattern} not found in #{file}, skipping"
|
|
137
|
+
return content
|
|
138
|
+
else
|
|
139
|
+
abort "#{color(:red)}Error:#{reset} pattern #{pattern} not found in #{file}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
puts "Replaced #{pattern} in #{file}"
|
|
143
|
+
content.gsub(regex, replacement)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def expand(value)
|
|
147
|
+
value.to_s.gsub(/\$(\w+)/) { ENV[$1] || abort("#{color(:red)}Error:#{reset} $#{$1} not set") }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def ensure_trailing_newline(content)
|
|
151
|
+
(content.empty? || content.end_with?("\n")) ? content : content + "\n"
|
|
152
|
+
end
|
|
153
|
+
|
|
130
154
|
def run_pre_setup(commands)
|
|
131
155
|
commands.each do |cmd|
|
|
132
156
|
puts "Running: #{cmd}"
|
data/lib/bonchi/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bonchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gert Goet
|
|
@@ -41,10 +41,14 @@ files:
|
|
|
41
41
|
- lib/bonchi/port_pool.rb
|
|
42
42
|
- lib/bonchi/setup.rb
|
|
43
43
|
- lib/bonchi/version.rb
|
|
44
|
-
homepage: https://github.com/
|
|
44
|
+
homepage: https://github.com/eval/bonchi
|
|
45
45
|
licenses:
|
|
46
46
|
- MIT
|
|
47
|
-
metadata:
|
|
47
|
+
metadata:
|
|
48
|
+
homepage_uri: https://github.com/eval/bonchi
|
|
49
|
+
source_code_uri: https://github.com/eval/bonchi
|
|
50
|
+
changelog_uri: https://github.com/eval/bonchi/blob/main/CHANGELOG.md
|
|
51
|
+
rubygems_mfa_required: 'true'
|
|
48
52
|
rdoc_options: []
|
|
49
53
|
require_paths:
|
|
50
54
|
- lib
|
|
@@ -59,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
59
63
|
- !ruby/object:Gem::Version
|
|
60
64
|
version: '0'
|
|
61
65
|
requirements: []
|
|
62
|
-
rubygems_version:
|
|
66
|
+
rubygems_version: 4.0.10
|
|
63
67
|
specification_version: 4
|
|
64
68
|
summary: Git worktree manager
|
|
65
69
|
test_files: []
|