yamine 0.13.0 → 0.13.1

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: 6f7c7127a262069469056cf55f94b5785de8e1890bc0814d84453159e1a01096
4
- data.tar.gz: 0001bfbad73772c3caa7e615384a0c70dfa82732987174f55cbfbffac703dd15
3
+ metadata.gz: 43ea000326072db8f0173360b3ef58a6cce132e20f372b65f0e777bafbb97e9d
4
+ data.tar.gz: 3e878d2cae8afffd1dca255a006a5863e12dbd4471a4b6214656dda5bc97d37f
5
5
  SHA512:
6
- metadata.gz: 702d9b80482ac8f541f16ab5946b8916b0c23750774e398eabe85cd940a5d39306bdc188e010065e17ecf91081feb79b94d021a24c71186978ba8d1e4ede7dba
7
- data.tar.gz: f0f12d6cc5b32bcc46804ef3009a72ab4fac500f792b6d6ea39f05324857031b4cb0f06fe131ef291a61f2faf1d1fd87eb85520d6698b63a6a5b6dde394e637d
6
+ metadata.gz: 272dae53dfac6b0521c09c00ae096cdc10620dcabf8064a286a0fe25813a61738b3fefc4bb34cf8fdfdd8dd1467a0064ec4e8ceb6ec7b262a8ed638b66101bda
7
+ data.tar.gz: e16540d1830850c50a54451ec5f0bfb0ad87fd770b111a0b4276aad9a6916dadeeed4c106b61392e5fc5794e6276f003f718a5e53135885f36cc142944d1a12f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.1] — 2026-09-12
4
+
5
+ ### Fixed
6
+
7
+ - **`worktree clean` no longer reports "could not drop — remove manually"
8
+ for a database that never existed.** `dropdb` fails on a missing
9
+ database, and the old code collapsed that into a generic failure. Now
10
+ `Database.drop` returns an honest three-state outcome — `:dropped`,
11
+ `:missing` (nothing to drop, not an error), `:failed` (server
12
+ unreachable or drop refused) — by splitting "server reachable" from
13
+ "database present". Clean prints each outcome (`dropped N`,
14
+ `N did not exist — nothing to drop`) plus a summary line, exits 1 only
15
+ when something really failed, and **keeps the claim of a failed drop**
16
+ so the next clean can retry it (the old code deleted all claims
17
+ regardless, losing track of databases it failed to drop).
18
+ - `db drop` and `db create` resolve the DATABASE_URL template the way
19
+ boot does (ENV, then config env.clear) instead of ENV-only, so they
20
+ work from a shell without the variable exported.
21
+
3
22
  ## [0.13.0] — 2026-09-11
4
23
 
5
24
  ### Changed
@@ -546,13 +546,7 @@ module Yamine
546
546
  end
547
547
 
548
548
  def database_template_from_config(resolved)
549
- top = resolved.env.is_a?(Hash) ? resolved.env["clear"] : nil
550
- from_top = top.is_a?(Hash) ? top["DATABASE_URL"] : nil
551
- return from_top if from_top && !from_top.to_s.strip.empty?
552
-
553
- env = resolved.processes.values.map { |e| e["env"] || {} }
554
- clear = env.map { |e| e["clear"] || {} }.reduce({}, :merge)
555
- clear["DATABASE_URL"]
549
+ Database.template_for(resolved)
556
550
  end
557
551
 
558
552
  # Schema-load on first boot uses the app's own command when declared
@@ -893,9 +893,9 @@ module Yamine
893
893
  resolved = Resolver.resolve(Dir.pwd)
894
894
  name = Database.name_for(Dir.pwd, env: boot_env_name,
895
895
  state_dir: ctx.store.dir)
896
- template = ENV["DATABASE_URL"]
896
+ template = drop_template
897
897
  unless template && !template.strip.empty?
898
- $stderr.puts "Error: set DATABASE_URL (template with host/user; database segment is replaced)."
898
+ $stderr.puts "Error: no DATABASE_URL template found (ENV or env.clear in config/local.yml)."
899
899
  exit 1
900
900
  end
901
901
  if Database.ensure_exists(name, template)
@@ -917,38 +917,41 @@ module Yamine
917
917
  $stderr.puts "Error: worktree #{info["dir"]} still exists. Use --force to drop #{name} anyway."
918
918
  exit 1
919
919
  end
