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
data/lib/railbow/params.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "date"
|
|
4
4
|
require_relative "calendar"
|
|
5
|
+
require_relative "clock"
|
|
5
6
|
require_relative "config"
|
|
6
7
|
require_relative "name_formatter"
|
|
7
8
|
require_relative "name_collision_resolver"
|
|
@@ -10,10 +11,11 @@ module Railbow
|
|
|
10
11
|
module Params
|
|
11
12
|
module_function
|
|
12
13
|
|
|
13
|
-
# Parse compound ENV value
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Parse a compound ENV value; repeated keys collect into arrays.
|
|
15
|
+
# parse_compound("author:me,diff,base:develop")
|
|
16
|
+
# # => { "author" => "me", "diff" => true, "base" => "develop" }
|
|
17
|
+
# parse_compound("hide:date,hide:author")
|
|
18
|
+
# # => { "hide" => ["date", "author"] }
|
|
17
19
|
def parse_compound(value)
|
|
18
20
|
return {} if value.nil? || value.strip.empty?
|
|
19
21
|
|
|
@@ -54,11 +56,12 @@ module Railbow
|
|
|
54
56
|
amount = match[1].to_i
|
|
55
57
|
unit = match[2].downcase
|
|
56
58
|
|
|
59
|
+
today = Clock.today
|
|
57
60
|
case unit
|
|
58
|
-
when "d" then
|
|
59
|
-
when "w" then
|
|
60
|
-
when "mo", "m" then
|
|
61
|
-
when "y" then
|
|
61
|
+
when "d" then today - amount
|
|
62
|
+
when "w" then today - (amount * 7)
|
|
63
|
+
when "mo", "m" then today.prev_month(amount)
|
|
64
|
+
when "y" then today.prev_year(amount)
|
|
62
65
|
end
|
|
63
66
|
end
|
|
64
67
|
|
|
@@ -80,12 +83,9 @@ module Railbow
|
|
|
80
83
|
(ENV["RBW_SINCE"] || Config.load["since"] || "all").strip.downcase
|
|
81
84
|
end
|
|
82
85
|
|
|
83
|
-
# Floor on how many migrations survive the SINCE window
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# Deliberately a knob of its own rather than a token inside RBW_SINCE, so
|
|
88
|
-
# an ad-hoc `RBW_SINCE=2mo` keeps the floor instead of silently dropping it.
|
|
86
|
+
# Floor on how many migrations survive the SINCE window: when it leaves
|
|
87
|
+
# fewer rows, the oldest are pulled back in. 0 disables the floor. A knob of
|
|
88
|
+
# its own, not a RBW_SINCE token, so an ad-hoc `RBW_SINCE=2mo` keeps it.
|
|
89
89
|
def since_min
|
|
90
90
|
value = ENV["RBW_SINCE_MIN"] || Config.load["since_min"] || 0
|
|
91
91
|
[value.to_i, 0].max
|
|
@@ -161,15 +161,14 @@ module Railbow
|
|
|
161
161
|
ENV["RBW_AUTHOR_FORMAT"] || Config.load["author_format"] || "short"
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
# custom patterns ("FF L", "FFFF L.", "LLLL, FFFF").
|
|
164
|
+
# author_format is a preset ("initials", "first_name", ...) or a custom
|
|
165
|
+
# pattern ("FF L", "FFFF L.", "LLLL, FFFF").
|
|
167
166
|
def format_author(name)
|
|
168
167
|
NameFormatter.format(name, author_format)
|
|
169
168
|
end
|
|
170
169
|
|
|
171
|
-
#
|
|
172
|
-
# Returns Hash{String => String}
|
|
170
|
+
# Formats a batch of names so two different authors never render the same.
|
|
171
|
+
# Returns Hash{String => String} of raw name to formatted name.
|
|
173
172
|
def format_authors(names)
|
|
174
173
|
NameCollisionResolver.resolve(names, author_format)
|
|
175
174
|
end
|
|
@@ -188,12 +187,11 @@ module Railbow
|
|
|
188
187
|
val.is_a?(String) ? val : ""
|
|
189
188
|
end
|
|
190
189
|
|
|
191
|
-
#
|
|
192
|
-
#
|
|
190
|
+
# Ticket ids in branch names: ps-123, 123, abc123, ab-123-456, feat/ps-1234,
|
|
191
|
+
# feat/123-some-feature.
|
|
193
192
|
TICKET_RE = /(?:^|\/)([a-z]{1,5}-?\d+(?:-\d+)*|\d+(?:-\d+)*)/i
|
|
194
193
|
|
|
195
|
-
#
|
|
196
|
-
# Returns the matched identifier or the original branch name if no match.
|
|
194
|
+
# The branch's ticket id, or the whole branch name when it has none.
|
|
197
195
|
def extract_branch_ticket(branch)
|
|
198
196
|
m = branch.match(TICKET_RE)
|
|
199
197
|
m ? m[1] : branch
|
|
@@ -208,6 +206,15 @@ module Railbow
|
|
|
208
206
|
val.to_s.strip
|
|
209
207
|
end
|
|
210
208
|
|
|
209
|
+
# --- RBW_TZ ---
|
|
210
|
+
|
|
211
|
+
def tz
|
|
212
|
+
val = ENV["RBW_TZ"] || Config.load["tz"]
|
|
213
|
+
return "local" if val.nil? || val.to_s.strip.empty?
|
|
214
|
+
|
|
215
|
+
val.to_s.strip
|
|
216
|
+
end
|
|
217
|
+
|
|
211
218
|
# --- Compound: RBW_VIEW ---
|
|
212
219
|
|
|
213
220
|
def view
|
|
@@ -244,10 +251,9 @@ module Railbow
|
|
|
244
251
|
db["full"] == true
|
|
245
252
|
end
|
|
246
253
|
|
|
247
|
-
# Expand only the first database, summarizing the rest in a line each
|
|
248
|
-
#
|
|
249
|
-
#
|
|
250
|
-
# on. RBW_DB=full overrides it, and it does not apply to inline runs.
|
|
254
|
+
# Expand only the first database, summarizing the rest in a line each. A
|
|
255
|
+
# database with pending migrations is always drawn in full, since that is
|
|
256
|
+
# what you may need to act on. RBW_DB=full overrides it; inline ignores it.
|
|
251
257
|
def db_focus?
|
|
252
258
|
db["focus"] == true
|
|
253
259
|
end
|
data/lib/railbow/railtie.rb
CHANGED
|
@@ -4,37 +4,39 @@ require "rails/railtie"
|
|
|
4
4
|
|
|
5
5
|
module Railbow
|
|
6
6
|
class Railtie < Rails::Railtie
|
|
7
|
-
#
|
|
7
|
+
# on_load, so booting the app does not force ActiveRecord to load early.
|
|
8
8
|
initializer "railbow.enhance_migrations" do
|
|
9
9
|
ActiveSupport.on_load(:active_record) do
|
|
10
10
|
require_relative "migration_formatter"
|
|
11
11
|
|
|
12
|
-
# Prepend our formatter module to ActiveRecord::Migration
|
|
13
12
|
ActiveRecord::Migration.prepend(Railbow::MigrationFormatter)
|
|
14
13
|
end
|
|
15
14
|
end
|
|
16
15
|
|
|
17
|
-
# Enhance routes output with colors
|
|
18
16
|
initializer "railbow.enhance_routes", after: :load_config_initializers do
|
|
19
17
|
require "action_dispatch/routing/inspector"
|
|
20
18
|
require_relative "routes_formatter"
|
|
21
19
|
ActionDispatch::Routing::ConsoleFormatter::Sheet.prepend(Railbow::RoutesFormatter)
|
|
22
20
|
end
|
|
23
21
|
|
|
24
|
-
# Enhance `rails about` output
|
|
25
22
|
initializer "railbow.enhance_about", after: :load_config_initializers do
|
|
26
23
|
require_relative "about_formatter"
|
|
27
24
|
Rails::Info.singleton_class.prepend(Railbow::AboutFormatter)
|
|
28
25
|
end
|
|
29
26
|
|
|
30
|
-
# Enhance `rails notes` output
|
|
31
27
|
initializer "railbow.enhance_notes", after: :load_config_initializers do
|
|
32
28
|
require "rails/source_annotation_extractor"
|
|
33
29
|
require_relative "notes_formatter"
|
|
34
30
|
Rails::SourceAnnotationExtractor.prepend(Railbow::NotesFormatter)
|
|
35
31
|
end
|
|
36
32
|
|
|
37
|
-
#
|
|
33
|
+
# Since Rails 8.0 `rails stats` is a Rails command that boots the app but
|
|
34
|
+
# never loads rake tasks, so the .rake file alone cannot install this.
|
|
35
|
+
initializer "railbow.enhance_stats", after: :load_config_initializers do
|
|
36
|
+
require_relative "stats_formatter"
|
|
37
|
+
Railbow::StatsFormatter.install!
|
|
38
|
+
end
|
|
39
|
+
|
|
38
40
|
rake_tasks do
|
|
39
41
|
load "railbow/tasks/migrate_status.rake"
|
|
40
42
|
load "railbow/tasks/stats.rake"
|
|
@@ -62,12 +62,22 @@ module Railbow
|
|
|
62
62
|
return
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
# A failure drops whatever this section already buffered, then renders
|
|
66
|
+
# it the Rails way, header included since railbow normally hides it.
|
|
67
|
+
mark = @buffer.size
|
|
68
|
+
plain = lambda do
|
|
69
|
+
@buffer.slice!(mark..)
|
|
70
|
+
header(routes)
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
Railbow.guard(plain) do
|
|
74
|
+
strip_format = Railbow::Params.compact_strip_format?
|
|
75
|
+
prepared = routes.map { |r| prepare_route(r, strip_format) }
|
|
76
|
+
prepared = filter_by_verb(prepared)
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
groups = group_routes(prepared)
|
|
79
|
+
groups.each { |label, group| render_group(label, group) }
|
|
80
|
+
end
|
|
71
81
|
end
|
|
72
82
|
|
|
73
83
|
private
|
|
@@ -97,7 +107,9 @@ module Railbow
|
|
|
97
107
|
Table::Column.new(label: "Verb", sticky: true),
|
|
98
108
|
Table::Column.new(label: "URI Pattern", sticky: true),
|
|
99
109
|
Table::Column.new(label: "Controller#Action"),
|
|
100
|
-
|
|
110
|
+
# The route name is the least of the four, so a group too wide for the
|
|
111
|
+
# terminal gives it up rather than wrapping past the edge.
|
|
112
|
+
Table::Column.new(label: "Prefix", droppable: 1)
|
|
101
113
|
]
|
|
102
114
|
|
|
103
115
|
rows = routes.map { |r|
|
|
@@ -6,94 +6,164 @@ require_relative "table"
|
|
|
6
6
|
|
|
7
7
|
module Railbow
|
|
8
8
|
module StatsFormatter
|
|
9
|
+
RESET = Formatters::Base::RESET
|
|
10
|
+
DIM = Formatters::Base::DIM
|
|
11
|
+
CODE_COLOR = "\e[38;5;76m" # green
|
|
12
|
+
TEST_COLOR = "\e[38;5;39m" # cyan
|
|
13
|
+
|
|
14
|
+
BAR_WIDTH = 16
|
|
15
|
+
BAR_EIGHTHS = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"].freeze
|
|
16
|
+
FULL_BLOCK = "█"
|
|
17
|
+
|
|
18
|
+
# Rails 8 names the class Rails::CodeStatistics, Rails 7.2 a top-level
|
|
19
|
+
# CodeStatistics; both live in "rails/code_statistics". Prepending twice is
|
|
20
|
+
# a no-op, so every entry point may call this.
|
|
21
|
+
def self.install!
|
|
22
|
+
require "rails/code_statistics"
|
|
23
|
+
klass = defined?(Rails::CodeStatistics) ? Rails::CodeStatistics : ::CodeStatistics
|
|
24
|
+
klass.prepend(self)
|
|
25
|
+
rescue LoadError
|
|
26
|
+
# railties without code statistics: nothing to decorate
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Stock Rails prints the table from to_s and returns nil, and both the
|
|
30
|
+
# stats command and the rake task rely on that, so this prints too.
|
|
9
31
|
def to_s
|
|
10
32
|
return super if Railbow.plain?
|
|
11
33
|
|
|
12
|
-
|
|
34
|
+
Railbow.guard(-> { super }) do
|
|
35
|
+
puts railbow_render
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def railbow_render
|
|
43
|
+
f = Formatters::Base.new
|
|
44
|
+
tests = railbow_test_types
|
|
45
|
+
names = @pairs.map(&:first)
|
|
46
|
+
code_names = names.reject { |n| tests.include?(n) }
|
|
47
|
+
test_names = names.select { |n| tests.include?(n) }
|
|
48
|
+
|
|
49
|
+
total = railbow_sum(@statistics.values)
|
|
50
|
+
max_loc = @statistics.values.map(&:code_lines).max.to_i
|
|
13
51
|
|
|
14
|
-
|
|
52
|
+
rows = []
|
|
53
|
+
separators = {}
|
|
54
|
+
[["Code", code_names, CODE_COLOR, false], ["Tests", test_names, TEST_COLOR, true]].each do |label, group, color, test|
|
|
55
|
+
next if group.empty?
|
|
56
|
+
|
|
57
|
+
# A heading row rather than a renderer separator, which would put the
|
|
58
|
+
# label past the first column.
|
|
59
|
+
rows << [Table::Themes::WALLS.format_separator.call(label), *Array.new(7, "")]
|
|
60
|
+
group.each do |name|
|
|
61
|
+
rows << railbow_row(f, name, @statistics[name], total, max_loc, color, test: test)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
separators[rows.size] = "" if rows.size > 1
|
|
65
|
+
rows << railbow_total_row(f, total)
|
|
66
|
+
|
|
67
|
+
renderer = Table::Renderer.new(
|
|
68
|
+
columns: railbow_columns,
|
|
69
|
+
theme: Table::Themes::WALLS,
|
|
70
|
+
compact: Railbow::Params.compact_options,
|
|
71
|
+
aliases: Railbow::Config.table_aliases
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
out = +"\n"
|
|
75
|
+
out << renderer.render(rows, separators: separators)
|
|
76
|
+
out << "\n\n"
|
|
77
|
+
out << railbow_summary(f, code_names, test_names)
|
|
78
|
+
out << "\n"
|
|
79
|
+
out
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def railbow_columns
|
|
83
|
+
[
|
|
15
84
|
Table::Column.new(label: "Name", sticky: true),
|
|
16
85
|
Table::Column.new(label: "Lines", align: :right),
|
|
17
86
|
Table::Column.new(label: "LOC", align: :right),
|
|
18
87
|
Table::Column.new(label: "Classes", align: :right),
|
|
19
88
|
Table::Column.new(label: "Methods", align: :right),
|
|
20
89
|
Table::Column.new(label: "M/C", align: :right),
|
|
21
|
-
Table::Column.new(label: "LOC/M", align: :right)
|
|
90
|
+
Table::Column.new(label: "LOC/M", align: :right),
|
|
91
|
+
Table::Column.new(label: "Share of LOC")
|
|
22
92
|
]
|
|
93
|
+
end
|
|
23
94
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
mc = (classes > 0) ? (methods.to_f / classes).round(0).to_i : 0
|
|
39
|
-
locm = (methods > 0) ? ((loc.to_f / methods) - 2).round(0).to_i : 0
|
|
40
|
-
|
|
41
|
-
total_lines += lines
|
|
42
|
-
total_loc += loc
|
|
43
|
-
total_classes += classes
|
|
44
|
-
total_methods += methods
|
|
45
|
-
|
|
46
|
-
if stats.test_file?
|
|
47
|
-
test_loc += loc
|
|
48
|
-
else
|
|
49
|
-
code_loc += loc
|
|
50
|
-
end
|
|
95
|
+
# M/C and LOC/M say little about test code, where most blocks are not
|
|
96
|
+
# methods, so on test rows they step back.
|
|
97
|
+
def railbow_row(f, name, stats, total, max_loc, color, test:)
|
|
98
|
+
ratios = railbow_ratios(stats).map { |v| test ? f.dim(railbow_num(v)) : railbow_count(f, v) }
|
|
99
|
+
[
|
|
100
|
+
name,
|
|
101
|
+
railbow_count(f, stats.lines),
|
|
102
|
+
railbow_count(f, stats.code_lines),
|
|
103
|
+
railbow_count(f, stats.classes),
|
|
104
|
+
railbow_count(f, stats.methods),
|
|
105
|
+
*ratios,
|
|
106
|
+
railbow_share(f, stats.code_lines, total.code_lines, max_loc, color)
|
|
107
|
+
]
|
|
108
|
+
end
|
|
51
109
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
classes.to_s,
|
|
57
|
-
methods.to_s,
|
|
58
|
-
mc.to_s,
|
|
59
|
-
locm.to_s
|
|
60
|
-
]
|
|
61
|
-
end
|
|
110
|
+
def railbow_total_row(f, total)
|
|
111
|
+
cells = [total.lines, total.code_lines, total.classes, total.methods, *railbow_ratios(total)]
|
|
112
|
+
[f.bold("Total"), *cells.map { |v| f.bold(railbow_num(v)) }, ""]
|
|
113
|
+
end
|
|
62
114
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
formatter.bold(total_lines.to_s),
|
|
70
|
-
formatter.bold(total_loc.to_s),
|
|
71
|
-
formatter.bold(total_classes.to_s),
|
|
72
|
-
formatter.bold(total_methods.to_s),
|
|
73
|
-
formatter.bold(total_mc.to_s),
|
|
74
|
-
formatter.bold(total_locm.to_s)
|
|
75
|
-
]
|
|
115
|
+
# Integer math, as stock Rails does it, so the numbers match its table.
|
|
116
|
+
def railbow_ratios(stats)
|
|
117
|
+
m_over_c = stats.classes.zero? ? 0 : stats.methods / stats.classes
|
|
118
|
+
loc_over_m = stats.methods.zero? ? 0 : (stats.code_lines / stats.methods) - 2
|
|
119
|
+
[m_over_c, loc_over_m]
|
|
120
|
+
end
|
|
76
121
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
122
|
+
# The bar is scaled to the biggest row so small directories stay visible;
|
|
123
|
+
# the percentage is of the whole codebase.
|
|
124
|
+
def railbow_share(f, loc, total_loc, max_loc, color)
|
|
125
|
+
return f.dim("-") if loc.zero? || total_loc.zero?
|
|
126
|
+
|
|
127
|
+
eighths = (BAR_WIDTH * 8 * loc.fdiv(max_loc)).round.clamp(1, BAR_WIDTH * 8)
|
|
128
|
+
bar = (FULL_BLOCK * (eighths / 8)) + BAR_EIGHTHS[eighths % 8]
|
|
129
|
+
share = 100.0 * loc / total_loc
|
|
130
|
+
pct = (share < 0.1) ? "<0.1%" : format("%.1f%%", share)
|
|
131
|
+
"#{color}#{bar.ljust(BAR_WIDTH)}#{RESET} #{f.dim(pct.rjust(5))}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def railbow_summary(f, code_names, test_names)
|
|
135
|
+
code = code_names.sum { |n| @statistics[n].code_lines }
|
|
136
|
+
test = test_names.sum { |n| @statistics[n].code_lines }
|
|
83
137
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
138
|
+
parts = ["#{CODE_COLOR}Code#{RESET} #{f.bold(railbow_num(code))} LOC"]
|
|
139
|
+
parts << "#{TEST_COLOR}Tests#{RESET} #{f.bold(railbow_num(test))} LOC"
|
|
140
|
+
parts << "Code to Test Ratio #{f.bold(format("1:%.1f", test.fdiv(code)))}" if code.positive?
|
|
141
|
+
" #{parts.join(f.dim(" · "))}\n"
|
|
142
|
+
end
|
|
87
143
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
144
|
+
def railbow_sum(calculators)
|
|
145
|
+
total = Struct.new(:lines, :code_lines, :classes, :methods).new(0, 0, 0, 0)
|
|
146
|
+
calculators.each do |s|
|
|
147
|
+
total.lines += s.lines
|
|
148
|
+
total.code_lines += s.code_lines
|
|
149
|
+
total.classes += s.classes
|
|
150
|
+
total.methods += s.methods
|
|
94
151
|
end
|
|
152
|
+
total
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Rails 8 keeps the list in a class attribute that register_directory
|
|
156
|
+
# extends; Rails 7.2 in a constant that rspec-rails appends to.
|
|
157
|
+
def railbow_test_types
|
|
158
|
+
respond_to?(:test_types, true) ? test_types : self.class::TEST_TYPES
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def railbow_count(f, value)
|
|
162
|
+
value.zero? ? f.dim("0") : railbow_num(value)
|
|
163
|
+
end
|
|
95
164
|
|
|
96
|
-
|
|
165
|
+
def railbow_num(value)
|
|
166
|
+
value.to_s.reverse.scan(/\d{1,3}/).join(",").reverse.then { |s| value.negative? ? "-#{s}" : s }
|
|
97
167
|
end
|
|
98
168
|
end
|
|
99
169
|
end
|
|
@@ -46,8 +46,8 @@ module Railbow
|
|
|
46
46
|
|
|
47
47
|
rows = {}
|
|
48
48
|
versions.each do |v|
|
|
49
|
-
# Absent from detect
|
|
50
|
-
#
|
|
49
|
+
# Absent from detect means suppressed (dismissed, or superseded with
|
|
50
|
+
# hide_superseded on): render as plain NO FILE, do not re-recover.
|
|
51
51
|
next unless (orphan = orphans[v])
|
|
52
52
|
|
|
53
53
|
row = if orphan.filename && !orphan.filename.empty?
|
|
@@ -73,9 +73,9 @@ module Railbow
|
|
|
73
73
|
)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
# Detect reads stored snapshots only
|
|
77
|
-
#
|
|
78
|
-
#
|
|
76
|
+
# Detect reads stored snapshots only, so a version listed without a
|
|
77
|
+
# filename falls back to live git/worktree recovery. That keeps fresh
|
|
78
|
+
# clones working with zero setup.
|
|
79
79
|
def from_live_recovery(version, with_content)
|
|
80
80
|
snapshot = begin
|
|
81
81
|
Mighost::API.find_or_recover_snapshot(version)
|
|
@@ -131,7 +131,6 @@ module Railbow
|
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
# The name a ghost row displays, derived from its recovered filename.
|
|
135
134
|
def display_name(ghost)
|
|
136
135
|
ghost.filename
|
|
137
136
|
.sub(/\A\d+_/, "") # strip version prefix
|
|
@@ -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
|