railbow 0.4.0 → 0.5.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/CHANGELOG.md +54 -0
- data/README.md +79 -1
- data/lib/railbow/config.rb +3 -1
- data/lib/railbow/init.rb +16 -0
- data/lib/railbow/multi_db.rb +87 -0
- data/lib/railbow/params.rb +54 -0
- data/lib/railbow/status/ghosts.rb +144 -0
- data/lib/railbow/status/git_data.rb +327 -0
- data/lib/railbow/status/help.rb +121 -0
- data/lib/railbow/status/printer.rb +324 -0
- data/lib/railbow/status/section.rb +551 -0
- data/lib/railbow/table/column.rb +7 -2
- data/lib/railbow/table/renderer.rb +47 -18
- data/lib/railbow/tasks/migrate_status.rake +30 -756
- data/lib/railbow/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d599ddd7c1bd3a12893312ab22f40ff75def87f5d85d42dd2ee5a11da7183225
|
|
4
|
+
data.tar.gz: 4413568aafb22c8cf2b1025bbd6f90696f4da6f3bcead307c48ffeac3e96f04c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a02d2dda50ba6fa9ae4b04d166ebc51148532dcfccd5ab1a7777179d14767d2bb87b2fc4a9a5d3aadbb0eeae114a68f5365cd3bfd0d27286958b192f5fa43967
|
|
7
|
+
data.tar.gz: 5ad80b381822e25a9078093f82c8b52deccae5940334ce45645883e2d4c06a81070e2710876f58fc8af949cc3b96009369e141eb9945c3a1941233561c31f486
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,60 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.0] - 2026-07-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Multi-database support for `db:migrate:status`. Rails runs the task once per
|
|
13
|
+
database; railbow now wraps that loop and renders the databases together, as
|
|
14
|
+
one section each in `database.yml` order with column widths shared so the
|
|
15
|
+
tables line up. The section header names the database by its `database.yml`
|
|
16
|
+
name, which is what you configure and filter by, rather than the database
|
|
17
|
+
name Rails reports.
|
|
18
|
+
- A database with nothing in the time window and nothing pending collapses to a
|
|
19
|
+
one-line summary instead of an empty table, so the `cache`, `queue` and
|
|
20
|
+
`cable` databases of a stock Rails 8 app stop drowning out your own. Anything
|
|
21
|
+
it hides (pending migrations, ghosts) is counted in the line, along with the
|
|
22
|
+
flag that reveals them.
|
|
23
|
+
- Databases sharing a `migrations_paths` (horizontal sharding) merge into a
|
|
24
|
+
single table carrying a status glyph per database, making drift between
|
|
25
|
+
shards visible at a glance. A dim `·` marks a version one shard has never
|
|
26
|
+
seen.
|
|
27
|
+
- `RBW_DB` (config key `db`) selects and shapes a multi-database run:
|
|
28
|
+
`only:<name>` and `skip:<name>` (both repeatable) pick databases, `full`
|
|
29
|
+
draws the quiet ones in full, and `inline` merges everything into one table
|
|
30
|
+
ordered by version with a `Db` column, so the calendar spans the whole
|
|
31
|
+
application instead of restarting per database. Filtered-out databases are
|
|
32
|
+
named in a footer rather than silently dropped.
|
|
33
|
+
|
|
34
|
+
- `RBW_SINCE_MIN` (config key `since_min`, default 10) makes the time window a
|
|
35
|
+
soft limit: whatever `since` leaves, the floor tops it back up to this many
|
|
36
|
+
migrations. A database with five migrations now shows all five rather than
|
|
37
|
+
hiding the two that happen to be old and reporting an empty period. The
|
|
38
|
+
hidden-count line says `showing the last 10` when the floor overrode the
|
|
39
|
+
window, and a multi-database section header reads `last 10 of 50` rather than
|
|
40
|
+
`10 of 50`, so a table holding rows older than the window it names still
|
|
41
|
+
reads honestly in both views. `RBW_SINCE_MIN=0` restores the old hard cutoff.
|
|
42
|
+
- `RBW_DB=focus`, **the default**, expands only the first database and
|
|
43
|
+
summarizes the rest in a line each, so a multi-database app is one table to
|
|
44
|
+
read rather than several. A database holding pending migrations is expanded
|
|
45
|
+
anyway: the summary exists to hide what needs no action, and pending work is
|
|
46
|
+
the opposite of that. An empty `RBW_DB` (or `db: ""`) expands every database
|
|
47
|
+
that has recent activity, `full` expands everything including quiet ones, and
|
|
48
|
+
`focus` never applies to an `inline` run, which exists to hold every database
|
|
49
|
+
at once.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- Git lookups are now shared across a run: authors, landed dates and branch
|
|
54
|
+
origins are resolved once per migrations directory instead of once per
|
|
55
|
+
database. `RBW_HELP=1` prints the help once for the whole run rather than
|
|
56
|
+
once per database.
|
|
57
|
+
- `db:migrate:status` internals moved out of the rake file into
|
|
58
|
+
`Railbow::Status` (`Section`, `Printer`, `GitData`, `Ghosts`, `Help`). No
|
|
59
|
+
output changed: a single-database app renders byte for byte what it did
|
|
60
|
+
before, which a golden fixture now enforces.
|
|
61
|
+
|
|
8
62
|
## [0.4.0] - 2026-07-28
|
|
9
63
|
|
|
10
64
|
### Added
|
data/README.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- **About** - polished `rails about` output
|
|
23
23
|
- **Git Integration** - authors, diffs, branch origin, landing dates, uncommitted file indicators
|
|
24
24
|
- **Calendar View** - month separators plus week ticks or full week rows, with optional per-section counts
|
|
25
|
+
- **Multiple Databases** - read one table by default, the rest summarized in a line each (and expanded when they have migrations pending); sharded databases merged with a status per shard
|
|
25
26
|
- **Ghost Recovery** - uses the [mighost](https://github.com/amberpixels/mighost) gem (optional) to recover names, authors, and branch badges for `NO FILE` migrations
|
|
26
27
|
- **Smart Defaults** - auto-disables in CI, piped output, `NO_COLOR`, and LLM agents
|
|
27
28
|
|
|
@@ -103,7 +104,7 @@ Out of the box you get:
|
|
|
103
104
|
- **Landing dates** - `⤻ Mar 04` badge when a migration was merged to main after its creation
|
|
104
105
|
- **Branch badges** - `⎇ PS-142` showing the source branch/ticket
|
|
105
106
|
- **Affected tables** - color-coded table names extracted from migration files
|
|
106
|
-
- **Time filtering** - only the last 70 days shown by default (`since: 70d`)
|
|
107
|
+
- **Time filtering** - only the last 70 days shown by default (`since: 70d`), as a soft limit: never fewer than 10 migrations, however old (`since_min: 10`)
|
|
107
108
|
- **Your migrations highlighted** - rows authored by you are visually distinct
|
|
108
109
|
- **Pending migrations greyed out** - a `down` row keeps its status glyph but loses its colors
|
|
109
110
|
|
|
@@ -146,6 +147,79 @@ calendar: "" # month separators only
|
|
|
146
147
|
|
|
147
148
|
Dropping `calendar` from `view` instead removes the month separators too.
|
|
148
149
|
|
|
150
|
+
#### Multiple databases
|
|
151
|
+
|
|
152
|
+
Rails runs `db:migrate:status` once per database, so a multi-database app used
|
|
153
|
+
to get one blind table per database: widths that did not line up, the help
|
|
154
|
+
printed once per database, and every git lookup repeated. Railbow now renders
|
|
155
|
+
the whole run together.
|
|
156
|
+
|
|
157
|
+
By default you read one table: the first database in `database.yml` is rendered
|
|
158
|
+
in full and every other one is summarized in a line each (`db: focus`).
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
📊 2 databases · primary, raw
|
|
162
|
+
|
|
163
|
+
──── primary · myapp_development ──── 122 of 1500 ──────────────────────
|
|
164
|
+
↑↑ │ 20260721130000 │ 2026-07-21 13:00:00 │ Partition sms notifications ● sms_notifications
|
|
165
|
+
↓↓ │ 20260727170000 │ 2026-07-27 17:00:00 │ Add cr push columns to lines ● lines
|
|
166
|
+
|
|
167
|
+
⋯ raw · 5 migrations, all applied · latest Jul 22 2026
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**A database with migrations pending is always expanded**, however focused the
|
|
171
|
+
run is. The summary exists to hide what needs no action, and pending work is
|
|
172
|
+
the opposite of that.
|
|
173
|
+
|
|
174
|
+
Setting `db` to an empty value expands every database that has recent activity,
|
|
175
|
+
each as its own section with column widths shared so the tables line up:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
db: "" # or RBW_DB= on the command line
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
──── primary · myapp_development ──── 122 of 1500 ──────────────────────
|
|
183
|
+
↑↑ │ 20260721130000 │ 2026-07-21 13:00:00 │ Partition sms notifications ● sms_notifications
|
|
184
|
+
|
|
185
|
+
──── raw · raw_development ─────────────────────────────────────────────
|
|
186
|
+
↑↑ │ 20260722120000 │ 2026-07-22 12:00:00 │ Drop archived activity logs ● archived_activity_logs
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
A section header counts what it is showing: `122 of 1500` means 122 fell inside
|
|
190
|
+
the time window. When the window found too few and the `since_min` floor topped
|
|
191
|
+
the section back up, it reads `last 10 of 50` instead, because the rows are
|
|
192
|
+
then the newest ten rather than a window's worth.
|
|
193
|
+
|
|
194
|
+
Even then, a database with nothing in the time window collapses to one line
|
|
195
|
+
rather than an empty table, which is what keeps the `cache`, `queue` and
|
|
196
|
+
`cable` databases of a stock Rails 8 app from drowning out your own:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
⋯ cache · 5 migrations, all applied · latest Feb 12 2024
|
|
200
|
+
⋯ queue · 12 migrations, 3 pending outside the 70d window - RBW_SINCE=all
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Databases that share a `migrations_paths` (horizontal sharding) run the same
|
|
204
|
+
files, so they merge into one table carrying a status per database. Drift
|
|
205
|
+
between shards is then visible at a glance, and `·` marks a version a shard has
|
|
206
|
+
never seen:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
──── primary + primary_shard_one · db/migrate ──────────────────────────
|
|
210
|
+
↑↑ ↑↑ │ 20260601151200 │ 2026-06-01 15:12:00 │ Add owner id to pets
|
|
211
|
+
↑↑ ↓↓ │ 20260726143000 │ 2026-07-26 14:30:00 │ Add policy number to bills
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`RBW_DB` controls the rest: `only:<name>` and `skip:<name>` (both repeatable)
|
|
215
|
+
pick databases, `full` draws every section in full and overrides `focus`, and
|
|
216
|
+
`inline` merges everything into a single table ordered by version with a `Db`
|
|
217
|
+
column, so the calendar spans the whole application rather than restarting per
|
|
218
|
+
database. `focus` never applies to an inline run - a merged table exists to
|
|
219
|
+
hold every database at once.
|
|
220
|
+
|
|
221
|
+
A single-database app sees none of this: its output is unchanged.
|
|
222
|
+
|
|
149
223
|
If the [mighost](https://github.com/amberpixels/mighost) gem is installed (`gem "mighost", group: [:development, :test]`), migrations whose files were deleted (e.g. after switching branches) show up with a 👻 status and their recovered name instead of a bare `********** NO FILE **********` row - Railbow talks to it through the stable `Mighost::API`. Each ghost row carries the most informative badge mighost can provide: `≡ <version>` when the migration was re-timestamped and lives on under another version (shown with a calmer 🪦 status), `⌥ <branch>` when a branch still holds the file, or `✂ deleted in:<sha>` pointing at the commit that removed it. Ghosts dismissed via `mighost:dismiss` (or hidden by `hide_superseded`) render as plain `NO FILE`.
|
|
150
224
|
|
|
151
225
|
### `rails db:migrate:down`
|
|
@@ -193,10 +267,12 @@ rake railbow:init
|
|
|
193
267
|
|
|
194
268
|
```yaml
|
|
195
269
|
since: 70d
|
|
270
|
+
since_min: 10
|
|
196
271
|
date: rel
|
|
197
272
|
git: "author:me,diff,mask:auto"
|
|
198
273
|
view: "calendar,tables"
|
|
199
274
|
calendar: wticks
|
|
275
|
+
db: focus
|
|
200
276
|
|
|
201
277
|
compact: "maxw:120"
|
|
202
278
|
|
|
@@ -218,10 +294,12 @@ Every option can also be set via `RBW_*` environment variables, which override c
|
|
|
218
294
|
| `RBW_PLAIN` | `1` | Disable all formatting |
|
|
219
295
|
| `RBW_FORCE` | `1` | Force formatting even when piped, in CI, or run by an LLM agent (`RBW_PLAIN=1` still wins) |
|
|
220
296
|
| `RBW_SINCE` | `2mo`, `70d`, `1y`, `all` | Filter migrations by time period |
|
|
297
|
+
| `RBW_SINCE_MIN` | `10`, `0` | Never show fewer than this many migrations, however old (default 10; `0` disables) |
|
|
221
298
|
| `RBW_DATE` | `full`, `rel`, `short`, `custom(%b %d)` | Date display format |
|
|
222
299
|
| `RBW_GIT` | `author:me,diff,mask:auto` | Git integration options |
|
|
223
300
|
| `RBW_VIEW` | `calendar,tables` | Enable calendar view and table detection |
|
|
224
301
|
| `RBW_CALENDAR` | `wticks`, `wdividers,counts`, `` (empty) | Week markers: tick marks, full week separator rows, per-section counts. Empty means month separators only |
|
|
302
|
+
| `RBW_DB` | `focus` (default), `` (empty), `only:primary`, `skip:cache`, `full`, `inline` | Multi-database runs: focus one database, expand them all, pick some, or merge them into one table |
|
|
225
303
|
| `RBW_COMPACT` | `oneline,dense,noheader,maxw:80` | Compact display options |
|
|
226
304
|
| `RBW_VERB` | `GET,POST` | Filter routes by HTTP method |
|
|
227
305
|
| `RBW_SORT` | `file`, `date` | Sort order for notes |
|
data/lib/railbow/config.rb
CHANGED
|
@@ -14,10 +14,12 @@ module Railbow
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"since" => "70d",
|
|
17
|
+
"since_min" => 10,
|
|
17
18
|
"git" => "author:all,diff,mask:auto",
|
|
18
19
|
"author_format" => "short",
|
|
19
20
|
"view" => "calendar,tables",
|
|
20
|
-
"calendar" => "wticks"
|
|
21
|
+
"calendar" => "wticks",
|
|
22
|
+
"db" => "focus"
|
|
21
23
|
}.freeze
|
|
22
24
|
|
|
23
25
|
@root = nil
|
data/lib/railbow/init.rb
CHANGED
|
@@ -22,6 +22,11 @@ module Railbow
|
|
|
22
22
|
# Time window for migrations to display (e.g. 30d, 2mo, 1y, all)
|
|
23
23
|
since: "70d"
|
|
24
24
|
|
|
25
|
+
# Never show fewer than this many migrations, however old they are.
|
|
26
|
+
# The window above is a soft limit: a database with five migrations shows
|
|
27
|
+
# all five rather than reporting an empty period. 0 disables the floor.
|
|
28
|
+
since_min: 10
|
|
29
|
+
|
|
25
30
|
# Sort order: file (by filename/version) or date (by timestamp)
|
|
26
31
|
# sort: "file"
|
|
27
32
|
|
|
@@ -55,6 +60,17 @@ module Railbow
|
|
|
55
60
|
# Date format: full, rel, short, or custom(%b %d, %Y)
|
|
56
61
|
# date: "full"
|
|
57
62
|
|
|
63
|
+
# Multi-database runs (comma-separated). Leave empty ("") to expand every
|
|
64
|
+
# database into its own section, in database.yml order; a database with
|
|
65
|
+
# nothing in the time window collapses to a one-line summary either way.
|
|
66
|
+
# focus - expand only the first database, summarizing the rest.
|
|
67
|
+
# A database with migrations pending is always expanded
|
|
68
|
+
# only:<name> - render only this database (repeatable)
|
|
69
|
+
# skip:<name> - drop this database (repeatable)
|
|
70
|
+
# full - draw every database in full, overriding focus
|
|
71
|
+
# inline - one merged table ordered by version, with a Db column
|
|
72
|
+
db: "focus"
|
|
73
|
+
|
|
58
74
|
# Compact mode (comma-separated):
|
|
59
75
|
# oneline - one line per migration
|
|
60
76
|
# dense - reduce padding
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railbow
|
|
4
|
+
# Rails runs db:migrate:status once per database, inside
|
|
5
|
+
# with_temporary_pool_for_each. Each call on its own cannot know whether more
|
|
6
|
+
# databases are coming, so railbow wraps that loop: sections collect into the
|
|
7
|
+
# open batch and the batch renders them together when the loop ends.
|
|
8
|
+
#
|
|
9
|
+
# A batch is inert unless something adds to it, so wrapping the other tasks
|
|
10
|
+
# that share the same Rails helper (db:migrate, db:prepare) costs one object.
|
|
11
|
+
module MultiDb
|
|
12
|
+
KEY = :railbow_multi_db_batch
|
|
13
|
+
|
|
14
|
+
class Batch
|
|
15
|
+
attr_reader :git_cache
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@sections = []
|
|
19
|
+
@skipped = []
|
|
20
|
+
@git_cache = {}
|
|
21
|
+
@help_shown = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A database excluded by RBW_DB. Recorded rather than dropped so the run
|
|
25
|
+
# can say what it left out.
|
|
26
|
+
def skip(name)
|
|
27
|
+
@skipped << name.to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Databases sharing a migrations path see the same files, so they merge
|
|
31
|
+
# into one section that carries a status per database.
|
|
32
|
+
def add(section)
|
|
33
|
+
existing = @sections.find { |s| s.same_migrations?(section) }
|
|
34
|
+
if existing
|
|
35
|
+
existing.merge!(section)
|
|
36
|
+
else
|
|
37
|
+
@sections << section
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def claim_help
|
|
42
|
+
return false if @help_shown
|
|
43
|
+
|
|
44
|
+
@help_shown = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def empty?
|
|
48
|
+
@sections.empty? && @skipped.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def flush
|
|
52
|
+
return if empty?
|
|
53
|
+
|
|
54
|
+
Railbow::Status::Printer.new(@sections, skipped: @skipped).print
|
|
55
|
+
@sections = []
|
|
56
|
+
@skipped = []
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
# Opens a batch for the duration of the block. The flush is in an ensure so
|
|
63
|
+
# that a database aborting mid-run (a missing schema_migrations table, say)
|
|
64
|
+
# still prints the sections collected before it.
|
|
65
|
+
def batch
|
|
66
|
+
previous = Thread.current[KEY]
|
|
67
|
+
batch = Batch.new
|
|
68
|
+
Thread.current[KEY] = batch
|
|
69
|
+
begin
|
|
70
|
+
yield
|
|
71
|
+
ensure
|
|
72
|
+
Thread.current[KEY] = previous
|
|
73
|
+
batch.flush
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def current
|
|
78
|
+
Thread.current[KEY]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# True the first time it is asked within a run. Without a batch every call
|
|
82
|
+
# is its own run, so the answer is always true.
|
|
83
|
+
def claim_help
|
|
84
|
+
current ? current.claim_help : true
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/railbow/params.rb
CHANGED
|
@@ -80,6 +80,17 @@ module Railbow
|
|
|
80
80
|
(ENV["RBW_SINCE"] || Config.load["since"] || "all").strip.downcase
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Floor on how many migrations survive the SINCE window. The window is a
|
|
84
|
+
# soft limit: when it leaves fewer rows than this, the oldest ones are
|
|
85
|
+
# pulled back in until the count is met. 0 disables the floor.
|
|
86
|
+
#
|
|
87
|
+
# Deliberately a knob of its own rather than a token inside RBW_SINCE, so
|
|
88
|
+
# an ad-hoc `RBW_SINCE=2mo` keeps the floor instead of silently dropping it.
|
|
89
|
+
def since_min
|
|
90
|
+
value = ENV["RBW_SINCE_MIN"] || Config.load["since_min"] || 0
|
|
91
|
+
[value.to_i, 0].max
|
|
92
|
+
end
|
|
93
|
+
|
|
83
94
|
def sort
|
|
84
95
|
(ENV["RBW_SORT"] || Config.load["sort"] || "file").strip.downcase
|
|
85
96
|
end
|
|
@@ -215,6 +226,49 @@ module Railbow
|
|
|
215
226
|
!val.nil?
|
|
216
227
|
end
|
|
217
228
|
|
|
229
|
+
# --- Compound: RBW_DB ---
|
|
230
|
+
|
|
231
|
+
def db
|
|
232
|
+
parse_compound(ENV["RBW_DB"] || Config.load["db"])
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Render every database in one time-ordered table instead of a section
|
|
236
|
+
# each. Only meaningful when the databases keep separate migration sets.
|
|
237
|
+
def db_inline?
|
|
238
|
+
db["inline"] == true
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Draw the full table for every database, including the quiet ones that
|
|
242
|
+
# would otherwise collapse to a summary line.
|
|
243
|
+
def db_full?
|
|
244
|
+
db["full"] == true
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Expand only the first database, summarizing the rest in a line each - but
|
|
248
|
+
# never at the cost of hiding work: a database holding pending migrations
|
|
249
|
+
# is always drawn in full, since that is the one thing you may need to act
|
|
250
|
+
# on. RBW_DB=full overrides it, and it does not apply to inline runs.
|
|
251
|
+
def db_focus?
|
|
252
|
+
db["focus"] == true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def db_only
|
|
256
|
+
Array(db["only"]).select { |v| v.is_a?(String) }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def db_skip
|
|
260
|
+
Array(db["skip"]).select { |v| v.is_a?(String) }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Whether a database, by its database.yml name, is part of this run.
|
|
264
|
+
def db_included?(name)
|
|
265
|
+
name = name.to_s
|
|
266
|
+
only = db_only
|
|
267
|
+
return only.include?(name) if only.any?
|
|
268
|
+
|
|
269
|
+
!db_skip.include?(name)
|
|
270
|
+
end
|
|
271
|
+
|
|
218
272
|
# --- Compound: RBW_CALENDAR ---
|
|
219
273
|
|
|
220
274
|
def calendar
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railbow
|
|
4
|
+
module Status
|
|
5
|
+
# Recovery of "NO FILE" migrations through the optional mighost gem: rows
|
|
6
|
+
# applied to the database whose migration file is no longer on disk.
|
|
7
|
+
module Ghosts
|
|
8
|
+
GHOST_FG = "\e[38;5;217m" # matches Table::Renderer::GHOST_FG, the row it sits in
|
|
9
|
+
MUTED = "\e[38;5;245m"
|
|
10
|
+
BRANCH = "\e[38;5;222m"
|
|
11
|
+
|
|
12
|
+
# A ghost normalized for rendering, whether it came from mighost's orphan
|
|
13
|
+
# classification or from a live snapshot recovery.
|
|
14
|
+
class Row
|
|
15
|
+
attr_reader :filename, :branch_name, :source, :superseded_by, :deleted_in_sha,
|
|
16
|
+
:author_name, :author_email, :content
|
|
17
|
+
|
|
18
|
+
def initialize(filename: nil, branch_name: nil, source: nil, superseded_by: nil,
|
|
19
|
+
deleted_in_sha: nil, author_name: nil, author_email: nil, content: nil)
|
|
20
|
+
@filename = filename
|
|
21
|
+
@branch_name = branch_name
|
|
22
|
+
@source = source
|
|
23
|
+
@superseded_by = superseded_by
|
|
24
|
+
@deleted_in_sha = deleted_in_sha
|
|
25
|
+
@author_name = author_name
|
|
26
|
+
@author_email = author_email
|
|
27
|
+
@content = content
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def available?
|
|
34
|
+
defined?(Mighost::API) && Mighost.enabled?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load(versions, with_content: false)
|
|
38
|
+
# Detect once: OrphanedMigration carries the classification (supersession,
|
|
39
|
+
# deletion commit) that a bare snapshot does not, and already honors
|
|
40
|
+
# dismissals and hide_superseded.
|
|
41
|
+
orphans = begin
|
|
42
|
+
Mighost::API.orphaned_migrations.to_h { |o| [o.version.to_s, o] }
|
|
43
|
+
rescue
|
|
44
|
+
return {}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
rows = {}
|
|
48
|
+
versions.each do |v|
|
|
49
|
+
# Absent from detect = deliberately suppressed (dismissed, or superseded
|
|
50
|
+
# with hide_superseded on) - render as plain NO FILE, do not re-recover.
|
|
51
|
+
next unless (orphan = orphans[v])
|
|
52
|
+
|
|
53
|
+
row = if orphan.filename && !orphan.filename.empty?
|
|
54
|
+
from_orphan(orphan, v, with_content)
|
|
55
|
+
else
|
|
56
|
+
from_live_recovery(v, with_content)
|
|
57
|
+
end
|
|
58
|
+
rows[v] = row if row
|
|
59
|
+
end
|
|
60
|
+
rows
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def from_orphan(orphan, version, with_content)
|
|
64
|
+
Row.new(
|
|
65
|
+
filename: orphan.filename,
|
|
66
|
+
branch_name: orphan.branch_name,
|
|
67
|
+
source: read_attr(orphan, :source),
|
|
68
|
+
superseded_by: read_attr(orphan, :superseded_by),
|
|
69
|
+
deleted_in_sha: read_attr(orphan, :deleted_in_sha),
|
|
70
|
+
author_name: read_attr(orphan, :author_name),
|
|
71
|
+
author_email: read_attr(orphan, :author_email),
|
|
72
|
+
content: with_content ? snapshot_content(version) : nil
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Detect reads stored snapshots only. A version it lists without a filename
|
|
77
|
+
# has no snapshot yet, so fall back to live git/worktree recovery - that
|
|
78
|
+
# keeps fresh clones working with zero setup.
|
|
79
|
+
def from_live_recovery(version, with_content)
|
|
80
|
+
snapshot = begin
|
|
81
|
+
Mighost::API.find_or_recover_snapshot(version)
|
|
82
|
+
rescue
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
return nil unless snapshot&.filename && !snapshot.filename.empty?
|
|
86
|
+
|
|
87
|
+
Row.new(
|
|
88
|
+
filename: snapshot.filename,
|
|
89
|
+
branch_name: snapshot.branch_name,
|
|
90
|
+
source: read_attr(snapshot, :source),
|
|
91
|
+
superseded_by: api_superseded_by(version),
|
|
92
|
+
deleted_in_sha: read_attr(snapshot, :deleted_in_sha),
|
|
93
|
+
author_name: read_attr(snapshot, :author_name),
|
|
94
|
+
author_email: read_attr(snapshot, :author_email),
|
|
95
|
+
content: with_content ? snapshot.content : nil
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def read_attr(obj, name)
|
|
100
|
+
obj.respond_to?(name) ? obj.public_send(name) : nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def snapshot_content(version)
|
|
104
|
+
Mighost::API.find_snapshot(version)&.content
|
|
105
|
+
rescue
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def api_superseded_by(version)
|
|
110
|
+
return nil unless Mighost::API.respond_to?(:superseded_by)
|
|
111
|
+
|
|
112
|
+
Mighost::API.superseded_by(version)
|
|
113
|
+
rescue
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# One tag slot per ghost row; the most informative wins. Each ends in the
|
|
118
|
+
# ghost foreground rather than a reset, so the rest of the row keeps its
|
|
119
|
+
# ghost styling.
|
|
120
|
+
def tag(ghost)
|
|
121
|
+
if ghost.superseded_by
|
|
122
|
+
"#{MUTED}≡ #{ghost.superseded_by}#{GHOST_FG}"
|
|
123
|
+
elsif ghost.branch_name
|
|
124
|
+
if ghost.source == "worktree"
|
|
125
|
+
"#{BRANCH}⌥ₜ#{ghost.branch_name}#{GHOST_FG}"
|
|
126
|
+
else
|
|
127
|
+
"#{BRANCH}⌥ #{ghost.branch_name}#{GHOST_FG}"
|
|
128
|
+
end
|
|
129
|
+
elsif ghost.deleted_in_sha
|
|
130
|
+
"#{MUTED}✂ deleted in:#{ghost.deleted_in_sha[0, 8]}#{GHOST_FG}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The name a ghost row displays, derived from its recovered filename.
|
|
135
|
+
def display_name(ghost)
|
|
136
|
+
ghost.filename
|
|
137
|
+
.sub(/\A\d+_/, "") # strip version prefix
|
|
138
|
+
.sub(/\.rb\z/, "") # strip extension
|
|
139
|
+
.tr("_", " ")
|
|
140
|
+
.gsub(/\b\w/, &:upcase) # titleize
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|