920
- if drop_database(ctx, name)
920
+ template = drop_template
921
+ unless template && !template.strip.empty?
922
+ $stderr.puts "Error: no DATABASE_URL template found (ENV or env.clear in config/local.yml) — cannot reach the server to drop #{name}."
923
+ exit 1
924
+ end
925
+ case Database.drop(name, template)
926
+ when :dropped
921
927
  map.delete(name)
922
928
  Database.save_map(ctx.store.dir, map)
923
929
  puts "Dropped #{name}."
924
- else
925
- $stderr.puts "Error: could not drop #{name}."
930
+ when :missing
931
+ map.delete(name)
932
+ Database.save_map(ctx.store.dir, map)
933
+ puts "#{name} did not exist — nothing to drop (claim removed)."
934
+ when :failed
935
+ $stderr.puts "Error: could not drop #{name} — is the database server running? The claim is kept."
926
936
  exit 1
927
937
  end
928
938
  end
929
939
 
930
- def drop_database(_ctx, name)
931
- template = ENV["DATABASE_URL"]
932
- return false unless template && !template.strip.empty?
940
+ # Template for reaching the database server outside of boot:
941
+ # ENV first, then the app's config (env.clear). Runs from the
942
+ # main checkout, so the config's own template is the usual source.
943
+ def drop_template
944
+ env = ENV["DATABASE_URL"]
945
+ return env if env && !env.strip.empty?
933
946
 
934
- case Database.adapter_for(template)
935
- when :postgres
936
- uri = URI.parse(template)
937
- _out, status = Open3.capture2("dropdb", name, env: Database.pg_env(uri))
938
- status.success?
939
- when :mysql
940
- uri = URI.parse(template)
941
- args = ["-h", uri.host || "127.0.0.1", "-u",
942
- URI.decode_www_form_component(uri.user || "root")]
943
- _out, status = Open3.capture2("mysqladmin", *args, "drop", name, "-f")
944
- status.success?
945
- else
946
- false
947
- end
948
- rescue SystemCallError
949
- false
947
+ config = Config.load(Dir.pwd)
948
+ return nil unless config
949
+
950
+ Database.template_for(TemplateSource.new(config.env_config, config.processes))
950
951
  end
951
952
 
953
+ TemplateSource = Struct.new(:env, :processes)
954
+
952
955
  def boot_env_name
953
956
  ENV.fetch("RAILS_ENV", "development")
954
957
  end
@@ -974,16 +977,31 @@ module Yamine
974
977
  puts "No orphaned worktree databases."
975
978
  return
976
979
  end
980
+ template = drop_template
981
+ unless template && !template.strip.empty?
982
+ $stderr.puts "Error: no DATABASE_URL template found (ENV or env.clear in config/local.yml) — cannot drop the orphaned databases."
983
+ exit 1
984
+ end
985
+ dropped, missing, failed = [], [], []
977
986
  orphaned.each do |name, _info|
978
- if drop_database(ctx, name)
979
- puts "Dropped orphaned #{name}."
980
- else
981
- $stderr.puts "Could not drop #{name} — remove manually."
987
+ case Database.drop(name, template)
988
+ when :dropped then dropped << name
989
+ when :missing then missing << name
990
+ when :failed then failed << name
982
991
  end
983
992
  end
993
+ dropped.each { |n| puts " dropped #{n}" }
994
+ missing.each { |n| puts " #{n} did not exist — nothing to drop" }
995
+ failed.each { |n| warn " could not drop #{n} — is the server running? Claim kept for a retry." }
996
+ # Forget only the claims we actually handled; a failed drop
997
+ # stays recorded so the next clean can retry it.
984
998
  map = Database.load_map(ctx.store.dir)
985
- orphaned.each_key { |name| map.delete(name) }
999
+ (dropped + missing).each { |n| map.delete(n) }
986
1000
  Database.save_map(ctx.store.dir, map)
1001
+ summary = "Cleaned #{orphaned.length} orphaned claim(s): #{dropped.length} dropped, #{missing.length} already gone"
1002
+ summary += ", #{failed.length} failed" if failed.any?
1003
+ puts "#{summary}."
1004
+ exit 1 if failed.any?
987
1005
  else
988
1006
  raise Error, "Usage: yamine worktree [list|clean]"
989
1007
  end
@@ -100,6 +100,28 @@ module Yamine
100
100
  nil
101
101
  end
102
102
 
