bonchi 0.6.0.rc3 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8333ff1974d32544950e5519f6ba6cec214a2d62563f98a8969c979efc8de7e
4
- data.tar.gz: 9cc9f9147b528b6d4eb893dd8ab0787a65d3d4b64e631277513d001ef15ebcaf
3
+ metadata.gz: 2af66b530d16c1004fe1d290edf3a28ea189999df19561fbd70f0d2779f31510
4
+ data.tar.gz: 99edcbb95cbb4cdd4aa6d752b6266e3392f69833654fece86f8cef8918c5441c
5
5
  SHA512:
6
- metadata.gz: ab3822a6e85532202323eeaa0c8323917a58e7bc3f6e768b0ba5efa5f74ac1106afc405a95b68c3023a471cfd38a3f8d148464dd16cc3bec7052fddf00db48a8
7
- data.tar.gz: b22890d111d7c33fd80cbfcbcff9df8dfd2ad819a9174d362e30e5d838bf972c7205e07b8d295aae8fcd81abd3921b169224e80d88ffa3cef840f389a74f4df9
6
+ metadata.gz: e58e2d259376816627e4905885d19a5785ca525c1fda135af38c6a849726f37e088c5244b03e46789b164a088a8ece8d9c3153377d3a8476c0f851555b576ed4
7
+ data.tar.gz: e4d9300d822379d8423fc1f55b6501a6abae8c0441be618ca9651d6af2197b12556de05e4eee96b94f946d51b5f1069e1ed379c6924f69154c4d105071203d68
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)
@@ -281,17 +282,21 @@ module Bonchi
281
282
  # ports:
282
283
  # - PORT
283
284
 
284
- # Regex replacements in copied files. Env vars ($VAR) are expanded.
285
- # Short form:
286
- # replace:
285
+ # Edits applied to files. Env vars ($VAR) are expanded.
286
+ # Three actions: regex replace, append, upsert (replace-or-append).
287
+ # edit:
287
288
  # .env.local:
289
+ # # Replace — short form
288
290
  # - "^PORT=.*": "PORT=$PORT"
289
- # Full form (with optional missing: warn, default: halt):
290
- # replace:
291
- # .env.local:
292
- # - match: "^PORT=.*"
293
- # with: "PORT=$PORT"
291
+ # # Replace — full form (with optional missing: warn, default: halt)
292
+ # - match: "^DATABASE_URL=.*"
293
+ # with: "DATABASE_URL=postgres:///myapp_$WORKTREE_BRANCH_SLUG"
294
294
  # missing: warn
295
+ # # Append a line unconditionally
296
+ # - append: "FOO=bar"
297
+ # # Replace if pattern matches, otherwise append
298
+ # - upsert: "^DEBUG="
299
+ # with: "DEBUG=1"
295
300
 
296
301
  # Commands to run before the setup command (port env vars are available).
297
302
  # 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} 'replace' must be a mapping of filename to list of replacements"
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} 'replace.#{file}' must be a list of replacements"
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 replacement in 'replace.#{file}' must be a mapping"
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
@@ -66,12 +66,20 @@ module Bonchi
66
66
  end
67
67
 
68
68
  def merged?(branch, into: default_base_branch)
69
- system("git", "merge-base", "--is-ancestor", branch, into) ||
70
- system("git", "merge-base", "--is-ancestor", branch, "origin/#{into}")
69
+ locally_merged?(branch, into: into) ||
70
+ remotely_merged?(branch, into: into)
71
+ end
72
+
73
+ def locally_merged?(branch, into: default_base_branch)
74
+ system("git", "merge-base", "--is-ancestor", branch, into)
75
+ end
76
+
77
+ def remotely_merged?(branch, into: default_base_branch)
78
+ system("git", "merge-base", "--is-ancestor", branch, "origin/#{into}")
71
79
  end
72
80
 
73
81
  def delete_branch(branch, force: false)
74
- flag = force ? "-D" : "-d"
82
+ flag = (force || !locally_merged?(branch)) ? "-D" : "-d"
75
83
  system("git", "branch", flag, branch) || abort("Failed to delete branch: #{branch}")
76
84
  end
77
85
 
@@ -79,6 +87,15 @@ module Bonchi
79
87
  system("git", "fetch", "origin", "pull/#{pr_number}/head:pr-#{pr_number}")
80
88
  end
81
89
 
90
+ def set_pr_upstream(pr_number)
91
+ head_ref = `gh pr view #{pr_number} --json headRefName -q .headRefName`.strip
92
+ return if head_ref.empty?
93
+
94
+ if system("git", "branch", "--set-upstream-to", "origin/#{head_ref}", "pr-#{pr_number}")
95
+ puts "Upstream set to origin/#{head_ref}"
96
+ end
97
+ end
98
+
82
99
  def worktree_dir(branch)
83
100
  File.join(GlobalConfig.new.worktree_root, repo_name, branch)
84
101
  end
data/lib/bonchi/setup.rb CHANGED
@@ -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.from_main_worktree
25
- abort "#{color(:red)}Error:#{reset} .worktree.yml not found in main worktree" unless config
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 do |entry|
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}"
@@ -1,3 +1,3 @@
1
1
  module Bonchi
2
- VERSION = "0.6.0.rc3"
2
+ VERSION = "0.7.0"
3
3
  end
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.6.0.rc3
4
+ version: 0.7.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/gertgoet/bonchi
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