railbow 0.6.2 → 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 +58 -0
- data/README.md +30 -6
- 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/status_demo.rb +4 -11
- data/lib/railbow/formatters/base.rb +11 -32
- data/lib/railbow/init.rb +5 -1
- 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 +6 -0
- data/lib/railbow/status/printer.rb +19 -33
- data/lib/railbow/status/section.rb +28 -44
- data/lib/railbow/table/renderer.rb +27 -35
- 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
|
@@ -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
|
|
@@ -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
|
|
|
@@ -377,9 +366,8 @@ module Railbow
|
|
|
377
366
|
name = entry[:name]
|
|
378
367
|
statuses = entry[:statuses]
|
|
379
368
|
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
# 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.
|
|
383
371
|
@down_rows << idx if statuses.values.all? { |s| s == "down" }
|
|
384
372
|
|
|
385
373
|
ghost = name.include?(NO_FILE) ? ghosts[version] : nil
|
|
@@ -426,8 +414,7 @@ module Railbow
|
|
|
426
414
|
@status_aliases[status] || status
|
|
427
415
|
end
|
|
428
416
|
|
|
429
|
-
#
|
|
430
|
-
# carries. Yields a status indicator when the row earns one.
|
|
417
|
+
# Yields a status indicator when the row earns one.
|
|
431
418
|
def name_cell(name:, version:, ghost:, latest_mig_date:)
|
|
432
419
|
if ghost
|
|
433
420
|
return with_tags(Ghosts.display_name(ghost), Ghosts.tag(ghost), strip: false)
|
|
@@ -468,8 +455,8 @@ module Railbow
|
|
|
468
455
|
formatter.landed_tag(landed, fresh: latest_mig_date && landed >= latest_mig_date)
|
|
469
456
|
end
|
|
470
457
|
|
|
471
|
-
#
|
|
472
|
-
#
|
|
458
|
+
# Tags sit flush right in a capped column, two spaces after the name
|
|
459
|
+
# otherwise.
|
|
473
460
|
def with_tags(name, tags, strip: true)
|
|
474
461
|
return name if tags.nil? || tags.empty?
|
|
475
462
|
return "#{name} #{tags}" unless @name_col_width
|
|
@@ -553,10 +540,7 @@ module Railbow
|
|
|
553
540
|
end
|
|
554
541
|
|
|
555
542
|
def version_date(version)
|
|
556
|
-
|
|
557
|
-
Date.new(v[0..3].to_i, v[4..5].to_i, v[6..7].to_i)
|
|
558
|
-
rescue Date::Error
|
|
559
|
-
nil
|
|
543
|
+
Railbow::Clock.from_version(version)&.to_date
|
|
560
544
|
end
|
|
561
545
|
end
|
|
562
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
|
|
@@ -413,11 +408,9 @@ module Railbow
|
|
|
413
408
|
widths
|
|
414
409
|
end
|
|
415
410
|
|
|
416
|
-
#
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
# too - it is drawn as-is, so a label wider than every cell would poke
|
|
420
|
-
# 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.
|
|
421
414
|
def required_width(widths, rows)
|
|
422
415
|
last = columns.size - 1
|
|
423
416
|
last_w = rows.map { |row| display_width(strip_ansi(row[last].to_s)) }.max || 0
|
|
@@ -567,14 +560,13 @@ module Railbow
|
|
|
567
560
|
value_aliases = @aliases[:values]
|
|
568
561
|
return rows unless value_aliases&.any?
|
|
569
562
|
|
|
570
|
-
# Build column index → value alias map
|
|
571
563
|
col_map = {}
|
|
572
564
|
columns.each_with_index do |col, i|
|
|
573
565
|
next unless col.aliased
|
|
574
566
|
|
|
575
567
|
label = col.label
|
|
576
568
|
col_map[i] = value_aliases[label] if value_aliases[label]
|
|
577
|
-
#
|
|
569
|
+
# Value aliases may name a renamed column by its original label
|
|
578
570
|
original = @reverse_col_aliases[label]
|
|
579
571
|
col_map[i] = value_aliases[original] if original && value_aliases[original]
|
|
580
572
|
end
|
|
@@ -595,7 +587,7 @@ module Railbow
|
|
|
595
587
|
|
|
596
588
|
def apply_cell_alias(cell, aliases_for_col)
|
|
597
589
|
plain = strip_ansi(cell)
|
|
598
|
-
#
|
|
590
|
+
# Exact match first, then a prefix match for cells with an indicator appended
|
|
599
591
|
replacement = aliases_for_col[plain]
|
|
600
592
|
if replacement
|
|
601
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
|
|
|
@@ -1,85 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
# CodeStatistics is autoloaded by Rails when the `stats` task runs,
|
|
5
|
-
# but is not available at rake-file load time.
|
|
6
|
-
#
|
|
7
|
-
# Rails 8.1 namespaced the class to Rails::CodeStatistics, removed
|
|
8
|
-
# the bare `require "code_statistics"` shim, and dropped the built-in
|
|
9
|
-
# `stats` rake task. We handle both old and new layouts.
|
|
10
|
-
Rails.application.config.after_initialize do
|
|
11
|
-
klass = nil
|
|
3
|
+
require_relative "../stats_formatter"
|
|
12
4
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
rescue LoadError
|
|
17
|
-
# fall through
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
unless klass
|
|
21
|
-
begin
|
|
22
|
-
require "code_statistics"
|
|
23
|
-
klass = CodeStatistics
|
|
24
|
-
rescue LoadError
|
|
25
|
-
# code_statistics not available (e.g., production without railties dev deps)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
5
|
+
# Rails 7.2 runs `rails stats` as a rake task that never boots the app, so the
|
|
6
|
+
# railtie initializer does not fire there: install from here as well.
|
|
7
|
+
Railbow::StatsFormatter.install!
|
|
28
8
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Rails 8.1 removed the built-in `stats` task. Re-define it so that
|
|
36
|
-
# `rails stats` / `rake stats` keeps working with Railbow formatting.
|
|
37
|
-
unless Rake::Task.task_defined?(:stats)
|
|
9
|
+
# Rails 8.1 dropped the `stats` rake task for the `rails stats` command; this
|
|
10
|
+
# keeps `rake stats` working. Gated on the version, not task_defined?: railtie
|
|
11
|
+
# tasks load before Rails' own, so on older Rails both would run.
|
|
12
|
+
if Rails.gem_version >= Gem::Version.new("8.1")
|
|
38
13
|
desc "Report code statistics (lines / LOC / classes / methods / M/C / LOC/M)"
|
|
39
14
|
task stats: :environment do
|
|
40
|
-
|
|
41
|
-
Rails
|
|
42
|
-
elsif defined?(CodeStatistics)
|
|
43
|
-
CodeStatistics
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
unless klass
|
|
47
|
-
abort "CodeStatistics is not available. Make sure railties is loaded."
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
stat_directories = [
|
|
51
|
-
["Controllers", "app/controllers"],
|
|
52
|
-
["Helpers", "app/helpers"],
|
|
53
|
-
["Jobs", "app/jobs"],
|
|
54
|
-
["Models", "app/models"],
|
|
55
|
-
["Mailers", "app/mailers"],
|
|
56
|
-
["Channels", "app/channels"],
|
|
57
|
-
["JavaScripts", "app/javascript"],
|
|
58
|
-
["Libraries", "lib"],
|
|
59
|
-
["APIs", "app/apis"],
|
|
60
|
-
["Controller tests", "test/controllers"],
|
|
61
|
-
["Helper tests", "test/helpers"],
|
|
62
|
-
["Job tests", "test/jobs"],
|
|
63
|
-
["Model tests", "test/models"],
|
|
64
|
-
["Mailer tests", "test/mailers"],
|
|
65
|
-
["Channel tests", "test/channels"],
|
|
66
|
-
["Integration tests", "test/integration"],
|
|
67
|
-
["System tests", "test/system"],
|
|
68
|
-
["Controller specs", "spec/controllers"],
|
|
69
|
-
["Helper specs", "spec/helpers"],
|
|
70
|
-
["Job specs", "spec/jobs"],
|
|
71
|
-
["Model specs", "spec/models"],
|
|
72
|
-
["Mailer specs", "spec/mailers"],
|
|
73
|
-
["Channel specs", "spec/channels"],
|
|
74
|
-
["Request specs", "spec/requests"],
|
|
75
|
-
["System specs", "spec/system"]
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
root = Rails.root.to_s
|
|
79
|
-
pairs = stat_directories
|
|
80
|
-
.map { |name, dir| [name, File.join(root, dir)] }
|
|
15
|
+
stat_directories = Rails::CodeStatistics.directories
|
|
16
|
+
.map { |name, dir| [name, Rails.root.join(dir)] }
|
|
81
17
|
.select { |_, dir| File.directory?(dir) }
|
|
82
18
|
|
|
83
|
-
|
|
19
|
+
Rails::CodeStatistics.new(*stat_directories).to_s
|
|
84
20
|
end
|
|
85
21
|
end
|
data/lib/railbow/version.rb
CHANGED