103
+ # The DATABASE_URL template per-worktree names derive from: ENV
104
+ # first, then the config's top-level env.clear, then per-process
105
+ # env.clear (configs written before top-level env existed). Shared
106
+ # by boot, `db create/drop`, and `worktree clean` so they all agree.
107
+ # Takes anything responding to .env (resolver Result / Config) —
108
+ # pass env_config for a bare Config.
109
+ def template_for(resolved)
110
+ source = if resolved.respond_to?(:env)
111
+ resolved.env
112
+ else
113
+ resolved.env_config
114
+ end
115
+ top = source.is_a?(Hash) ? source["clear"] : nil
116
+ from_top = top.is_a?(Hash) ? top["DATABASE_URL"] : nil
117
+ return from_top if from_top && !from_top.to_s.strip.empty?
118
+
119
+ processes = resolved.respond_to?(:processes) ? resolved.processes : {}
120
+ env = processes.values.map { |e| e["env"] || {} }
121
+ clear = env.map { |e| e["clear"] || {} }.reduce({}, :merge)
122
+ clear["DATABASE_URL"]
123
+ end
124
+
103
125
  # Databases whose worktree dirs no longer exist (for `worktree clean`).
104
126
  def orphaned(state_dir)
105
127
  load_map(state_dir).select { |_, v| !File.directory?(v["dir"]) }
@@ -146,6 +168,73 @@ module Yamine
146
168
  end
147
169
  end
148
170
 
171
+ # Drop a database with an honest outcome. Returns :dropped,
172
+ # :missing (the database did not exist — nothing to do, NOT an
173
+ # error), or :failed (server unreachable, drop refused). The
174
+ # distinction matters: `worktree clean` used to print "could not
175
+ # drop — remove manually" for a database that never existed,
176
+ # sending the user hunting for locks and permissions that were
177
+ # never the problem.
178
+ def drop(name, database_url)
179
+ case adapter_for(database_url)
180
+ when :postgres then pg_drop(name, database_url)
181
+ when :mysql then mysql_drop(name, database_url)
182
+ else :failed
183
+ end
184
+ end
185
+
186
+ def pg_drop(name, database_url)
187
+ reachable, exists = pg_state(name, database_url)
188
+ return :missing if reachable && !exists
189
+ return :failed unless reachable
190
+
191
+ uri = URI.parse(database_url)
192
+ _out, status = Open3.capture2("dropdb", name, env: pg_env(uri))
193
+ return :dropped if status.success?
194
+
195
+ reachable, exists = pg_state(name, database_url)
196
+ reachable && exists ? :failed : :dropped
197
+ rescue SystemCallError
198
+ :failed
199
+ end
200
+
201
+ def mysql_drop(name, database_url)
202
+ reachable, exists = mysql_state(name, database_url)
203
+ return :missing if reachable && !exists
204
+ return :failed unless reachable
205
+
206
+ uri = URI.parse(database_url)
207
+ _out, status = Open3.capture2("mysqladmin", *mysql_args(uri), "drop", name, "-f")
208
+ return :dropped if status.success?
209
+
210
+ reachable, exists = mysql_state(name, database_url)
211
+ reachable && exists ? :failed : :dropped
212
+ rescue SystemCallError
213
+ :failed
214
+ end
215
+
216
+ # [server_reachable, database_exists] — the split that lets drop
217
+ # tell "nothing to drop" apart from "could not reach the server".
218
+ def pg_state(name, database_url)
219
+ uri = URI.parse(database_url)
220
+ out, status = Open3.capture2(pg_env(uri), "psql", "-lqt")
221
+ return [false, false] unless status.success?
222
+
223
+ [true, out.split("\n").any? { |l| l.split("|").first.to_s.strip == name }]
224
+ rescue SystemCallError
225
+ [false, false]
226
+ end
227
+
228
+ def mysql_state(name, database_url)
229
+ uri = URI.parse(database_url)
230
+ out, status = Open3.capture2("mysql", *mysql_args(uri), "-e", "SHOW DATABASES;")
231
+ return [false, false] unless status.success?
232
+
233
+ [true, out.split("\n").include?(name)]
234
+ rescue SystemCallError
235
+ [false, false]
236
+ end
237
+
149
238
  # Create the database if missing. Uses createdb/mysqladmin when
150
239
  # available; returns true/false, never raises (boot reports, not dies).
151
240
  def ensure_exists(name, database_url)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yamine
4
- VERSION = "0.13.0"
4
+ VERSION = "0.13.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yamine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto