railbow 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -0
- data/README.md +40 -16
- data/lib/railbow/about_formatter.rb +57 -21
- data/lib/railbow/calendar.rb +2 -4
- data/lib/railbow/clock.rb +72 -0
- data/lib/railbow/color_assigner.rb +6 -12
- data/lib/railbow/config.rb +1 -1
- data/lib/railbow/demo/fixtures.rb +4 -4
- data/lib/railbow/demo/status_demo.rb +5 -11
- data/lib/railbow/formatters/base.rb +34 -26
- data/lib/railbow/init.rb +6 -2
- data/lib/railbow/migration_formatter.rb +41 -25
- data/lib/railbow/migration_parser.rb +12 -19
- data/lib/railbow/multi_db.rb +15 -1
- data/lib/railbow/name_collision_resolver.rb +14 -27
- data/lib/railbow/name_formatter.rb +15 -16
- data/lib/railbow/notes_formatter.rb +42 -19
- data/lib/railbow/params.rb +33 -27
- data/lib/railbow/railtie.rb +8 -6
- data/lib/railbow/routes_formatter.rb +18 -6
- data/lib/railbow/stats_formatter.rb +139 -69
- data/lib/railbow/status/ghosts.rb +5 -6
- data/lib/railbow/status/git_data.rb +11 -18
- data/lib/railbow/status/help.rb +8 -2
- data/lib/railbow/status/printer.rb +20 -34
- data/lib/railbow/status/section.rb +29 -44
- data/lib/railbow/table/renderer.rb +32 -36
- data/lib/railbow/tasks/migrate_status.rake +33 -17
- data/lib/railbow/tasks/stats.rake +11 -75
- data/lib/railbow/version.rb +1 -1
- data/lib/railbow.rb +28 -4
- metadata +2 -2
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "../clock"
|
|
4
6
|
require_relative "../git_utils"
|
|
5
7
|
require_relative "../multi_db"
|
|
6
8
|
require_relative "../params"
|
|
7
9
|
|
|
8
10
|
module Railbow
|
|
9
11
|
module Status
|
|
10
|
-
# Every git lookup db:migrate:status makes about
|
|
11
|
-
#
|
|
12
|
-
# introduced it, and what is still uncommitted.
|
|
13
|
-
#
|
|
14
|
-
# One instance per migrations directory. Multi-database runs reuse the
|
|
15
|
-
# instance across databases that share a directory, which is what keeps
|
|
16
|
-
# `git log` from running once per database.
|
|
12
|
+
# Every git lookup db:migrate:status makes about one migrations directory:
|
|
13
|
+
# authors, landing dates, originating branches and uncommitted files.
|
|
17
14
|
class GitData
|
|
18
15
|
EMPTY_AUTHORS = {names: {}, emails: {}}.freeze
|
|
19
16
|
|
|
@@ -37,10 +34,8 @@ module Railbow
|
|
|
37
34
|
load_diff(branch_mask, base_override) if diff_enabled && migrate_dir
|
|
38
35
|
end
|
|
39
36
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# several databases would otherwise repeat the same full-history
|
|
43
|
-
# `git log` once per database.
|
|
37
|
+
# One instance per migrations directory across a multi-database run, so
|
|
38
|
+
# the full-history `git log` does not repeat once per database.
|
|
44
39
|
def self.for(migrate_dir:, **opts)
|
|
45
40
|
cache = Railbow::MultiDb.current&.git_cache
|
|
46
41
|
return new(migrate_dir: migrate_dir, **opts) unless cache
|
|
@@ -137,8 +132,8 @@ module Railbow
|
|
|
137
132
|
line = line.strip
|
|
138
133
|
if line.start_with?("COMMIT:")
|
|
139
134
|
current_date = begin
|
|
140
|
-
|
|
141
|
-
rescue
|
|
135
|
+
Railbow::Clock.in_zone(Time.iso8601(line.sub("COMMIT:", ""))).to_date
|
|
136
|
+
rescue ArgumentError
|
|
142
137
|
nil
|
|
143
138
|
end
|
|
144
139
|
elsif !line.empty? && current_date
|
|
@@ -242,11 +237,9 @@ module Railbow
|
|
|
242
237
|
branches_out.each_line.map(&:strip).reject(&:empty?)
|
|
243
238
|
end
|
|
244
239
|
|
|
245
|
-
# Picks the branch that
|
|
246
|
-
#
|
|
247
|
-
#
|
|
248
|
-
# adding commit - it has been active longest since, so it is the
|
|
249
|
-
# original rather than a newer fork.
|
|
240
|
+
# Picks the branch that introduced the commit: child branches drop out
|
|
241
|
+
# (if A is an ancestor of B, the commit came from A), then the branch
|
|
242
|
+
# with the most commits since wins, as the original rather than a fork.
|
|
250
243
|
def pick_origin_branch(branches, commit)
|
|
251
244
|
return branches.first if branches.size == 1
|
|
252
245
|
|
data/lib/railbow/status/help.rb
CHANGED
|
@@ -33,6 +33,12 @@ module Railbow
|
|
|
33
33
|
short - Jan 30 (column: Date)
|
|
34
34
|
custom(…) - user strftime, e.g. custom(%b %d, %Y)
|
|
35
35
|
|
|
36
|
+
RBW_TZ=<zone> Timezone dates are shown in (default: local):
|
|
37
|
+
local - this machine's timezone
|
|
38
|
+
utc - UTC, the digits of the Migration ID
|
|
39
|
+
<name> - any zone Rails knows, e.g.
|
|
40
|
+
America/New_York
|
|
41
|
+
|
|
36
42
|
RBW_VIEW=<options> Display options (comma-separated):
|
|
37
43
|
calendar - show month/year separator lines + week ticks
|
|
38
44
|
tables - parse migration files, show Tables column
|
|
@@ -86,7 +92,7 @@ module Railbow
|
|
|
86
92
|
diff - tag migrations by git origin
|
|
87
93
|
base:<branch> - base branch for diff (default: auto-detected)
|
|
88
94
|
mask:<re> - regex to extract branch label
|
|
89
|
-
e.g. mask:(
|
|
95
|
+
e.g. mask:(PS-[^/]+)/
|
|
90
96
|
mask:auto - auto-extract ticket id from branch name
|
|
91
97
|
|
|
92
98
|
RBW_PLAIN=1 Disable Railbow formatting (plain Rails output)
|
|
@@ -109,7 +115,7 @@ module Railbow
|
|
|
109
115
|
RBW_DATE='custom(%b %d, %Y)' rake db:migrate:status
|
|
110
116
|
RBW_GIT=diff rake db:migrate:status
|
|
111
117
|
RBW_GIT=diff,base:develop rake db:migrate:status
|
|
112
|
-
RBW_GIT=diff,mask:(
|
|
118
|
+
RBW_GIT=diff,mask:(PS-[^/]+)/ rake db:migrate:status
|
|
113
119
|
RBW_DB= rake db:migrate:status
|
|
114
120
|
RBW_DB=only:primary rake db:migrate:status
|
|
115
121
|
RBW_DB=inline rake db:migrate:status
|
|
@@ -9,12 +9,9 @@ require_relative "../table"
|
|
|
9
9
|
|
|
10
10
|
module Railbow
|
|
11
11
|
module Status
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# A run holding one database renders exactly as it did before multi-database
|
|
17
|
-
# support existed: no overview line, no section rule, nothing new.
|
|
12
|
+
# The only place db:migrate:status writes to stdout, which is what lets a
|
|
13
|
+
# multi-database run share column widths and print a single header. A
|
|
14
|
+
# single-database run gets no overview line and no section rule.
|
|
18
15
|
class Printer
|
|
19
16
|
RULE = "─"
|
|
20
17
|
COLLAPSED = "⋯"
|
|
@@ -88,9 +85,8 @@ module Railbow
|
|
|
88
85
|
puts render_table(section)
|
|
89
86
|
end
|
|
90
87
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
# "hidden - SINCE=70d" would read as a contradiction next to them.
|
|
88
|
+
# Admits when the floor overrode the window: a bare "hidden - SINCE=90d"
|
|
89
|
+
# would contradict the older rows shown below it.
|
|
94
90
|
def hidden_note(section)
|
|
95
91
|
return nil unless section.hidden_count > 0
|
|
96
92
|
|
|
@@ -110,8 +106,7 @@ module Railbow
|
|
|
110
106
|
puts
|
|
111
107
|
puts section_header(section, renderer.fixed_width(widths))
|
|
112
108
|
|
|
113
|
-
# Reachable with RBW_DB=full, which
|
|
114
|
-
# in full, including the ones that would otherwise have collapsed.
|
|
109
|
+
# Reachable with RBW_DB=full, which draws even quiet sections in full.
|
|
115
110
|
if section.state == :no_migrations
|
|
116
111
|
puts formatter.yellow(" No migrations found")
|
|
117
112
|
return
|
|
@@ -123,10 +118,9 @@ module Railbow
|
|
|
123
118
|
puts render_table(section, renderer)
|
|
124
119
|
end
|
|
125
120
|
|
|
126
|
-
# RBW_DB=inline: every database in one time-ordered table
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
# it. Quiet databases still collapse below the table.
|
|
121
|
+
# RBW_DB=inline: every database in one time-ordered table with a Db
|
|
122
|
+
# badge per row, so the calendar spans the whole application. Quiet
|
|
123
|
+
# databases still collapse below the table.
|
|
130
124
|
def print_inline
|
|
131
125
|
live = sections.reject { |s| collapse?(s) || s.rows.empty? }
|
|
132
126
|
collapsed = sections - live
|
|
@@ -149,9 +143,8 @@ module Railbow
|
|
|
149
143
|
collapsed.each { |section| puts collapsed_line(section) }
|
|
150
144
|
end
|
|
151
145
|
|
|
152
|
-
# Sorted by version
|
|
153
|
-
#
|
|
154
|
-
# that share a timestamp in database.yml order rather than an arbitrary one.
|
|
146
|
+
# Sorted by version; ties fall back to database.yml order rather than an
|
|
147
|
+
# arbitrary one.
|
|
155
148
|
def inline_entries(live)
|
|
156
149
|
assigner = Railbow::ColorAssigner.new(live.map { |s| inline_label(s) })
|
|
157
150
|
|
|
@@ -209,27 +202,21 @@ module Railbow
|
|
|
209
202
|
puts formatter.dim("#{COLLAPSED} #{count_label(skipped.size, "database")} hidden: #{skipped.join(", ")}")
|
|
210
203
|
end
|
|
211
204
|
|
|
212
|
-
# A quiet database
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
# RBW_DB=focus collapses the busy ones too, keeping the first database as
|
|
217
|
-
# the one you actually read. Pending migrations veto that: a database
|
|
218
|
-
# with work waiting is drawn in full however focused the run is.
|
|
205
|
+
# A quiet database has no table to draw, so one line says it all.
|
|
206
|
+
# RBW_DB=focus collapses all but the first database too, unless one has
|
|
207
|
+
# migrations pending.
|
|
219
208
|
def collapse?(section)
|
|
220
209
|
return false if Railbow::Params.db_full?
|
|
221
210
|
return true if section.quiet?
|
|
222
|
-
# Focus
|
|
223
|
-
# drop every database but the first out of the very thing that exists
|
|
224
|
-
# to hold all of them.
|
|
211
|
+
# Focus would empty the inline table of every database but the first.
|
|
225
212
|
return false if Railbow::Params.db_inline?
|
|
226
213
|
return false unless Railbow::Params.db_focus?
|
|
227
214
|
|
|
228
215
|
!focused?(section) && !section.pending_in_view?
|
|
229
216
|
end
|
|
230
217
|
|
|
231
|
-
# The first database in database.yml
|
|
232
|
-
#
|
|
218
|
+
# The first database in database.yml: conventionally primary, and the one
|
|
219
|
+
# Rails migrates first.
|
|
233
220
|
def focused?(section)
|
|
234
221
|
section.equal?(sections.first)
|
|
235
222
|
end
|
|
@@ -254,7 +241,7 @@ module Railbow
|
|
|
254
241
|
end
|
|
255
242
|
end
|
|
256
243
|
|
|
257
|
-
# "──── primary ·
|
|
244
|
+
# "──── primary · pawshop-dev ──── 12 of 240 ────", ruled out to where
|
|
258
245
|
# the last column starts so it frames the table rather than floating.
|
|
259
246
|
def section_header(section, rule_width)
|
|
260
247
|
label = section.db_names.join(" + ")
|
|
@@ -279,9 +266,8 @@ module Railbow
|
|
|
279
266
|
nil
|
|
280
267
|
end
|
|
281
268
|
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
# says "last 10 of 50" - the same words the single-database note uses.
|
|
269
|
+
# "last 10 of 50" when the floor overrode the window, since a bare
|
|
270
|
+
# "10 of 50" reads as "10 fell inside the window".
|
|
285
271
|
def shown_count(section)
|
|
286
272
|
shown = section.total_count - section.hidden_count
|
|
287
273
|
section.floor_applied? ? "last #{shown}" : shown.to_s
|
|
@@ -12,16 +12,11 @@ require_relative "ghosts"
|
|
|
12
12
|
|
|
13
13
|
module Railbow
|
|
14
14
|
module Status
|
|
15
|
-
# One migrations directory's worth of db:migrate:status.
|
|
16
|
-
#
|
|
17
|
-
# Usually that is one database. Databases sharing a migrations_paths (the
|
|
18
|
-
# shape horizontal sharding takes) see the same files and differ only in
|
|
19
|
-
# what they have applied, so they merge into a single section carrying a
|
|
15
|
+
# One migrations directory's worth of db:migrate:status. Databases sharing
|
|
16
|
+
# a migrations_paths (horizontal sharding) merge into one section with a
|
|
20
17
|
# status per database.
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# before the status cells can be built. A section never prints: Printer
|
|
24
|
-
# owns output, which is what lets sections share column widths.
|
|
18
|
+
# Rows are built lazily because merging must finish first. Printer owns
|
|
19
|
+
# output, which is what lets sections share column widths.
|
|
25
20
|
class Section
|
|
26
21
|
# Index of the Date column, where calendar week ticks are drawn.
|
|
27
22
|
TICK_COL = 2
|
|
@@ -42,8 +37,11 @@ module Railbow
|
|
|
42
37
|
attr_reader :databases, :migrations_key, :state, :hidden_count, :total_count,
|
|
43
38
|
:since_value, :entries, :ghosts
|
|
44
39
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
40
|
+
# What stock Rails prints for the databases in this section, shown if
|
|
41
|
+
# railbow fails to render it.
|
|
42
|
+
attr_accessor :fallback_output
|
|
43
|
+
|
|
44
|
+
# True when the floor topped the section back up, so the table shows
|
|
47
45
|
# migrations from outside the window it advertises.
|
|
48
46
|
def floor_applied?
|
|
49
47
|
@floor_applied == true
|
|
@@ -64,8 +62,7 @@ module Railbow
|
|
|
64
62
|
collect(pool)
|
|
65
63
|
end
|
|
66
64
|
|
|
67
|
-
#
|
|
68
|
-
# #databases; this is the single-database convenience.
|
|
65
|
+
# Single-database convenience; multi-database sections use #databases.
|
|
69
66
|
def database
|
|
70
67
|
databases.first[:database]
|
|
71
68
|
end
|
|
@@ -82,9 +79,8 @@ module Railbow
|
|
|
82
79
|
migrations_key == other.migrations_key
|
|
83
80
|
end
|
|
84
81
|
|
|
85
|
-
# Folds another database
|
|
86
|
-
#
|
|
87
|
-
# header, and anything already built is discarded.
|
|
82
|
+
# Folds in another database running the same migration files. Anything
|
|
83
|
+
# already built is discarded.
|
|
88
84
|
def merge!(other)
|
|
89
85
|
other.entries.each do |version, entry|
|
|
90
86
|
mine = (@entries[version] ||= {name: entry[:name], statuses: {}})
|
|
@@ -92,6 +88,7 @@ module Railbow
|
|
|
92
88
|
mine[:statuses].merge!(entry[:statuses])
|
|
93
89
|
end
|
|
94
90
|
@databases.concat(other.databases)
|
|
91
|
+
@fallback_output = [fallback_output, other.fallback_output].compact.join
|
|
95
92
|
@ghosts = other.ghosts.merge(@ghosts)
|
|
96
93
|
@total_count = [@total_count, other.total_count].max
|
|
97
94
|
@hidden_count = [@hidden_count, other.hidden_count].max
|
|
@@ -107,10 +104,8 @@ module Railbow
|
|
|
107
104
|
databases.size > 1
|
|
108
105
|
end
|
|
109
106
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# point, since it is asked in order to decide whether to build the rows
|
|
113
|
-
# at all.
|
|
107
|
+
# Answered from collected entries, so it costs no git work: it is asked
|
|
108
|
+
# to decide whether to build the rows at all.
|
|
114
109
|
def pending_in_view?
|
|
115
110
|
entries.any? { |_, entry| entry[:statuses].value?("down") }
|
|
116
111
|
end
|
|
@@ -223,11 +218,9 @@ module Railbow
|
|
|
223
218
|
@latest_applied_version = applied.last&.dig(1)&.to_s
|
|
224
219
|
end
|
|
225
220
|
|
|
226
|
-
# The time window is a soft limit
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
# old. The window is still what decides whether the section reads as
|
|
230
|
-
# recent - see #collect.
|
|
221
|
+
# The time window is a soft limit: the floor tops the result back up to
|
|
222
|
+
# RBW_SINCE_MIN rows. The window alone still decides whether the section
|
|
223
|
+
# reads as recent (see #collect).
|
|
231
224
|
def apply_since_filter(db_list)
|
|
232
225
|
since_cutoff = Railbow::Params.parse_since(since_value, context: "migrations")
|
|
233
226
|
unless since_cutoff
|
|
@@ -235,7 +228,7 @@ module Railbow
|
|
|
235
228
|
return db_list
|
|
236
229
|
end
|
|
237
230
|
|
|
238
|
-
cutoff_version =
|
|
231
|
+
cutoff_version = Railbow::Clock.version_at(since_cutoff).to_i
|
|
239
232
|
@window_count = db_list.count { |_, v, _| v.to_i >= cutoff_version }
|
|
240
233
|
|
|
241
234
|
# Not Comparable#clamp: the floor can exceed the total, and clamp
|
|
@@ -313,13 +306,9 @@ module Railbow
|
|
|
313
306
|
@calendar = build_calendar
|
|
314
307
|
end
|
|
315
308
|
|
|
316
|
-
#
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
# On a terminal too narrow for the full row the width budget degrades the
|
|
320
|
-
# table instead of letting it wrap: the name column shrinks first, then
|
|
321
|
-
# Tables is dropped, then the formatted date (the raw Migration ID keeps
|
|
322
|
-
# the timestamp), then Who.
|
|
309
|
+
# On a narrow terminal the table degrades instead of wrapping: the name
|
|
310
|
+
# column shrinks first, then Tables, the date (the Migration ID keeps the
|
|
311
|
+
# timestamp) and Who are dropped, in that order.
|
|
323
312
|
def build_columns
|
|
324
313
|
@name_col_width = needs_name_truncation? ? NAME_COL_WIDTH : nil
|
|
325
314
|
|
|
@@ -332,6 +321,7 @@ module Railbow
|
|
|
332
321
|
Table::Column.new(label: "Migration Name",
|
|
333
322
|
max_width: @name_col_width,
|
|
334
323
|
truncate: !@name_col_width.nil?,
|
|
324
|
+
truncate_fn: ->(cell_raw, max_w) { formatter.name_with_tags_fitted(cell_raw, max_w) },
|
|
335
325
|
shrinkable: true, shrink_floor: NAME_COL_MIN_WIDTH)
|
|
336
326
|
]
|
|
337
327
|
cols << Table::Column.new(label: "Who", droppable: 3) if author_mode == "all"
|
|
@@ -376,9 +366,8 @@ module Railbow
|
|
|
376
366
|
name = entry[:name]
|
|
377
367
|
statuses = entry[:statuses]
|
|
378
368
|
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
# a row pending everywhere reads as inactive.
|
|
369
|
+
# Pending rows are greyed out as not in effect; in a shard group only
|
|
370
|
+
# a row pending everywhere.
|
|
382
371
|
@down_rows << idx if statuses.values.all? { |s| s == "down" }
|
|
383
372
|
|
|
384
373
|
ghost = name.include?(NO_FILE) ? ghosts[version] : nil
|
|
@@ -425,8 +414,7 @@ module Railbow
|
|
|
425
414
|
@status_aliases[status] || status
|
|
426
415
|
end
|
|
427
416
|
|
|
428
|
-
#
|
|
429
|
-
# carries. Yields a status indicator when the row earns one.
|
|
417
|
+
# Yields a status indicator when the row earns one.
|
|
430
418
|
def name_cell(name:, version:, ghost:, latest_mig_date:)
|
|
431
419
|
if ghost
|
|
432
420
|
return with_tags(Ghosts.display_name(ghost), Ghosts.tag(ghost), strip: false)
|
|
@@ -467,8 +455,8 @@ module Railbow
|
|
|
467
455
|
formatter.landed_tag(landed, fresh: latest_mig_date && landed >= latest_mig_date)
|
|
468
456
|
end
|
|
469
457
|
|
|
470
|
-
#
|
|
471
|
-
#
|
|
458
|
+
# Tags sit flush right in a capped column, two spaces after the name
|
|
459
|
+
# otherwise.
|
|
472
460
|
def with_tags(name, tags, strip: true)
|
|
473
461
|
return name if tags.nil? || tags.empty?
|
|
474
462
|
return "#{name} #{tags}" unless @name_col_width
|
|
@@ -552,10 +540,7 @@ module Railbow
|
|
|
552
540
|
end
|
|
553
541
|
|
|
554
542
|
def version_date(version)
|
|
555
|
-
|
|
556
|
-
Date.new(v[0..3].to_i, v[4..5].to_i, v[6..7].to_i)
|
|
557
|
-
rescue Date::Error
|
|
558
|
-
nil
|
|
543
|
+
Railbow::Clock.from_version(version)&.to_date
|
|
559
544
|
end
|
|
560
545
|
end
|
|
561
546
|
end
|
|
@@ -61,10 +61,9 @@ module Railbow
|
|
|
61
61
|
|
|
62
62
|
rows = prepare_rows(rows)
|
|
63
63
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# again with what is left.
|
|
64
|
+
# When even fully shrunk columns overflow the terminal, drop the most
|
|
65
|
+
# expendable column and render again. One column per pass: the reduced
|
|
66
|
+
# renderer asks again with what is left.
|
|
68
67
|
if (drop = drop_index_to_fit(rows))
|
|
69
68
|
return dropped_renderer(drop).render(
|
|
70
69
|
rows.map { |row| row.reject.with_index { |_, i| i == drop } },
|
|
@@ -76,7 +75,6 @@ module Railbow
|
|
|
76
75
|
resolved = resolve_widths(rows)
|
|
77
76
|
shrink_to_fit!(resolved, rows)
|
|
78
77
|
|
|
79
|
-
# In oneline mode, truncate non-sticky non-last columns at resolved width
|
|
80
78
|
if @compact[:oneline]
|
|
81
79
|
rows = rows.map { |row|
|
|
82
80
|
row.each_with_index.map { |cell, i|
|
|
@@ -134,7 +132,7 @@ module Railbow
|
|
|
134
132
|
if col.fixed?
|
|
135
133
|
w = col.width
|
|
136
134
|
elsif i == last
|
|
137
|
-
#
|
|
135
|
+
# The last column flexes to the terminal, so it is never padded
|
|
138
136
|
w = 0
|
|
139
137
|
else
|
|
140
138
|
header_w = display_width(effective_label(col))
|
|
@@ -205,8 +203,6 @@ module Railbow
|
|
|
205
203
|
"#{pad}#{content}#{RESET}#{pad}"
|
|
206
204
|
}
|
|
207
205
|
|
|
208
|
-
# Join prefix parts with per-position separators.
|
|
209
|
-
# Loop index i joins column i-1 and column i (separator index = i-1).
|
|
210
206
|
# dup: appending to prefix_parts.first itself would corrupt the parts
|
|
211
207
|
# that render_last_cell measures for the wrapped-line indent.
|
|
212
208
|
prefix = prefix_parts.first.to_s.dup
|
|
@@ -216,7 +212,6 @@ module Railbow
|
|
|
216
212
|
|
|
217
213
|
last_cell_raw = row[last].to_s
|
|
218
214
|
|
|
219
|
-
# Separator before the last column has index (last - 1)
|
|
220
215
|
last_sep = separator_at(last - 1, tick_col)
|
|
221
216
|
render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: last_sep, highlight: highlight, ghost: ghost, dim: dim)
|
|
222
217
|
end
|
|
@@ -225,7 +220,6 @@ module Railbow
|
|
|
225
220
|
pad = effective_padding
|
|
226
221
|
sep = col_sep || theme.col_separator
|
|
227
222
|
|
|
228
|
-
# Truncate if configured via column settings
|
|
229
223
|
if columns[last].truncate && columns[last].max_width
|
|
230
224
|
last_cell_raw = truncate_str(last_cell_raw, columns[last].max_width)
|
|
231
225
|
end
|
|
@@ -235,7 +229,7 @@ module Railbow
|
|
|
235
229
|
prefix_width = compute_prefix_width(widths, last)
|
|
236
230
|
last_col_max = term_w ? [term_w - prefix_width - display_width(pad), MIN_LAST_COL].max : nil
|
|
237
231
|
|
|
238
|
-
#
|
|
232
|
+
# A column's own truncate_fn knows its content (table tags fold into +N)
|
|
239
233
|
if columns[last].truncate_fn && last_col_max &&
|
|
240
234
|
display_width(last_cell_plain) > last_col_max
|
|
241
235
|
last_cell_raw = columns[last].truncate_fn.call(last_cell_raw, last_col_max)
|
|
@@ -243,7 +237,6 @@ module Railbow
|
|
|
243
237
|
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
244
238
|
end
|
|
245
239
|
|
|
246
|
-
# Truncate to terminal width (by whole words) instead of wrapping
|
|
247
240
|
if columns[last].truncate && !columns[last].max_width && last_col_max &&
|
|
248
241
|
display_width(last_cell_plain) > last_col_max
|
|
249
242
|
last_cell_raw = truncate_by_words(last_cell_raw, last_col_max)
|
|
@@ -251,7 +244,6 @@ module Railbow
|
|
|
251
244
|
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
252
245
|
end
|
|
253
246
|
|
|
254
|
-
# In oneline mode, truncate instead of wrapping
|
|
255
247
|
if @compact[:oneline] && last_col_max && display_width(last_cell_plain) > last_col_max
|
|
256
248
|
last_cell_raw = truncate_by_words(last_cell_raw, last_col_max)
|
|
257
249
|
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
@@ -260,17 +252,21 @@ module Railbow
|
|
|
260
252
|
|
|
261
253
|
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
262
254
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
255
|
+
whole = "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
256
|
+
return whole unless last_col_max && display_width(strip_ansi(last_cell_raw)) > last_col_max
|
|
257
|
+
|
|
258
|
+
# Wrap only when every line fits beside the fixed columns. Otherwise the
|
|
259
|
+
# terminal re-wraps each padded continuation line, leaving a blank line
|
|
260
|
+
# before every fragment, so the value stays on its row whole.
|
|
261
|
+
room = term_w - prefix_width - display_width(pad)
|
|
262
|
+
wrapped = ansi_word_wrap(last_cell_raw, last_col_max)
|
|
263
|
+
return whole unless wrapped.all? { |line| display_width(strip_ansi(line)) <= room }
|
|
264
|
+
|
|
265
|
+
blank_prefix = prefix_parts.map { |part|
|
|
266
|
+
" " * display_width(strip_ansi(part))
|
|
267
|
+
}.join(sep)
|
|
268
|
+
"#{prefix}#{sep}#{pad}#{wrapped.first}#{RESET}#{pad}\n" +
|
|
269
|
+
wrapped[1..].map { |line| "#{blank_prefix}#{sep}#{pad}#{line}#{RESET}#{pad}" }.join("\n")
|
|
274
270
|
end
|
|
275
271
|
|
|
276
272
|
def style_cell(content, highlight: false, ghost: false, dim: false, accent: false)
|
|
@@ -294,14 +290,13 @@ module Railbow
|
|
|
294
290
|
def compute_prefix_width(widths, last)
|
|
295
291
|
pad_w = display_width(effective_padding)
|
|
296
292
|
sep_w = display_width(theme.col_separator)
|
|
297
|
-
#
|
|
293
|
+
# Padded fixed columns, the separator after each, and the last
|
|
294
|
+
# column's leading pad.
|
|
298
295
|
total = 0
|
|
299
296
|
(0...last).each do |i|
|
|
300
297
|
total += pad_w + widths[i] + pad_w
|
|
301
298
|
end
|
|
302
|
-
# Separators between columns + trailing separator before last column
|
|
303
299
|
total += sep_w * last
|
|
304
|
-
# Plus pad on last column
|
|
305
300
|
total += pad_w
|
|
306
301
|
total
|
|
307
302
|
end
|
|
@@ -388,7 +383,11 @@ module Railbow
|
|
|
388
383
|
|
|
389
384
|
widths[i] -= cut
|
|
390
385
|
overflow -= cut
|
|
391
|
-
|
|
386
|
+
# A column that knows how to re-fit its own cells does it here: a
|
|
387
|
+
# blind cut from the right would drop whatever the cell parked at its
|
|
388
|
+
# far edge (right-aligned tags) instead of shortening the content.
|
|
389
|
+
fit = col.truncate_fn || method(:truncate_ansi)
|
|
390
|
+
rows.each { |row| row[i] = fit.call(row[i].to_s, widths[i]) }
|
|
392
391
|
end
|
|
393
392
|
end
|
|
394
393
|
|
|
@@ -409,11 +408,9 @@ module Railbow
|
|
|
409
408
|
widths
|
|
410
409
|
end
|
|
411
410
|
|
|
412
|
-
#
|
|
413
|
-
#
|
|
414
|
-
#
|
|
415
|
-
# too - it is drawn as-is, so a label wider than every cell would poke
|
|
416
|
-
# past the terminal edge otherwise.
|
|
411
|
+
# Every fixed column plus a reserve for the last column, which truncates
|
|
412
|
+
# down to MIN_LAST_COL but no further. Its header label counts too: it is
|
|
413
|
+
# drawn as-is, so a label wider than every cell would poke past the edge.
|
|
417
414
|
def required_width(widths, rows)
|
|
418
415
|
last = columns.size - 1
|
|
419
416
|
last_w = rows.map { |row| display_width(strip_ansi(row[last].to_s)) }.max || 0
|
|
@@ -563,14 +560,13 @@ module Railbow
|
|
|
563
560
|
value_aliases = @aliases[:values]
|
|
564
561
|
return rows unless value_aliases&.any?
|
|
565
562
|
|
|
566
|
-
# Build column index → value alias map
|
|
567
563
|
col_map = {}
|
|
568
564
|
columns.each_with_index do |col, i|
|
|
569
565
|
next unless col.aliased
|
|
570
566
|
|
|
571
567
|
label = col.label
|
|
572
568
|
col_map[i] = value_aliases[label] if value_aliases[label]
|
|
573
|
-
#
|
|
569
|
+
# Value aliases may name a renamed column by its original label
|
|
574
570
|
original = @reverse_col_aliases[label]
|
|
575
571
|
col_map[i] = value_aliases[original] if original && value_aliases[original]
|
|
576
572
|
end
|
|
@@ -591,7 +587,7 @@ module Railbow
|
|
|
591
587
|
|
|
592
588
|
def apply_cell_alias(cell, aliases_for_col)
|
|
593
589
|
plain = strip_ansi(cell)
|
|
594
|
-
#
|
|
590
|
+
# Exact match first, then a prefix match for cells with an indicator appended
|
|
595
591
|
replacement = aliases_for_col[plain]
|
|
596
592
|
if replacement
|
|
597
593
|
cell.sub(plain) { replacement }
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "stringio"
|
|
3
4
|
require_relative "../multi_db"
|
|
4
5
|
require_relative "../status/help"
|
|
5
6
|
require_relative "../status/printer"
|
|
6
7
|
require_relative "../status/section"
|
|
7
8
|
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# the loop is what lets railbow render the databases together instead of one
|
|
13
|
-
# blind table each.
|
|
9
|
+
# Rails calls migrate_status once per database inside
|
|
10
|
+
# with_temporary_pool_for_each, for db:migrate:status and
|
|
11
|
+
# db:migrate:status:<database_name> alike. Wrapping that loop is what lets
|
|
12
|
+
# railbow render the databases together instead of one blind table each.
|
|
14
13
|
module Railbow
|
|
15
14
|
module MigrateStatusFormatter
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# applying its own defaults.
|
|
15
|
+
# Rails 7.2 through 8.1 all route through here. Arguments pass verbatim so
|
|
16
|
+
# Rails keeps applying its own defaults.
|
|
19
17
|
def with_temporary_pool_for_each(*args, **kwargs, &block)
|
|
20
18
|
Railbow::MultiDb.batch { super(*args, **kwargs, &block) }
|
|
21
19
|
end
|
|
@@ -39,17 +37,35 @@ module Railbow
|
|
|
39
37
|
Kernel.abort "Schema migrations table does not exist yet."
|
|
40
38
|
end
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
# Outside the guard: an error here is Rails' own (a database that is
|
|
41
|
+
# down) and must raise as it would without railbow. Captured while the
|
|
42
|
+
# pool is still open, it is also the fallback.
|
|
43
|
+
stock = railbow_capture_stdout { super }
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
batch
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
Railbow.guard(-> { $stdout.write(stock) }) do
|
|
46
|
+
section = Railbow::Status::Section.new(migration_connection_pool)
|
|
47
|
+
section.fallback_output = stock
|
|
48
|
+
|
|
49
|
+
# No batch (a direct call, or a Rails that skips
|
|
50
|
+
# with_temporary_pool_for_each): the section prints on its own.
|
|
51
|
+
if batch
|
|
52
|
+
batch.add(section)
|
|
53
|
+
else
|
|
54
|
+
Railbow::Status::Printer.new([section]).print
|
|
55
|
+
end
|
|
51
56
|
end
|
|
52
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def railbow_capture_stdout
|
|
62
|
+
original = $stdout
|
|
63
|
+
$stdout = StringIO.new
|
|
64
|
+
yield
|
|
65
|
+
$stdout.string
|
|
66
|
+
ensure
|
|
67
|
+
$stdout = original
|
|
68
|
+
end
|
|
53
69
|
end
|
|
54
70
|
end
|
|
55
71
|
|