Module: Textus::MigrateKeys
- Defined in:
- lib/textus/migrate_keys.rb
Overview
Run-once helper that renames files/directories whose basenames don’t conform to the strict key grammar (§3 of plan-1.2). Only walks nested: true manifest entries — leaf entries with illegal declared keys are caught by Manifest load and must be fixed by hand.
Constant Summary collapse
- SEGMENT =
/\A[a-z0-9][a-z0-9-]*\z/
Class Method Summary collapse
-
.apply!(store, renames) ⇒ Object
—————————————————————— Apply ——————————————————————.
-
.build_plan(store) ⇒ Object
Returns { renames: […], collisions: […] } Each rename: { from:, to:, old_key:, new_key:, kind: :file|:dir } Each collision: { target:, sources: […] }.
-
.compute_new_key(rename, renames) ⇒ Object
New full key after applying all renames up through this one.
- .envelope_collision(col) ⇒ Object
-
.envelope_rename(r) ⇒ Object
—————————————————————— Envelope helpers ——————————————————————.
-
.normalize(s) ⇒ Object
Deterministic transform per plan §3.
- .path_to_key(path, base, entry, kind) ⇒ Object
-
.resolve_current_path(path, renames) ⇒ Object
If an ancestor of ‘path` was renamed earlier in this batch, rewrite the path.
-
.run(store, write: false) ⇒ Object
Returns the envelope hash described in plan-1.2 §3.
-
.walk(root, &block) ⇒ Object
Yields [absolute_path, is_dir] for every entry under root.
Class Method Details
.apply!(store, renames) ⇒ Object
Apply
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/textus/migrate_keys.rb', line 114 def apply!(store, renames) audit = AuditLog.new(store.root) renames.each do |r| # Bottom-up order means a child's ancestors haven't moved yet, so # `from`/`to` are valid as-recorded. The audit `key` reflects the # eventual full key once every rename in this batch has applied. from = r[:from] to = r[:to] File.rename(from, to) new_key = compute_new_key(r, renames) audit.append( role: "script", verb: "migrate-keys", key: new_key, etag_before: nil, etag_after: nil, extras: { "from" => from, "to" => to }, ) end end |
.build_plan(store) ⇒ Object
Returns { renames: […], collisions: […] } Each rename: { from:, to:, old_key:, new_key:, kind: :file|:dir } Each collision: { target:, sources: […] }
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/textus/migrate_keys.rb', line 36 def build_plan(store) # rubocop:disable Metrics/AbcSize renames = [] target_buckets = Hash.new { |h, k| h[k] = [] } # target_path => [source_path, ...] store.manifest.entries.each do |entry| next unless entry.nested base = File.join(store.root, "zones", entry.path) next unless File.directory?(base) # Walk depth-first. Order matters when computing the "new key" # for files inside a renamed directory: we record renames bottom-up, # so children are renamed before their parents on apply. walk(base) do |abs_path, is_dir| next if abs_path == base basename = File.basename(abs_path) stem = is_dir ? basename : basename.sub(/#{Regexp.escape(File.extname(basename))}\z/, "") next if stem.match?(SEGMENT) new_stem = normalize(stem) # Skip if normalization yields the same stem (e.g. already-legal # under a different lens). In practice match?(SEGMENT) catches that # above; this is a safety net. next if new_stem == stem new_basename = is_dir ? new_stem : new_stem + File.extname(basename) target = File.join(File.dirname(abs_path), new_basename) target_buckets[target] << abs_path renames << { from: abs_path, to: target, kind: is_dir ? :dir : :file, entry: entry, base: base, } end end collisions = target_buckets.select { |_, srcs| srcs.length > 1 } .map { |t, srcs| { target: t, sources: srcs.sort } } # Drop colliding entries from renames (we won't apply any of them) colliding_targets = collisions.to_set { |c| c[:target] } renames.reject! { |r| colliding_targets.include?(r[:to]) } # Sort renames bottom-up (deepest path first) so children move before parents. renames.sort_by! { |r| -r[:from].count("/") } { renames: renames, collisions: collisions } end |
.compute_new_key(rename, renames) ⇒ Object
New full key after applying all renames up through this one.
146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/textus/migrate_keys.rb', line 146 def compute_new_key(rename, renames) base = rename[:base] entry = rename[:entry] new_to = resolve_current_path(rename[:to], renames) rel = new_to.sub(%r{\A#{Regexp.escape(base)}/?}, "") stripped = rel.sub(/#{Regexp.escape(File.extname(rel))}\z/, "") unless rename[:kind] == :dir stripped ||= rel segs = stripped.split("/").reject(&:empty?) (entry.key.split(".") + segs).join(".") end |
.envelope_collision(col) ⇒ Object
171 172 173 |
# File 'lib/textus/migrate_keys.rb', line 171 def envelope_collision(col) { "target" => col[:target], "sources" => col[:sources] } end |
.envelope_rename(r) ⇒ Object
Envelope helpers
162 163 164 165 166 167 168 169 |
# File 'lib/textus/migrate_keys.rb', line 162 def envelope_rename(r) { "from" => r[:from], "to" => r[:to], "old_key" => path_to_key(r[:from], r[:base], r[:entry], r[:kind]), "new_key" => path_to_key(r[:to], r[:base], r[:entry], r[:kind]), } end |
.normalize(s) ⇒ Object
Deterministic transform per plan §3.
103 104 105 106 107 108 |
# File 'lib/textus/migrate_keys.rb', line 103 def normalize(s) s = s.downcase s = s.gsub(/[^a-z0-9-]/, "-") # ., _, and anything else become - s = s.gsub(/-+/, "-") s.sub(/\A-+/, "").sub(/-+\z/, "") end |
.path_to_key(path, base, entry, kind) ⇒ Object
175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/textus/migrate_keys.rb', line 175 def path_to_key(path, base, entry, kind) rel = path.sub(%r{\A#{Regexp.escape(base)}/?}, "") stripped = if kind == :dir rel else rel.sub(/#{Regexp.escape(File.extname(rel))}\z/, "") end segs = stripped.split("/").reject(&:empty?) (entry.key.split(".") + segs).join(".") end |
.resolve_current_path(path, renames) ⇒ Object
If an ancestor of ‘path` was renamed earlier in this batch, rewrite the path.
136 137 138 139 140 141 142 143 |
# File 'lib/textus/migrate_keys.rb', line 136 def resolve_current_path(path, renames) out = path renames.each do |r| prefix = r[:from] + "/" out = r[:to] + out[r[:from].length..] if out.start_with?(prefix) end out end |
.run(store, write: false) ⇒ Object
Returns the envelope hash described in plan-1.2 §3.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/textus/migrate_keys.rb', line 12 def run(store, write: false) plan = build_plan(store) collisions = plan[:collisions] renames = plan[:renames] ok = collisions.empty? apply!(store, renames) if write && ok { "protocol" => Textus::PROTOCOL, "mode" => write ? "write" : "dry-run", "renames" => renames.map { |r| envelope_rename(r) }, "collisions" => collisions.map { |c| envelope_collision(c) }, "ok" => ok, } end |
.walk(root, &block) ⇒ Object
Yields [absolute_path, is_dir] for every entry under root. Depth-first.
90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/textus/migrate_keys.rb', line 90 def walk(root, &block) Dir.each_child(root) do |name| abs = File.join(root, name) if File.directory?(abs) walk(abs, &block) yield abs, true else yield abs, false end end end |