bonchi 0.2.0.rc1 → 0.3.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 +82 -0
- data/lib/bonchi/colors.rb +20 -0
- data/lib/bonchi/config.rb +36 -1
- data/lib/bonchi/git.rb +5 -0
- data/lib/bonchi/setup.rb +48 -5
- data/lib/bonchi/version.rb +1 -1
- data/lib/bonchi.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: 137e1eebb563dfaf05a9e6928abeb3832ff8e6231f7e1b4ea176037b605b2489
|
|
4
|
+
data.tar.gz: 8f22938f47fbc5068cb0a40a02fd31c875b5cdc5b7e699b6049752060ec3accb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47d60879e7c32a8e518c3719d78c6c952dcc850c9897f49e4da0b4370d3a3e07e2d1233cf4b15a8eaf257671eae02aad4fd7d89d40d5a86525b8bf6a3ab6157b
|
|
7
|
+
data.tar.gz: f6a8c72b294fd4c6e29dff35d20c085463e0411bab413971c934ca43b80b331caa095b726d1cace6305a9db20865a6ec3877de1cdaf89d9d98f3aa9bbef2b284
|
data/lib/bonchi/cli.rb
CHANGED
|
@@ -14,6 +14,13 @@ module Bonchi
|
|
|
14
14
|
map "-v" => :version
|
|
15
15
|
|
|
16
16
|
desc "create BRANCH [BASE]", "Create new branch + worktree"
|
|
17
|
+
long_desc <<~DESC
|
|
18
|
+
Create a new branch and worktree. BASE defaults to the repository's default branch
|
|
19
|
+
(e.g. main). If a worktree for BRANCH already exists, switches to it instead.
|
|
20
|
+
|
|
21
|
+
When a .worktree.yml exists in the main worktree, setup runs automatically
|
|
22
|
+
(copy files, allocate ports, run pre_setup and setup commands). Skip with --no-setup.
|
|
23
|
+
DESC
|
|
17
24
|
option :setup, type: :boolean, default: true, desc: "Run setup after creating worktree"
|
|
18
25
|
def create(branch, base = nil)
|
|
19
26
|
base ||= Git.default_base_branch
|
|
@@ -38,6 +45,13 @@ module Bonchi
|
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
desc "switch BRANCH", "Switch to existing branch in worktree"
|
|
48
|
+
long_desc <<~DESC
|
|
49
|
+
Create a worktree for an existing branch and cd into it.
|
|
50
|
+
If a worktree for BRANCH already exists, switches to it instead.
|
|
51
|
+
|
|
52
|
+
The branch must already exist locally or on the remote.
|
|
53
|
+
To create a new branch, use `bonchi create` instead.
|
|
54
|
+
DESC
|
|
41
55
|
def switch(branch)
|
|
42
56
|
existing = Git.worktree_path_for(branch)
|
|
43
57
|
if existing
|
|
@@ -58,6 +72,13 @@ module Bonchi
|
|
|
58
72
|
end
|
|
59
73
|
|
|
60
74
|
desc "pr NUMBER_OR_URL", "Checkout GitHub PR in worktree"
|
|
75
|
+
long_desc <<~DESC
|
|
76
|
+
Fetch a GitHub pull request and check it out in a new worktree.
|
|
77
|
+
Accepts a PR number (e.g. 123) or a full GitHub PR URL.
|
|
78
|
+
|
|
79
|
+
The worktree branch will be named pr-<number>.
|
|
80
|
+
If the worktree already exists, switches to it instead.
|
|
81
|
+
DESC
|
|
61
82
|
def pr(input)
|
|
62
83
|
pr_number = extract_pr_number(input)
|
|
63
84
|
branch = "pr-#{pr_number}"
|
|
@@ -77,6 +98,22 @@ module Bonchi
|
|
|
77
98
|
signal_cd(path)
|
|
78
99
|
end
|
|
79
100
|
|
|
101
|
+
desc "init", "Generate a .worktree.yml in the current project"
|
|
102
|
+
long_desc <<~DESC
|
|
103
|
+
Generate a .worktree.yml config file in the current directory with
|
|
104
|
+
sensible defaults. Edit the file to customize which files to copy,
|
|
105
|
+
which ports to allocate, and what setup command to run.
|
|
106
|
+
DESC
|
|
107
|
+
def init
|
|
108
|
+
path = File.join(Dir.pwd, ".worktree.yml")
|
|
109
|
+
if File.exist?(path)
|
|
110
|
+
abort "Error: .worktree.yml already exists"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
File.write(path, WORKTREE_YML_TEMPLATE)
|
|
114
|
+
puts "Created #{path}"
|
|
115
|
+
end
|
|
116
|
+
|
|
80
117
|
desc "setup [-- ARGS...]", "Run setup in current worktree (ports, copy, pre_setup, setup cmd)"
|
|
81
118
|
def setup(*args)
|
|
82
119
|
Setup.new.run(args)
|
|
@@ -88,6 +125,12 @@ module Bonchi
|
|
|
88
125
|
end
|
|
89
126
|
|
|
90
127
|
desc "remove BRANCH", "Remove a worktree"
|
|
128
|
+
long_desc <<~DESC
|
|
129
|
+
Remove a worktree and its directory. Refuses to remove worktrees
|
|
130
|
+
with uncommitted changes or untracked files (use `git worktree remove --force` to override).
|
|
131
|
+
|
|
132
|
+
Aliases: rm
|
|
133
|
+
DESC
|
|
91
134
|
def remove(branch)
|
|
92
135
|
path = Git.worktree_path_for(branch)
|
|
93
136
|
abort "Error: No worktree found for branch: #{branch}" unless path
|
|
@@ -97,6 +140,13 @@ module Bonchi
|
|
|
97
140
|
end
|
|
98
141
|
|
|
99
142
|
desc "prune", "Prune stale worktree admin files"
|
|
143
|
+
long_desc <<~DESC
|
|
144
|
+
Clean up stale worktree tracking data. Git internally tracks worktrees in
|
|
145
|
+
.git/worktrees/. When a worktree directory is deleted manually (e.g. rm -rf)
|
|
146
|
+
instead of via `bonchi remove`, the tracking data becomes stale.
|
|
147
|
+
|
|
148
|
+
This runs `git worktree prune` to remove those orphaned entries.
|
|
149
|
+
DESC
|
|
100
150
|
def prune
|
|
101
151
|
Git.worktree_prune
|
|
102
152
|
puts "Pruned stale worktree administrative files"
|
|
@@ -133,6 +183,38 @@ module Bonchi
|
|
|
133
183
|
end
|
|
134
184
|
end
|
|
135
185
|
|
|
186
|
+
WORKTREE_YML_TEMPLATE = <<~YAML
|
|
187
|
+
# Worktree configuration for bonchi.
|
|
188
|
+
# See https://github.com/eval/bonchi
|
|
189
|
+
|
|
190
|
+
# Files to copy from the main worktree before setup.
|
|
191
|
+
# copy:
|
|
192
|
+
# - .env.local
|
|
193
|
+
|
|
194
|
+
# Env var names to allocate unique ports for (from global pool).
|
|
195
|
+
# ports:
|
|
196
|
+
# - PORT
|
|
197
|
+
|
|
198
|
+
# Regex replacements in copied files. Env vars ($VAR) are expanded.
|
|
199
|
+
# Short form:
|
|
200
|
+
# replace:
|
|
201
|
+
# .env.local:
|
|
202
|
+
# - "^PORT=.*": "PORT=$PORT"
|
|
203
|
+
# Full form (with optional missing: warn, default: halt):
|
|
204
|
+
# replace:
|
|
205
|
+
# .env.local:
|
|
206
|
+
# - match: "^PORT=.*"
|
|
207
|
+
# with: "PORT=$PORT"
|
|
208
|
+
# missing: warn
|
|
209
|
+
|
|
210
|
+
# Commands to run before the setup command (port env vars are available).
|
|
211
|
+
# pre_setup:
|
|
212
|
+
# - echo "preparing..."
|
|
213
|
+
|
|
214
|
+
# The setup command to run (default: bin/setup).
|
|
215
|
+
setup: bin/setup
|
|
216
|
+
YAML
|
|
217
|
+
|
|
136
218
|
SHELL_ENV = <<~'SHELL'
|
|
137
219
|
bonchi() {
|
|
138
220
|
local bonchi_cd_file="${TMPDIR:-/tmp}/bonchi_cd.$$"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Bonchi
|
|
2
|
+
module Colors
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def color(name)
|
|
6
|
+
return "" if ENV.key?("NO_COLOR")
|
|
7
|
+
|
|
8
|
+
case name
|
|
9
|
+
when :red then "\e[31m"
|
|
10
|
+
when :yellow then "\e[33m"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reset
|
|
15
|
+
return "" if ENV.key?("NO_COLOR")
|
|
16
|
+
|
|
17
|
+
"\e[0m"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/bonchi/config.rb
CHANGED
|
@@ -2,14 +2,25 @@ require "yaml"
|
|
|
2
2
|
|
|
3
3
|
module Bonchi
|
|
4
4
|
class Config
|
|
5
|
-
|
|
5
|
+
include Colors
|
|
6
|
+
|
|
7
|
+
KNOWN_KEYS = %w[copy ports replace pre_setup setup].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :copy, :ports, :replace, :pre_setup, :setup
|
|
6
10
|
|
|
7
11
|
def initialize(path)
|
|
8
12
|
data = YAML.load_file(path)
|
|
13
|
+
|
|
14
|
+
unknown = data.keys - KNOWN_KEYS
|
|
15
|
+
unknown.each { |k| warn "#{color(:yellow)}Warning:#{reset} unknown key '#{k}' in .worktree.yml, ignoring" }
|
|
16
|
+
|
|
9
17
|
@copy = Array(data["copy"])
|
|
10
18
|
@ports = Array(data["ports"])
|
|
19
|
+
@replace = data["replace"] || {}
|
|
11
20
|
@pre_setup = Array(data["pre_setup"])
|
|
12
21
|
@setup = data["setup"] || "bin/setup"
|
|
22
|
+
|
|
23
|
+
validate!
|
|
13
24
|
end
|
|
14
25
|
|
|
15
26
|
def self.from_main_worktree
|
|
@@ -19,5 +30,29 @@ module Bonchi
|
|
|
19
30
|
|
|
20
31
|
new(path)
|
|
21
32
|
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validate!
|
|
37
|
+
unless @replace.is_a?(Hash)
|
|
38
|
+
abort "#{color(:red)}Error:#{reset} 'replace' must be a mapping of filename to list of replacements"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@replace.each do |file, entries|
|
|
42
|
+
unless entries.is_a?(Array)
|
|
43
|
+
abort "#{color(:red)}Error:#{reset} 'replace.#{file}' must be a list of replacements"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
entries.each do |entry|
|
|
47
|
+
unless entry.is_a?(Hash)
|
|
48
|
+
abort "#{color(:red)}Error:#{reset} each replacement in 'replace.#{file}' must be a mapping"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless @setup.is_a?(String)
|
|
54
|
+
abort "#{color(:red)}Error:#{reset} 'setup' must be a string"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
22
57
|
end
|
|
23
58
|
end
|
data/lib/bonchi/git.rb
CHANGED
|
@@ -4,6 +4,11 @@ module Bonchi
|
|
|
4
4
|
module Git
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
+
def current_branch(worktree = nil)
|
|
8
|
+
dir = worktree || Dir.pwd
|
|
9
|
+
`git -C #{dir.shellescape} rev-parse --abbrev-ref HEAD`.strip
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
def repo_name
|
|
8
13
|
url = `git remote get-url origin 2>/dev/null`.strip
|
|
9
14
|
base = url.empty? ? `git rev-parse --show-toplevel`.strip : url
|
data/lib/bonchi/setup.rb
CHANGED
|
@@ -3,6 +3,8 @@ require "shellwords"
|
|
|
3
3
|
|
|
4
4
|
module Bonchi
|
|
5
5
|
class Setup
|
|
6
|
+
include Colors
|
|
7
|
+
|
|
6
8
|
def initialize(worktree: nil)
|
|
7
9
|
@worktree = worktree || Dir.pwd
|
|
8
10
|
@main_worktree = Git.main_worktree
|
|
@@ -10,19 +12,23 @@ module Bonchi
|
|
|
10
12
|
|
|
11
13
|
def run(args = [])
|
|
12
14
|
if @worktree == @main_worktree
|
|
13
|
-
abort "Error
|
|
15
|
+
abort "#{color(:red)}Error:#{reset} already in the main worktree"
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
config = Config.from_main_worktree
|
|
17
|
-
abort "Error
|
|
19
|
+
abort "#{color(:red)}Error:#{reset} .worktree.yml not found in main worktree" unless config
|
|
18
20
|
|
|
19
|
-
ENV["
|
|
20
|
-
ENV["
|
|
21
|
+
ENV["WORKTREE_MAIN"] = @main_worktree
|
|
22
|
+
ENV["WORKTREE_LINKED"] = @worktree
|
|
23
|
+
ENV["WORKTREE_BRANCH"] = Git.current_branch(@worktree)
|
|
24
|
+
ENV["WORKTREE_BRANCH_SLUG"] = ENV["WORKTREE_BRANCH"].gsub(/[^a-zA-Z0-9_]/, "_")
|
|
25
|
+
ENV["WORKTREE_ROOT"] ||= GlobalConfig.new.worktree_root
|
|
21
26
|
|
|
22
27
|
puts "Setting up worktree from: #{@main_worktree}"
|
|
23
28
|
|
|
24
29
|
allocate_ports(config.ports) if config.ports.any?
|
|
25
30
|
copy_files(config.copy)
|
|
31
|
+
replace_in_files(config.replace) if config.replace.any?
|
|
26
32
|
run_pre_setup(config.pre_setup)
|
|
27
33
|
exec_setup(config.setup, args)
|
|
28
34
|
end
|
|
@@ -42,8 +48,45 @@ module Bonchi
|
|
|
42
48
|
FileUtils.cp(src, File.join(@worktree, file))
|
|
43
49
|
puts "Copied #{file}"
|
|
44
50
|
else
|
|
45
|
-
puts "Warning
|
|
51
|
+
puts "#{color(:yellow)}Warning:#{reset} #{file} not found in main worktree, skipping"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def replace_in_files(replacements)
|
|
57
|
+
replacements.each do |file, entries|
|
|
58
|
+
path = File.join(@worktree, file)
|
|
59
|
+
abort "#{color(:red)}Error:#{reset} #{file} not found" unless File.exist?(path)
|
|
60
|
+
|
|
61
|
+
content = File.read(path)
|
|
62
|
+
entries.each do |entry|
|
|
63
|
+
if entry.is_a?(Hash) && entry.key?("match")
|
|
64
|
+
pattern = entry["match"]
|
|
65
|
+
replacement = entry["with"]
|
|
66
|
+
missing = entry["missing"] || "halt"
|
|
67
|
+
elsif entry.is_a?(Hash)
|
|
68
|
+
pattern, replacement = entry.first
|
|
69
|
+
missing = "halt"
|
|
70
|
+
else
|
|
71
|
+
abort "#{color(:red)}Error:#{reset} invalid replace entry in #{file}: #{entry.inspect}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
expanded = replacement.gsub(/\$(\w+)/) { ENV[$1] || abort("#{color(:red)}Error:#{reset} $#{$1} not set") }
|
|
75
|
+
regex = Regexp.new(pattern)
|
|
76
|
+
|
|
77
|
+
unless content.match?(regex)
|
|
78
|
+
if missing == "warn"
|
|
79
|
+
puts "#{color(:yellow)}Warning:#{reset} pattern #{pattern} not found in #{file}, skipping"
|
|
80
|
+
next
|
|
81
|
+
else
|
|
82
|
+
abort "#{color(:red)}Error:#{reset} pattern #{pattern} not found in #{file}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
content = content.gsub(regex, expanded)
|
|
87
|
+
puts "Replaced #{pattern} in #{file}"
|
|
46
88
|
end
|
|
89
|
+
File.write(path, content)
|
|
47
90
|
end
|
|
48
91
|
end
|
|
49
92
|
|
data/lib/bonchi/version.rb
CHANGED
data/lib/bonchi.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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gert Goet
|
|
@@ -34,6 +34,7 @@ files:
|
|
|
34
34
|
- exe/bonchi
|
|
35
35
|
- lib/bonchi.rb
|
|
36
36
|
- lib/bonchi/cli.rb
|
|
37
|
+
- lib/bonchi/colors.rb
|
|
37
38
|
- lib/bonchi/config.rb
|
|
38
39
|
- lib/bonchi/git.rb
|
|
39
40
|
- lib/bonchi/global_config.rb
|