railbow 0.2.0 โ 0.4.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 +68 -0
- data/README.md +48 -3
- data/lib/railbow/calendar.rb +89 -0
- data/lib/railbow/demo/status_demo.rb +22 -38
- data/lib/railbow/init.rb +7 -1
- data/lib/railbow/params.rb +24 -1
- data/lib/railbow/table/column.rb +5 -2
- data/lib/railbow/table/renderer.rb +60 -31
- data/lib/railbow/table/theme.rb +3 -2
- data/lib/railbow/tasks/migrate_status.rake +179 -91
- data/lib/railbow/version.rb +1 -1
- data/lib/railbow.rb +2 -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: 55d44fb326ce90c9bc4e843104639b49c2d96c866401bb53c962f226166bb22f
|
|
4
|
+
data.tar.gz: 808baadb49bf2e7d96586a1a809501bdbe87cfa454db019d184ca36804cc813a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c256343dee985473f88c27aa26e0bb827587d49001eaf82f7a8d85dbf3a56197d65ba12e3641d64c87cfbcac290a2b787020d1850a53ce0e8fbf746dc2d3ef9e
|
|
7
|
+
data.tar.gz: 4d5740b3a9a98aa692b7b031fa26a811709c4724c996ee9dfd00f0dfc3f339bf431499c27c3af46b354a7aeb7ae9bd95503f99402b1d5e44f3b1fc6314cd64a1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,74 @@ 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.4.0] - 2026-07-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `RBW_CALENDAR=wdividers` gives every ISO week its own separator row, the
|
|
13
|
+
fuller counterpart to `wticks`. A week that opens a new month is left to the
|
|
14
|
+
month row rather than being announced twice. The default stays `wticks`, so
|
|
15
|
+
existing output is unchanged until you opt in.
|
|
16
|
+
- `RBW_CALENDAR=counts` appends `ยท N migrations` to every separator row: how
|
|
17
|
+
many migrations that section holds, counted up to the next separator.
|
|
18
|
+
- `RBW_CALENDAR=wlabel:<fmt>` sets the strftime pattern for week rows,
|
|
19
|
+
alongside the existing `label:` for month rows. It defaults to the month
|
|
20
|
+
pattern, so the week number sits at the same offset on every separator row
|
|
21
|
+
rather than shifting left when no new month is announced.
|
|
22
|
+
- `railbow demo status` now honors `RBW_CALENDAR`, so the calendar modes can be
|
|
23
|
+
tried without a Rails project.
|
|
24
|
+
- An empty `RBW_CALENDAR` (month separators, no week markers) is now documented
|
|
25
|
+
in the help output, the `railbow init` template, and the README. It always
|
|
26
|
+
worked; nobody could have guessed it.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Calendar separator rows are drawn in a muted purple (256-color 97) instead of
|
|
31
|
+
the brighter 141, so they frame the migrations without competing with them.
|
|
32
|
+
- Migrations that are not applied (`down`) now render as a greyed-out row in
|
|
33
|
+
`db:migrate:status`, instead of signalling their state through the status
|
|
34
|
+
column alone. The row keeps its layout and its status glyph keeps its color;
|
|
35
|
+
everything else drops its own colors, so pending migrations read as inactive
|
|
36
|
+
next to the applied ones.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Rows whose last column wrapped were indented with too wide a blank prefix:
|
|
41
|
+
building the row mutated the very cell that the wrapped-line indent is
|
|
42
|
+
measured from. Reachable from `routes`, `about`, and `stats`.
|
|
43
|
+
- A separator label wider than its column no longer pushes the rest of the row
|
|
44
|
+
sideways. Separator rows are drawn as empty walls with the label laid over
|
|
45
|
+
them, so a long label spills into the blank space to its right.
|
|
46
|
+
- `RBW_HELP` output for `db:migrate:status` uses plain hyphens instead of
|
|
47
|
+
em-dashes, matching the rest of the user-facing text.
|
|
48
|
+
|
|
49
|
+
## [0.3.0] - 2026-07-23
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- Ghost rows in `db:migrate:status` now surface mighost 0.5's classification:
|
|
54
|
+
a superseded ghost (re-timestamped migration) shows a calmer ๐ชฆ status with
|
|
55
|
+
a `โก <version>` badge pointing at its successor, and a ghost deleted with no
|
|
56
|
+
surviving branch shows `โ deleted in:<sha>`. Branch badges are unchanged and
|
|
57
|
+
take precedence right after supersession.
|
|
58
|
+
- `RBW_FORCE=1` forces Railbow formatting past every auto-detection
|
|
59
|
+
(`NO_COLOR`, CI, piped output, LLM agent detection). An explicit
|
|
60
|
+
`RBW_PLAIN=1` still wins.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- Ghost data now comes from `Mighost::API.orphaned_migrations`, so
|
|
65
|
+
`db:migrate:status` honors mighost dismissals and `hide_superseded` for the
|
|
66
|
+
first time: a dismissed or hidden ghost renders as plain `NO FILE` instead
|
|
67
|
+
of ๐ป. Live git/worktree recovery still kicks in for versions without a
|
|
68
|
+
stored snapshot, so fresh clones keep working with zero setup.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- A ghost row without a branch no longer inherits the previous ghost row's
|
|
73
|
+
branch badge (a stale local leaked across loop iterations; unreachable
|
|
74
|
+
before mighost 0.5 made branchless ghosts the normal case).
|
|
75
|
+
|
|
8
76
|
## [0.2.0] - 2026-07-23
|
|
9
77
|
|
|
10
78
|
### Added
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- **Notes** - `rails notes` with git blame, author colors, date filtering, and sorting
|
|
22
22
|
- **About** - polished `rails about` output
|
|
23
23
|
- **Git Integration** - authors, diffs, branch origin, landing dates, uncommitted file indicators
|
|
24
|
-
- **Calendar View** - month separators
|
|
24
|
+
- **Calendar View** - month separators plus week ticks or full week rows, with optional per-section counts
|
|
25
25
|
- **Ghost Recovery** - uses the [mighost](https://github.com/amberpixels/mighost) gem (optional) to recover names, authors, and branch badges for `NO FILE` migrations
|
|
26
26
|
- **Smart Defaults** - auto-disables in CI, piped output, `NO_COLOR`, and LLM agents
|
|
27
27
|
|
|
@@ -105,8 +105,48 @@ Out of the box you get:
|
|
|
105
105
|
- **Affected tables** - color-coded table names extracted from migration files
|
|
106
106
|
- **Time filtering** - only the last 70 days shown by default (`since: 70d`)
|
|
107
107
|
- **Your migrations highlighted** - rows authored by you are visually distinct
|
|
108
|
+
- **Pending migrations greyed out** - a `down` row keeps its status glyph but loses its colors
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
#### Weekly view
|
|
111
|
+
|
|
112
|
+
By default each new ISO week is marked with a tick on the date column
|
|
113
|
+
(`calendar: wticks`). For a fuller weekly breakdown, `wdividers` gives every
|
|
114
|
+
week its own separator row, and `counts` appends how many migrations sit under
|
|
115
|
+
each separator, week or month:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
RBW_CALENDAR=wdividers,counts rake db:migrate:status
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
Feb 2026 W06 ยท 2 migrations
|
|
123
|
+
โโ โ 20260202145343 โ 2026-02-02 14:53:43 โ Add weight field to animals โ animals
|
|
124
|
+
โโ โ 20260203134528 โ 2026-02-03 13:45:28 โ Create vaccination records โ vaccination_records
|
|
125
|
+
Feb 2026 W07 ยท 1 migration
|
|
126
|
+
โโ โ 20260210183902 โ 2026-02-10 18:39:02 โ Create pet tags โ pet_tags
|
|
127
|
+
Mar 2026 W10 ยท 1 migration
|
|
128
|
+
โโ โ 20260303120000 โ 2026-03-03 12:00:00 โ Add breed restrictions toโฆ โ adoption_policies
|
|
129
|
+
Mar 2026 W11 ยท 1 migration
|
|
130
|
+
โโ โ 20260313132325 โ 2026-03-13 13:23:25 โ Create veterinary appointmโฆ โ PS-142 โ veterinary_appointments
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Every separator carries the month, so the week number always sits at the same
|
|
134
|
+
offset instead of shifting left on rows that open no new month, and every
|
|
135
|
+
separator is drawn in the same muted purple. Both labels are `strftime`
|
|
136
|
+
patterns: `label:` for months, `wlabel:` for weeks (defaulting to the same
|
|
137
|
+
pattern).
|
|
138
|
+
|
|
139
|
+
`RBW_CALENDAR` is the list of *week* markers, so an empty value leaves you with
|
|
140
|
+
month separators and nothing else:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
view: "calendar,tables"
|
|
144
|
+
calendar: "" # month separators only
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Dropping `calendar` from `view` instead removes the month separators too.
|
|
148
|
+
|
|
149
|
+
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`.
|
|
110
150
|
|
|
111
151
|
### `rails db:migrate:down`
|
|
112
152
|
|
|
@@ -176,11 +216,12 @@ Every option can also be set via `RBW_*` environment variables, which override c
|
|
|
176
216
|
| Variable | Example | Description |
|
|
177
217
|
|---|---|---|
|
|
178
218
|
| `RBW_PLAIN` | `1` | Disable all formatting |
|
|
219
|
+
| `RBW_FORCE` | `1` | Force formatting even when piped, in CI, or run by an LLM agent (`RBW_PLAIN=1` still wins) |
|
|
179
220
|
| `RBW_SINCE` | `2mo`, `70d`, `1y`, `all` | Filter migrations by time period |
|
|
180
221
|
| `RBW_DATE` | `full`, `rel`, `short`, `custom(%b %d)` | Date display format |
|
|
181
222
|
| `RBW_GIT` | `author:me,diff,mask:auto` | Git integration options |
|
|
182
223
|
| `RBW_VIEW` | `calendar,tables` | Enable calendar view and table detection |
|
|
183
|
-
| `RBW_CALENDAR` | `wticks` |
|
|
224
|
+
| `RBW_CALENDAR` | `wticks`, `wdividers,counts`, `` (empty) | Week markers: tick marks, full week separator rows, per-section counts. Empty means month separators only |
|
|
184
225
|
| `RBW_COMPACT` | `oneline,dense,noheader,maxw:80` | Compact display options |
|
|
185
226
|
| `RBW_VERB` | `GET,POST` | Filter routes by HTTP method |
|
|
186
227
|
| `RBW_SORT` | `file`, `date` | Sort order for notes |
|
|
@@ -221,6 +262,10 @@ Formatting auto-disables when:
|
|
|
221
262
|
- Output is piped or redirected (non-TTY)
|
|
222
263
|
- Running inside an LLM agent (`CLAUDECODE` env var)
|
|
223
264
|
|
|
265
|
+
`RBW_FORCE=1` overrides all of the auto-detection above (useful for capturing
|
|
266
|
+
formatted output to a file, or letting an agent inspect the real rendering).
|
|
267
|
+
An explicit `RBW_PLAIN=1` always wins over `RBW_FORCE`.
|
|
268
|
+
|
|
224
269
|
## Requirements
|
|
225
270
|
|
|
226
271
|
- Ruby >= 3.1.0
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Railbow
|
|
6
|
+
# Turns an ordered list of migration versions into the calendar furniture that
|
|
7
|
+
# gives the status table its shape: a separator row when the month changes,
|
|
8
|
+
# optionally one when the ISO week changes, and optionally a tick mark on the
|
|
9
|
+
# date column at each week boundary.
|
|
10
|
+
#
|
|
11
|
+
# A separator introduces the rows beneath it and runs until the next separator,
|
|
12
|
+
# so the optional count always answers "how many migrations in this section".
|
|
13
|
+
module Calendar
|
|
14
|
+
DEFAULT_MONTH_LABEL = "%b %Y W%V"
|
|
15
|
+
# Week rows carry the month too, so the week number sits at the same offset
|
|
16
|
+
# on every separator row rather than jumping left when the month is absent.
|
|
17
|
+
DEFAULT_WEEK_LABEL = DEFAULT_MONTH_LABEL
|
|
18
|
+
|
|
19
|
+
# What the renderer needs to draw the calendar: a label per separator row,
|
|
20
|
+
# and which rows open a week (for tick marks). Both keyed by row index.
|
|
21
|
+
class Furniture
|
|
22
|
+
attr_reader :separators, :tick_rows
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@separators = {}
|
|
26
|
+
@tick_rows = Set.new
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def none
|
|
33
|
+
Furniture.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build(versions, weeks: false, ticks: false, counts: false,
|
|
37
|
+
month_label: DEFAULT_MONTH_LABEL, week_label: DEFAULT_WEEK_LABEL)
|
|
38
|
+
dates = versions.map { |v| parse_date(v) }
|
|
39
|
+
furniture = none
|
|
40
|
+
|
|
41
|
+
dates.each_with_index do |date, i|
|
|
42
|
+
prev = (i > 0) ? dates[i - 1] : nil
|
|
43
|
+
next unless date && prev
|
|
44
|
+
|
|
45
|
+
opens_week = week_key(prev) != week_key(date)
|
|
46
|
+
|
|
47
|
+
if month_key(prev) != month_key(date)
|
|
48
|
+
furniture.separators[i] = date.strftime(month_label)
|
|
49
|
+
elsif weeks && opens_week
|
|
50
|
+
furniture.separators[i] = date.strftime(week_label)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
furniture.tick_rows << i if ticks && opens_week
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
append_counts(furniture.separators, dates.size) if counts
|
|
57
|
+
furniture
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_date(version)
|
|
61
|
+
v = version.to_s
|
|
62
|
+
Date.new(v[0..3].to_i, v[4..5].to_i, v[6..7].to_i)
|
|
63
|
+
rescue Date::Error
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def month_key(date)
|
|
68
|
+
[date.year, date.month]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# cwyear, not year: the ISO week of Jan 1 can belong to the year before.
|
|
72
|
+
def week_key(date)
|
|
73
|
+
[date.cwyear, date.cweek]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def append_counts(separators, total)
|
|
77
|
+
indices = separators.keys.sort
|
|
78
|
+
indices.each_with_index do |index, nth|
|
|
79
|
+
section_end = indices[nth + 1] || total
|
|
80
|
+
separators[index] = "#{separators[index]} ยท #{migration_count(section_end - index)}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Spelled out: a bare number next to a week label reads as part of the date.
|
|
85
|
+
def migration_count(count)
|
|
86
|
+
"#{count} #{(count == 1) ? "migration" : "migrations"}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require "date"
|
|
4
4
|
require_relative "../formatters/base"
|
|
5
5
|
require_relative "../table"
|
|
6
|
+
require_relative "../calendar"
|
|
6
7
|
require_relative "../config"
|
|
8
|
+
require_relative "../params"
|
|
7
9
|
require_relative "fixtures"
|
|
8
10
|
|
|
9
11
|
module Railbow
|
|
@@ -20,7 +22,8 @@ module Railbow
|
|
|
20
22
|
puts "\n#{formatter.emoji(:status)} Database: #{formatter.cyan("db/development.sqlite3")}"
|
|
21
23
|
puts
|
|
22
24
|
|
|
23
|
-
#
|
|
25
|
+
# Fixed for the showcase: author:me, diff, tables, full date. The calendar
|
|
26
|
+
# options below come from the real params, so RBW_CALENDAR still applies.
|
|
24
27
|
date_format = "full"
|
|
25
28
|
tables_enabled = true
|
|
26
29
|
diff_enabled = true
|
|
@@ -28,7 +31,7 @@ module Railbow
|
|
|
28
31
|
git_email = Fixtures::DEMO_USER_EMAIL
|
|
29
32
|
git_name = Fixtures::DEMO_USER_NAME
|
|
30
33
|
|
|
31
|
-
# Latest migration date
|
|
34
|
+
# Latest migration date - for "fresh" landed badge detection
|
|
32
35
|
latest_version = migrations.last[:version]
|
|
33
36
|
latest_mig_date = parse_version_date(latest_version)
|
|
34
37
|
|
|
@@ -43,7 +46,7 @@ module Railbow
|
|
|
43
46
|
name_col_width = needs_name_truncation ? 60 : nil
|
|
44
47
|
|
|
45
48
|
table_columns = [
|
|
46
|
-
Railbow::Table::Column.new(label: "Status", max_width: 6, sticky: true),
|
|
49
|
+
Railbow::Table::Column.new(label: "Status", max_width: 6, sticky: true, accent: true),
|
|
47
50
|
Railbow::Table::Column.new(label: "Migration ID", sticky: true),
|
|
48
51
|
Railbow::Table::Column.new(label: "Created At"),
|
|
49
52
|
Railbow::Table::Column.new(label: "Migration Name",
|
|
@@ -57,7 +60,9 @@ module Railbow
|
|
|
57
60
|
|
|
58
61
|
# Build rows
|
|
59
62
|
highlight_rows = Set.new
|
|
63
|
+
down_rows = Set.new
|
|
60
64
|
rows = migrations.each_with_index.map do |m, idx|
|
|
65
|
+
down_rows << idx if m[:status] == "down"
|
|
61
66
|
colored_status = case m[:status]
|
|
62
67
|
when "up" then formatter.green_bold("up")
|
|
63
68
|
when "down" then formatter.yellow_bold("down")
|
|
@@ -106,40 +111,16 @@ module Railbow
|
|
|
106
111
|
[colored_status, m[:version], created_at, display_name, table_tags]
|
|
107
112
|
end
|
|
108
113
|
|
|
109
|
-
# Calendar
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
v = versions[i]
|
|
120
|
-
date = Date.new(v[0..3].to_i, v[4..5].to_i, v[6..7].to_i)
|
|
121
|
-
separators[i] = date.strftime(calendar_label_fmt)
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# Week ticks
|
|
127
|
-
tick_rows = Set.new
|
|
128
|
-
prev_week = nil
|
|
129
|
-
versions.each_with_index do |v, i|
|
|
130
|
-
y = v[0..3].to_i
|
|
131
|
-
m = v[4..5].to_i
|
|
132
|
-
d = v[6..7].to_i
|
|
133
|
-
next if y == 0 || m == 0 || d == 0
|
|
134
|
-
|
|
135
|
-
week = Date.new(y, m, d).cweek
|
|
136
|
-
|
|
137
|
-
if i > 0 && prev_week && week != prev_week
|
|
138
|
-
tick_rows << i
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
prev_week = week
|
|
142
|
-
end
|
|
114
|
+
# Calendar furniture. Driven by the real params, so RBW_CALENDAR can be
|
|
115
|
+
# tried out here without a Rails project.
|
|
116
|
+
calendar = Railbow::Calendar.build(
|
|
117
|
+
migrations.map { |m| m[:version] },
|
|
118
|
+
weeks: Railbow::Params.calendar_wdividers?,
|
|
119
|
+
ticks: Railbow::Params.calendar_wticks?,
|
|
120
|
+
counts: Railbow::Params.calendar_counts?,
|
|
121
|
+
month_label: Railbow::Params.calendar_label,
|
|
122
|
+
week_label: Railbow::Params.calendar_week_label
|
|
123
|
+
)
|
|
143
124
|
|
|
144
125
|
aliases = Railbow::Config.table_aliases
|
|
145
126
|
renderer = Railbow::Table::Renderer.new(
|
|
@@ -148,7 +129,10 @@ module Railbow
|
|
|
148
129
|
compact: {oneline: false, dense: false, noheader: false, maxw: nil, hidden_columns: []},
|
|
149
130
|
aliases: aliases
|
|
150
131
|
)
|
|
151
|
-
puts renderer.render(rows,
|
|
132
|
+
puts renderer.render(rows,
|
|
133
|
+
separators: calendar.separators,
|
|
134
|
+
highlight_rows: highlight_rows, dim_rows: down_rows,
|
|
135
|
+
tick_rows: calendar.tick_rows, tick_col: 2)
|
|
152
136
|
end
|
|
153
137
|
|
|
154
138
|
private
|
data/lib/railbow/init.rb
CHANGED
|
@@ -43,7 +43,13 @@ module Railbow
|
|
|
43
43
|
# View mode (comma-separated): calendar, tables
|
|
44
44
|
view: "calendar,tables"
|
|
45
45
|
|
|
46
|
-
# Calendar options
|
|
46
|
+
# Calendar options (comma-separated), all require view: calendar.
|
|
47
|
+
# Leave empty ("") for month separators only, with no week markers.
|
|
48
|
+
# wticks - week tick marks on the date column
|
|
49
|
+
# wdividers - a separator row per ISO week
|
|
50
|
+
# counts - append "ยท N migrations" to separator rows (per section)
|
|
51
|
+
# label:<fmt> - strftime for month separators (default: %b %Y W%V)
|
|
52
|
+
# wlabel:<fmt> - strftime for week separators (default: same as label)
|
|
47
53
|
calendar: "wticks"
|
|
48
54
|
|
|
49
55
|
# Date format: full, rel, short, or custom(%b %d, %Y)
|
data/lib/railbow/params.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "date"
|
|
4
|
+
require_relative "calendar"
|
|
4
5
|
require_relative "config"
|
|
5
6
|
require_relative "name_formatter"
|
|
6
7
|
require_relative "name_collision_resolver"
|
|
@@ -71,6 +72,10 @@ module Railbow
|
|
|
71
72
|
truthy?(ENV["RBW_PLAIN"])
|
|
72
73
|
end
|
|
73
74
|
|
|
75
|
+
def force?
|
|
76
|
+
truthy?(ENV["RBW_FORCE"])
|
|
77
|
+
end
|
|
78
|
+
|
|
74
79
|
def since
|
|
75
80
|
(ENV["RBW_SINCE"] || Config.load["since"] || "all").strip.downcase
|
|
76
81
|
end
|
|
@@ -222,8 +227,26 @@ module Railbow
|
|
|
222
227
|
calendar["wticks"] == true
|
|
223
228
|
end
|
|
224
229
|
|
|
230
|
+
# A separator row per ISO week, the fuller counterpart to wticks.
|
|
231
|
+
def calendar_wdividers?
|
|
232
|
+
return false unless view_calendar?
|
|
233
|
+
|
|
234
|
+
calendar["wdividers"] == true
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Append "ยท N migrations" to every separator row: how many it introduces.
|
|
238
|
+
def calendar_counts?
|
|
239
|
+
return false unless view_calendar?
|
|
240
|
+
|
|
241
|
+
calendar["counts"] == true
|
|
242
|
+
end
|
|
243
|
+
|
|
225
244
|
def calendar_label
|
|
226
|
-
calendar["label"] ||
|
|
245
|
+
calendar["label"] || Calendar::DEFAULT_MONTH_LABEL
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def calendar_week_label
|
|
249
|
+
calendar["wlabel"] || Calendar::DEFAULT_WEEK_LABEL
|
|
227
250
|
end
|
|
228
251
|
end
|
|
229
252
|
end
|
data/lib/railbow/table/column.rb
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module Railbow
|
|
4
4
|
module Table
|
|
5
5
|
class Column
|
|
6
|
-
attr_reader :label, :width, :min_width, :max_width, :align, :truncate, :truncate_fn, :sticky
|
|
6
|
+
attr_reader :label, :width, :min_width, :max_width, :align, :truncate, :truncate_fn, :sticky, :accent
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# accent: the column carries its own meaning through color (a status
|
|
9
|
+
# glyph, say), so it keeps that color when the row is dimmed.
|
|
10
|
+
def initialize(label:, width: :auto, min_width: nil, max_width: nil, align: :left, truncate: false, truncate_fn: nil, sticky: false, accent: false)
|
|
9
11
|
@label = label
|
|
10
12
|
@width = width
|
|
11
13
|
@min_width = min_width
|
|
@@ -14,6 +16,7 @@ module Railbow
|
|
|
14
16
|
@truncate = truncate
|
|
15
17
|
@truncate_fn = truncate_fn
|
|
16
18
|
@sticky = sticky
|
|
19
|
+
@accent = accent
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def fixed?
|
|
@@ -10,8 +10,9 @@ module Railbow
|
|
|
10
10
|
|
|
11
11
|
RESET = "\e[0m"
|
|
12
12
|
WHITE = "\e[97m"
|
|
13
|
-
GHOST_BG = "\e[48;5;52m" # deep red/maroon background
|
|
13
|
+
GHOST_BG = "\e[48;5;52m" # deep red/maroon background - stands out as abnormal
|
|
14
14
|
GHOST_FG = "\e[38;5;217m" # warm pink foreground for contrast
|
|
15
|
+
DIMMED_FG = "\e[38;5;242m" # muted grey - a row that is not currently in effect
|
|
15
16
|
|
|
16
17
|
attr_reader :columns, :theme
|
|
17
18
|
|
|
@@ -23,7 +24,7 @@ module Railbow
|
|
|
23
24
|
@theme = theme
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def render(rows, separators: {}, highlight_rows: Set.new, ghost_rows: Set.new, tick_rows: Set.new, tick_col: nil)
|
|
27
|
+
def render(rows, separators: {}, highlight_rows: Set.new, ghost_rows: Set.new, dim_rows: Set.new, tick_rows: Set.new, tick_col: nil)
|
|
27
28
|
return "" if columns.empty?
|
|
28
29
|
|
|
29
30
|
# Remap rows if columns were hidden
|
|
@@ -64,12 +65,11 @@ module Railbow
|
|
|
64
65
|
rows.each_with_index do |row, i|
|
|
65
66
|
tc = (tick_rows.include?(i) && tick_col) ? tick_col : nil
|
|
66
67
|
if separators.key?(i) && theme.format_separator
|
|
67
|
-
|
|
68
|
-
sep_row[1] = theme.format_separator.call(separators[i]) if columns.size > 1
|
|
69
|
-
lines << render_row(sep_row, resolved, tick_col: tc, tick_cross: true)
|
|
68
|
+
lines << render_separator_row(separators[i], resolved, tick_col: tc)
|
|
70
69
|
tc = nil # tick already shown on separator row
|
|
71
70
|
end
|
|
72
|
-
formatted = render_row(row, resolved, tick_col: tc, highlight: highlight_rows.include?(i),
|
|
71
|
+
formatted = render_row(row, resolved, tick_col: tc, highlight: highlight_rows.include?(i),
|
|
72
|
+
ghost: ghost_rows.include?(i), dim: dim_rows.include?(i))
|
|
73
73
|
lines << formatted
|
|
74
74
|
end
|
|
75
75
|
lines.join("\n")
|
|
@@ -112,44 +112,65 @@ module Railbow
|
|
|
112
112
|
}.join(theme.header_col_separator)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
# A separator row is furniture, not data: the walls are drawn empty and the
|
|
116
|
+
# label is laid over them starting at the second column. Writing over the
|
|
117
|
+
# blanks rather than filling a cell means a label wider than its column
|
|
118
|
+
# spills into the empty space to its right instead of shifting the row.
|
|
119
|
+
def render_separator_row(label, widths, tick_col: nil)
|
|
120
|
+
pad = effective_padding
|
|
121
|
+
cells = widths.map { |w| "#{pad}#{" " * w}#{pad}" }
|
|
122
|
+
|
|
123
|
+
skeleton = cells.first.to_s.dup
|
|
124
|
+
(1...cells.size).each do |i|
|
|
125
|
+
skeleton << "#{separator_at(i - 1, tick_col, cross: true)}#{cells[i]}"
|
|
126
|
+
end
|
|
127
|
+
return skeleton if cells.size < 2 || label.nil? || label.empty?
|
|
128
|
+
|
|
129
|
+
offset = display_width(cells[0]) + display_width(separator_at(0, tick_col, cross: true)) + display_width(pad)
|
|
130
|
+
tail = skeleton[(offset + display_width(label))..] || ""
|
|
131
|
+
"#{skeleton[0, offset]}#{theme.format_separator.call(label)}#{tail}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The separator character between column index and index + 1. Columns
|
|
135
|
+
# flanking the tick column get the tick variant so the week line reads
|
|
136
|
+
# as one continuous rule.
|
|
137
|
+
def separator_at(index, tick_col, cross: false)
|
|
138
|
+
flanks_tick = tick_col && (index == tick_col - 1 || index == tick_col)
|
|
139
|
+
return theme.col_separator unless flanks_tick
|
|
140
|
+
|
|
141
|
+
cross ? theme.tick_cross_separator : theme.tick_separator
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def render_row(row, widths, tick_col: nil, highlight: false, ghost: false, dim: false)
|
|
116
145
|
last = columns.size - 1
|
|
117
146
|
pad = effective_padding
|
|
118
|
-
default_sep = theme.col_separator
|
|
119
|
-
tick_sep = tick_cross ? theme.tick_cross_separator : theme.tick_separator
|
|
120
147
|
|
|
121
148
|
prefix_parts = row[0...last].each_with_index.map { |cell, i|
|
|
122
149
|
s = cell.to_s
|
|
123
150
|
cell_w = display_width(strip_ansi(s))
|
|
124
151
|
padding = " " * [widths[i] - cell_w, 0].max
|
|
125
152
|
content = (columns[i].align == :right) ? "#{padding}#{s}" : "#{s}#{padding}"
|
|
126
|
-
|
|
127
|
-
content = "#{GHOST_BG}#{GHOST_FG}#{content}#{RESET}"
|
|
128
|
-
elsif highlight
|
|
129
|
-
content = "#{WHITE}#{content}#{RESET}"
|
|
130
|
-
end
|
|
153
|
+
content = style_cell(content, highlight: highlight, ghost: ghost, dim: dim, accent: columns[i].accent)
|
|
131
154
|
"#{pad}#{content}#{RESET}#{pad}"
|
|
132
155
|
}
|
|
133
156
|
|
|
134
|
-
# Join prefix parts with per-position separators
|
|
135
|
-
# Loop index i joins column i-1 and column i (
|
|
136
|
-
#
|
|
137
|
-
|
|
157
|
+
# Join prefix parts with per-position separators.
|
|
158
|
+
# Loop index i joins column i-1 and column i (separator index = i-1).
|
|
159
|
+
# dup: appending to prefix_parts.first itself would corrupt the parts
|
|
160
|
+
# that render_last_cell measures for the wrapped-line indent.
|
|
161
|
+
prefix = prefix_parts.first.to_s.dup
|
|
138
162
|
(1...prefix_parts.size).each do |i|
|
|
139
|
-
|
|
140
|
-
sep = (tick_col && (sep_idx == tick_col - 1 || sep_idx == tick_col)) ? tick_sep : default_sep
|
|
141
|
-
prefix << "#{sep}#{prefix_parts[i]}"
|
|
163
|
+
prefix << "#{separator_at(i - 1, tick_col)}#{prefix_parts[i]}"
|
|
142
164
|
end
|
|
143
165
|
|
|
144
166
|
last_cell_raw = row[last].to_s
|
|
145
167
|
|
|
146
168
|
# Separator before the last column has index (last - 1)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: last_sep, highlight: highlight, ghost: ghost)
|
|
169
|
+
last_sep = separator_at(last - 1, tick_col)
|
|
170
|
+
render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: last_sep, highlight: highlight, ghost: ghost, dim: dim)
|
|
150
171
|
end
|
|
151
172
|
|
|
152
|
-
def render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: nil, highlight: false, ghost: false)
|
|
173
|
+
def render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: nil, highlight: false, ghost: false, dim: false)
|
|
153
174
|
pad = effective_padding
|
|
154
175
|
sep = col_sep || theme.col_separator
|
|
155
176
|
|
|
@@ -167,7 +188,7 @@ module Railbow
|
|
|
167
188
|
if columns[last].truncate_fn && last_col_max &&
|
|
168
189
|
display_width(last_cell_plain) > last_col_max
|
|
169
190
|
last_cell_raw = columns[last].truncate_fn.call(last_cell_raw, last_col_max)
|
|
170
|
-
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost)
|
|
191
|
+
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
171
192
|
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
172
193
|
end
|
|
173
194
|
|
|
@@ -175,18 +196,18 @@ module Railbow
|
|
|
175
196
|
if columns[last].truncate && !columns[last].max_width && last_col_max &&
|
|
176
197
|
display_width(last_cell_plain) > last_col_max
|
|
177
198
|
last_cell_raw = truncate_by_words(last_cell_raw, last_col_max)
|
|
178
|
-
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost)
|
|
199
|
+
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
179
200
|
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
180
201
|
end
|
|
181
202
|
|
|
182
203
|
# In oneline mode, truncate instead of wrapping
|
|
183
204
|
if @compact[:oneline] && last_col_max && display_width(last_cell_plain) > last_col_max
|
|
184
205
|
last_cell_raw = truncate_by_words(last_cell_raw, last_col_max)
|
|
185
|
-
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost)
|
|
206
|
+
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
186
207
|
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
187
208
|
end
|
|
188
209
|
|
|
189
|
-
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost)
|
|
210
|
+
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
190
211
|
|
|
191
212
|
if last_col_max && display_width(strip_ansi(last_cell_raw)) > last_col_max
|
|
192
213
|
blank_prefix = prefix_parts.map { |part|
|
|
@@ -201,9 +222,17 @@ module Railbow
|
|
|
201
222
|
end
|
|
202
223
|
end
|
|
203
224
|
|
|
204
|
-
def style_cell(content, highlight: false, ghost: false)
|
|
225
|
+
def style_cell(content, highlight: false, ghost: false, dim: false, accent: false)
|
|
205
226
|
if ghost
|
|
206
227
|
"#{GHOST_BG}#{GHOST_FG}#{content}#{RESET}"
|
|
228
|
+
elsif dim
|
|
229
|
+
# An accent column keeps its color - on a dimmed row it is the one
|
|
230
|
+
# cell still carrying meaning. Everything else drops its own colors:
|
|
231
|
+
# one flat grey is what reads as "not in effect", and the resets that
|
|
232
|
+
# end each inner color would break the grey run anyway.
|
|
233
|
+
return content if accent
|
|
234
|
+
|
|
235
|
+
"#{DIMMED_FG}#{strip_ansi(content)}#{RESET}"
|
|
207
236
|
elsif highlight
|
|
208
237
|
"#{WHITE}#{content}#{RESET}"
|
|
209
238
|
else
|
|
@@ -315,7 +344,7 @@ module Railbow
|
|
|
315
344
|
current << token
|
|
316
345
|
current_width += token_width
|
|
317
346
|
elsif current_width.zero?
|
|
318
|
-
# Single token wider than max
|
|
347
|
+
# Single token wider than max - try to break on underscores
|
|
319
348
|
broken = break_long_token(token, max_width)
|
|
320
349
|
lines.concat(broken[0...-1])
|
|
321
350
|
current = +broken.last
|
data/lib/railbow/table/theme.rb
CHANGED
|
@@ -8,7 +8,8 @@ module Railbow
|
|
|
8
8
|
:format_header_cell, :format_separator
|
|
9
9
|
|
|
10
10
|
def initialize(col_separator:, header_col_separator:, cell_padding:,
|
|
11
|
-
format_header_cell:, format_separator: nil,
|
|
11
|
+
format_header_cell:, format_separator: nil,
|
|
12
|
+
tick_separator: nil, tick_cross_separator: nil)
|
|
12
13
|
@col_separator = col_separator
|
|
13
14
|
@tick_separator = tick_separator || col_separator
|
|
14
15
|
@tick_cross_separator = tick_cross_separator || @tick_separator
|
|
@@ -23,7 +24,7 @@ module Railbow
|
|
|
23
24
|
RESET = "\e[0m"
|
|
24
25
|
BOLD = "\e[1m"
|
|
25
26
|
WHITE = "\e[97m"
|
|
26
|
-
PURPLE = "\e[38;5;
|
|
27
|
+
PURPLE = "\e[38;5;97m" # separator rows: present, but never louder than the data
|
|
27
28
|
BG_PURPLE = "\e[48;5;99m"
|
|
28
29
|
DIM = "\e[2m"
|
|
29
30
|
|
|
@@ -6,14 +6,120 @@ require_relative "../formatters/base"
|
|
|
6
6
|
require_relative "../migration_parser"
|
|
7
7
|
require_relative "../config"
|
|
8
8
|
require_relative "../table"
|
|
9
|
+
require_relative "../calendar"
|
|
9
10
|
require_relative "../logo"
|
|
10
11
|
|
|
11
12
|
# Override DatabaseTasks.migrate_status which is called by both
|
|
12
13
|
# db:migrate:status and db:migrate:status:<database_name> tasks.
|
|
13
14
|
module Railbow
|
|
14
15
|
module MigrateStatusFormatter
|
|
16
|
+
# Ghost migration data normalized for rendering, whether it came from
|
|
17
|
+
# mighost's orphan classification or a live snapshot recovery.
|
|
18
|
+
class GhostRow
|
|
19
|
+
attr_reader :filename, :branch_name, :source, :superseded_by, :deleted_in_sha,
|
|
20
|
+
:author_name, :author_email, :content
|
|
21
|
+
|
|
22
|
+
def initialize(filename: nil, branch_name: nil, source: nil, superseded_by: nil,
|
|
23
|
+
deleted_in_sha: nil, author_name: nil, author_email: nil, content: nil)
|
|
24
|
+
@filename = filename
|
|
25
|
+
@branch_name = branch_name
|
|
26
|
+
@source = source
|
|
27
|
+
@superseded_by = superseded_by
|
|
28
|
+
@deleted_in_sha = deleted_in_sha
|
|
29
|
+
@author_name = author_name
|
|
30
|
+
@author_email = author_email
|
|
31
|
+
@content = content
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
15
35
|
private
|
|
16
36
|
|
|
37
|
+
def mighost_attr(obj, name)
|
|
38
|
+
obj.respond_to?(name) ? obj.public_send(name) : nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mighost_snapshot_content(version)
|
|
42
|
+
Mighost::API.find_snapshot(version)&.content
|
|
43
|
+
rescue
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def load_ghost_rows(versions, with_content: false)
|
|
48
|
+
# Detect once: OrphanedMigration carries the classification (supersession,
|
|
49
|
+
# deletion commit) that a bare snapshot doesn't, and already honors
|
|
50
|
+
# dismissals and hide_superseded.
|
|
51
|
+
orphans = begin
|
|
52
|
+
Mighost::API.orphaned_migrations.to_h { |o| [o.version.to_s, o] }
|
|
53
|
+
rescue
|
|
54
|
+
return {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
rows = {}
|
|
58
|
+
versions.each do |v|
|
|
59
|
+
# Absent from detect = deliberately suppressed (dismissed, or superseded
|
|
60
|
+
# with hide_superseded on) - render as plain NO FILE, don't re-recover.
|
|
61
|
+
next unless (orphan = orphans[v])
|
|
62
|
+
|
|
63
|
+
if orphan.filename && !orphan.filename.empty?
|
|
64
|
+
rows[v] = GhostRow.new(
|
|
65
|
+
filename: orphan.filename,
|
|
66
|
+
branch_name: orphan.branch_name,
|
|
67
|
+
source: mighost_attr(orphan, :source),
|
|
68
|
+
superseded_by: mighost_attr(orphan, :superseded_by),
|
|
69
|
+
deleted_in_sha: mighost_attr(orphan, :deleted_in_sha),
|
|
70
|
+
author_name: mighost_attr(orphan, :author_name),
|
|
71
|
+
author_email: mighost_attr(orphan, :author_email),
|
|
72
|
+
content: with_content ? mighost_snapshot_content(v) : nil
|
|
73
|
+
)
|
|
74
|
+
else
|
|
75
|
+
# Detect reads stored snapshots only. A version it lists without a
|
|
76
|
+
# filename has no snapshot yet, so fall back to live git/worktree
|
|
77
|
+
# recovery - keeps fresh clones working with zero setup.
|
|
78
|
+
snapshot = begin
|
|
79
|
+
Mighost::API.find_or_recover_snapshot(v)
|
|
80
|
+
rescue
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
next unless snapshot&.filename && !snapshot.filename.empty?
|
|
84
|
+
|
|
85
|
+
rows[v] = GhostRow.new(
|
|
86
|
+
filename: snapshot.filename,
|
|
87
|
+
branch_name: snapshot.branch_name,
|
|
88
|
+
source: mighost_attr(snapshot, :source),
|
|
89
|
+
superseded_by: api_superseded_by(v),
|
|
90
|
+
deleted_in_sha: mighost_attr(snapshot, :deleted_in_sha),
|
|
91
|
+
author_name: mighost_attr(snapshot, :author_name),
|
|
92
|
+
author_email: mighost_attr(snapshot, :author_email),
|
|
93
|
+
content: with_content ? snapshot.content : nil
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
rows
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def api_superseded_by(version)
|
|
101
|
+
return nil unless Mighost::API.respond_to?(:superseded_by)
|
|
102
|
+
|
|
103
|
+
Mighost::API.superseded_by(version)
|
|
104
|
+
rescue
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# One tag slot per ghost row; most informative wins.
|
|
109
|
+
def ghost_tag(ghost)
|
|
110
|
+
if ghost.superseded_by
|
|
111
|
+
"\e[38;5;245mโก #{ghost.superseded_by}\e[38;5;217m"
|
|
112
|
+
elsif ghost.branch_name
|
|
113
|
+
if ghost.source == "worktree"
|
|
114
|
+
"\e[38;5;222mโฅโ#{ghost.branch_name}\e[38;5;217m"
|
|
115
|
+
else
|
|
116
|
+
"\e[38;5;222mโฅ #{ghost.branch_name}\e[38;5;217m"
|
|
117
|
+
end
|
|
118
|
+
elsif ghost.deleted_in_sha
|
|
119
|
+
"\e[38;5;245mโ deleted in:#{ghost.deleted_in_sha[0, 8]}\e[38;5;217m"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
17
123
|
def git_migration_authors(migrate_dir)
|
|
18
124
|
output, _status = Railbow::GitUtils.capture2(
|
|
19
125
|
"log", "--format=COMMIT:%aN\t%aE", "--diff-filter=AR", "--name-status", "--", migrate_dir
|
|
@@ -165,7 +271,7 @@ module Railbow
|
|
|
165
271
|
# 1. Filter out child branches: if branch A is an ancestor of branch B,
|
|
166
272
|
# the commit was introduced in A, not B.
|
|
167
273
|
# 2. Among remaining, prefer the branch with the MOST commits after the
|
|
168
|
-
# adding commit
|
|
274
|
+
# adding commit - it has been active longer since the commit was made,
|
|
169
275
|
# indicating it is the original branch (not a newer fork).
|
|
170
276
|
best = if branches.size == 1
|
|
171
277
|
branches.first
|
|
@@ -271,39 +377,50 @@ module Railbow
|
|
|
271
377
|
Units: d (days), w (weeks), mo/m (months), y (years)
|
|
272
378
|
|
|
273
379
|
RBW_DATE=<mode> Date column format (default: full):
|
|
274
|
-
full
|
|
275
|
-
rel
|
|
276
|
-
short
|
|
277
|
-
custom(โฆ)
|
|
380
|
+
full - 2026-01-30 12:08:54 (column: Created At)
|
|
381
|
+
rel - ~3d ago
|
|
382
|
+
short - Jan 30 (column: Date)
|
|
383
|
+
custom(โฆ) - user strftime, e.g. custom(%b %d, %Y)
|
|
278
384
|
|
|
279
385
|
RBW_VIEW=<options> Display options (comma-separated):
|
|
280
|
-
calendar
|
|
281
|
-
tables
|
|
386
|
+
calendar - show month/year separator lines + week ticks
|
|
387
|
+
tables - parse migration files, show Tables column
|
|
282
388
|
|
|
283
389
|
RBW_COMPACT=<options> Compact display (comma-separated):
|
|
284
|
-
oneline
|
|
285
|
-
dense
|
|
286
|
-
noheader
|
|
287
|
-
maxw:<n>
|
|
288
|
-
hide:<col>
|
|
289
|
-
|
|
290
|
-
RBW_CALENDAR=<options>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
390
|
+
oneline - truncate instead of wrapping
|
|
391
|
+
dense - remove cell padding
|
|
392
|
+
noheader - hide table header row
|
|
393
|
+
maxw:<n> - cap column widths at n chars
|
|
394
|
+
hide:<col> - hide a column by name (repeatable)
|
|
395
|
+
|
|
396
|
+
RBW_CALENDAR=<options> Calendar sub-options (requires RBW_VIEW=calendar):
|
|
397
|
+
(empty) - month separators only, no week
|
|
398
|
+
markers at all
|
|
399
|
+
wticks - week tick marks on the date column
|
|
400
|
+
wdividers - a separator row per ISO week
|
|
401
|
+
counts - append "ยท N migrations" to every
|
|
402
|
+
separator row (that section)
|
|
403
|
+
label:<fmt> - strftime for month separators
|
|
404
|
+
(default: %b %Y W%V)
|
|
405
|
+
wlabel:<fmt> - strftime for week separators
|
|
406
|
+
(default: same as label, so the
|
|
407
|
+
week number never shifts)
|
|
294
408
|
|
|
295
409
|
RBW_GIT=<options> Git integration (comma-separated):
|
|
296
|
-
author
|
|
297
|
-
author:all
|
|
298
|
-
author:me
|
|
299
|
-
diff
|
|
300
|
-
base:<branch>
|
|
301
|
-
mask:<re>
|
|
410
|
+
author - add an Author column (same as author:all)
|
|
411
|
+
author:all - add an Author column
|
|
412
|
+
author:me - highlight your own migrations
|
|
413
|
+
diff - tag migrations by git origin
|
|
414
|
+
base:<branch> - base branch for diff (default: auto-detected)
|
|
415
|
+
mask:<re> - regex to extract branch label
|
|
302
416
|
e.g. mask:(WS-[^/]+)/
|
|
303
|
-
mask:auto
|
|
417
|
+
mask:auto - auto-extract ticket id from branch name
|
|
304
418
|
|
|
305
419
|
RBW_PLAIN=1 Disable Railbow formatting (plain Rails output)
|
|
306
420
|
|
|
421
|
+
RBW_FORCE=1 Force Railbow formatting even when piped, in CI,
|
|
422
|
+
or called by an LLM agent (RBW_PLAIN=1 still wins)
|
|
423
|
+
|
|
307
424
|
RBW_HELP=1 Show this help message
|
|
308
425
|
|
|
309
426
|
\e[2mAuto-disabled when piped, in CI, or when called by an LLM agent.\e[0m
|
|
@@ -311,6 +428,7 @@ module Railbow
|
|
|
311
428
|
\e[1mExamples:\e[0m
|
|
312
429
|
rake db:migrate:status
|
|
313
430
|
RBW_SINCE=2mo RBW_VIEW=calendar rake db:migrate:status
|
|
431
|
+
RBW_CALENDAR=wdividers,counts rake db:migrate:status
|
|
314
432
|
RBW_VIEW=tables RBW_GIT=author rake db:migrate:status
|
|
315
433
|
RBW_GIT=author:me RBW_SINCE=3mo rake db:migrate:status
|
|
316
434
|
RBW_DATE=rel rake db:migrate:status
|
|
@@ -373,7 +491,7 @@ module Railbow
|
|
|
373
491
|
|
|
374
492
|
skipped = total_count - db_list.size
|
|
375
493
|
if skipped > 0
|
|
376
|
-
puts formatter.dim(" (#{skipped} older migrations hidden
|
|
494
|
+
puts formatter.dim(" (#{skipped} older migrations hidden - SINCE=#{since_value})")
|
|
377
495
|
puts
|
|
378
496
|
end
|
|
379
497
|
end
|
|
@@ -428,23 +546,16 @@ module Railbow
|
|
|
428
546
|
uncommitted_files.each { |f| branch_origins[f] ||= current_branch }
|
|
429
547
|
end
|
|
430
548
|
|
|
431
|
-
# Load mighost
|
|
549
|
+
# Load mighost ghost data for "NO FILE" migrations (if mighost gem is available)
|
|
432
550
|
mighost_snapshots = {}
|
|
433
551
|
mighost_available = defined?(Mighost::API) && Mighost.enabled?
|
|
434
552
|
if mighost_available
|
|
435
553
|
no_file_versions = db_list.select { |_, _, n| n.include?("NO FILE") }.map { |_, v, _| v.to_s }
|
|
436
|
-
no_file_versions
|
|
437
|
-
snapshot = begin
|
|
438
|
-
Mighost::API.find_or_recover_snapshot(v)
|
|
439
|
-
rescue
|
|
440
|
-
nil
|
|
441
|
-
end
|
|
442
|
-
mighost_snapshots[v] = snapshot if snapshot&.filename && !snapshot.filename.empty?
|
|
443
|
-
end
|
|
554
|
+
mighost_snapshots = load_ghost_rows(no_file_versions, with_content: tables_enabled) if no_file_versions.any?
|
|
444
555
|
end
|
|
445
556
|
|
|
446
557
|
# Build columns
|
|
447
|
-
# Latest migration ID date
|
|
558
|
+
# Latest migration ID date - used to determine "fresh" landed badges
|
|
448
559
|
latest_version = db_list.last&.dig(1).to_s
|
|
449
560
|
latest_mig_date = begin
|
|
450
561
|
Date.new(latest_version[0..3].to_i, latest_version[4..5].to_i, latest_version[6..7].to_i)
|
|
@@ -464,7 +575,7 @@ module Railbow
|
|
|
464
575
|
needs_name_truncation = tables_enabled || author_mode == "all" || diff_enabled || has_landed_tags
|
|
465
576
|
name_col_width = needs_name_truncation ? 60 : nil
|
|
466
577
|
table_columns = [
|
|
467
|
-
Railbow::Table::Column.new(label: "Status", max_width: 6, sticky: true),
|
|
578
|
+
Railbow::Table::Column.new(label: "Status", max_width: 6, sticky: true, accent: true),
|
|
468
579
|
Railbow::Table::Column.new(label: "Migration ID", sticky: true),
|
|
469
580
|
Railbow::Table::Column.new(label: (date_format == "full") ? "Created At" : "Date"),
|
|
470
581
|
Railbow::Table::Column.new(label: "Migration Name",
|
|
@@ -489,10 +600,14 @@ module Railbow
|
|
|
489
600
|
{}
|
|
490
601
|
end
|
|
491
602
|
|
|
492
|
-
# Build rows and track highlight/ghost indices
|
|
603
|
+
# Build rows and track highlight/ghost/down indices
|
|
493
604
|
highlight_rows = Set.new
|
|
494
605
|
ghost_rows = Set.new
|
|
606
|
+
down_rows = Set.new
|
|
495
607
|
rows = db_list.each_with_index.map do |(status, version, name), idx|
|
|
608
|
+
# A pending migration is not in effect yet: grey the whole row out so it
|
|
609
|
+
# reads as inactive next to the applied ones.
|
|
610
|
+
down_rows << idx if status == "down"
|
|
496
611
|
colored_status = case status
|
|
497
612
|
when "up" then formatter.green_bold("up")
|
|
498
613
|
when "down" then formatter.yellow_bold("down")
|
|
@@ -501,29 +616,25 @@ module Railbow
|
|
|
501
616
|
ghost_snapshot = name.include?("NO FILE") ? mighost_snapshots[version.to_s] : nil
|
|
502
617
|
if name.include?("NO FILE") && ghost_snapshot
|
|
503
618
|
ghost_rows << idx
|
|
504
|
-
# Mighost recovered this ghost migration
|
|
505
|
-
|
|
619
|
+
# Mighost recovered this ghost migration - show ghost status + name + badge.
|
|
620
|
+
# A superseded ghost lives on under another version: stale bookkeeping,
|
|
621
|
+
# not a lost migration, so it gets a calmer glyph.
|
|
622
|
+
colored_status = ghost_snapshot.superseded_by ? "๐ชฆ" : "๐ป"
|
|
506
623
|
ghost_name = ghost_snapshot.filename
|
|
507
624
|
.sub(/\A\d+_/, "") # strip version prefix
|
|
508
625
|
.sub(/\.rb\z/, "") # strip extension
|
|
509
626
|
.tr("_", " ")
|
|
510
627
|
.gsub(/\b\w/, &:upcase) # titleize
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
else
|
|
515
|
-
"\e[38;5;222mโฅ #{ghost_snapshot.branch_name}\e[38;5;217m"
|
|
516
|
-
end
|
|
517
|
-
end
|
|
518
|
-
if branch_tag && name_col_width
|
|
519
|
-
tag_width = formatter.display_width(formatter.strip_ansi(branch_tag))
|
|
628
|
+
ghost_badge = ghost_tag(ghost_snapshot)
|
|
629
|
+
if ghost_badge && name_col_width
|
|
630
|
+
tag_width = formatter.display_width(formatter.strip_ansi(ghost_badge))
|
|
520
631
|
available = name_col_width - tag_width - 2
|
|
521
632
|
ghost_name = formatter.truncate_str(ghost_name, available)
|
|
522
633
|
name_width = formatter.display_width(ghost_name)
|
|
523
634
|
padding = name_col_width - name_width - tag_width
|
|
524
|
-
display_name = "#{ghost_name}#{" " * [padding, 2].max}#{
|
|
525
|
-
elsif
|
|
526
|
-
display_name = "#{ghost_name} #{
|
|
635
|
+
display_name = "#{ghost_name}#{" " * [padding, 2].max}#{ghost_badge}"
|
|
636
|
+
elsif ghost_badge
|
|
637
|
+
display_name = "#{ghost_name} #{ghost_badge}"
|
|
527
638
|
else
|
|
528
639
|
display_name = ghost_name
|
|
529
640
|
end
|
|
@@ -604,7 +715,7 @@ module Railbow
|
|
|
604
715
|
filepath = version_to_file[version.to_s]
|
|
605
716
|
basename = filepath ? File.basename(filepath) : nil
|
|
606
717
|
|
|
607
|
-
# Uncommitted migrations have no git author
|
|
718
|
+
# Uncommitted migrations have no git author - treat them as mine.
|
|
608
719
|
# Match by email first; fall back to author name to handle cases where
|
|
609
720
|
# the commit email differs from git config (e.g. GitHub noreply emails
|
|
610
721
|
# after squash-merge, or mailmap rewrites).
|
|
@@ -639,44 +750,18 @@ module Railbow
|
|
|
639
750
|
row
|
|
640
751
|
end
|
|
641
752
|
|
|
642
|
-
# Calendar separators
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
date = Date.new(v[0..3].to_i, v[4..5].to_i, v[6..7].to_i)
|
|
655
|
-
separators[i] = date.strftime(calendar_label_fmt)
|
|
656
|
-
end
|
|
657
|
-
end
|
|
658
|
-
end
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
# Week tick separators: mark first row of each new ISO week
|
|
662
|
-
tick_rows = Set.new
|
|
663
|
-
if ticks_enabled
|
|
664
|
-
versions = db_list.map { |_, v, _| v.to_s }
|
|
665
|
-
prev_week = nil
|
|
666
|
-
versions.each_with_index do |v, i|
|
|
667
|
-
y = v[0..3].to_i
|
|
668
|
-
m = v[4..5].to_i
|
|
669
|
-
d = v[6..7].to_i
|
|
670
|
-
next if y == 0 || m == 0 || d == 0
|
|
671
|
-
|
|
672
|
-
week = Date.new(y, m, d).cweek
|
|
673
|
-
|
|
674
|
-
if i > 0 && prev_week && week != prev_week
|
|
675
|
-
tick_rows << i
|
|
676
|
-
end
|
|
677
|
-
|
|
678
|
-
prev_week = week
|
|
679
|
-
end
|
|
753
|
+
# Calendar furniture: month separators, week separators, week ticks
|
|
754
|
+
calendar = if calendar_enabled
|
|
755
|
+
Railbow::Calendar.build(
|
|
756
|
+
db_list.map { |_, v, _| v.to_s },
|
|
757
|
+
weeks: Railbow::Params.calendar_wdividers?,
|
|
758
|
+
ticks: ticks_enabled,
|
|
759
|
+
counts: Railbow::Params.calendar_counts?,
|
|
760
|
+
month_label: Railbow::Params.calendar_label,
|
|
761
|
+
week_label: Railbow::Params.calendar_week_label
|
|
762
|
+
)
|
|
763
|
+
else
|
|
764
|
+
Railbow::Calendar.none
|
|
680
765
|
end
|
|
681
766
|
|
|
682
767
|
renderer = Railbow::Table::Renderer.new(
|
|
@@ -686,7 +771,10 @@ module Railbow
|
|
|
686
771
|
aliases: Railbow::Config.table_aliases
|
|
687
772
|
)
|
|
688
773
|
tick_col = 2 # Date column index
|
|
689
|
-
puts renderer.render(rows,
|
|
774
|
+
puts renderer.render(rows,
|
|
775
|
+
separators: calendar.separators,
|
|
776
|
+
highlight_rows: highlight_rows, ghost_rows: ghost_rows, dim_rows: down_rows,
|
|
777
|
+
tick_rows: calendar.tick_rows, tick_col: tick_col)
|
|
690
778
|
end
|
|
691
779
|
end
|
|
692
780
|
end
|
data/lib/railbow/version.rb
CHANGED
data/lib/railbow.rb
CHANGED
|
@@ -9,8 +9,10 @@ module Railbow
|
|
|
9
9
|
|
|
10
10
|
# Returns true when Railbow formatting should be disabled.
|
|
11
11
|
# Checks for explicit opt-out, standard conventions, CI, and LLM agents.
|
|
12
|
+
# RBW_PLAIN always wins; RBW_FORCE beats every auto-detection below it.
|
|
12
13
|
def self.plain?
|
|
13
14
|
return true if Params.plain?
|
|
15
|
+
return false if Params.force?
|
|
14
16
|
return true if ENV.key?("NO_COLOR")
|
|
15
17
|
return true if ENV.key?("CLAUDECODE")
|
|
16
18
|
return true if ENV.key?("CI")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: railbow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eugene M
|
|
@@ -78,6 +78,7 @@ files:
|
|
|
78
78
|
- exe/railbow
|
|
79
79
|
- lib/railbow.rb
|
|
80
80
|
- lib/railbow/about_formatter.rb
|
|
81
|
+
- lib/railbow/calendar.rb
|
|
81
82
|
- lib/railbow/color_assigner.rb
|
|
82
83
|
- lib/railbow/config.rb
|
|
83
84
|
- lib/railbow/demo/fixtures.rb
|