railbow 0.1.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 +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE.txt +21 -0
- data/README.md +243 -0
- data/exe/railbow +110 -0
- data/lib/railbow/about_formatter.rb +37 -0
- data/lib/railbow/color_assigner.rb +53 -0
- data/lib/railbow/config.rb +113 -0
- data/lib/railbow/demo/fixtures.rb +121 -0
- data/lib/railbow/demo/migrate_demo.rb +35 -0
- data/lib/railbow/demo/routes_demo.rb +94 -0
- data/lib/railbow/demo/runner.rb +51 -0
- data/lib/railbow/demo/status_demo.rb +164 -0
- data/lib/railbow/formatters/base.rb +195 -0
- data/lib/railbow/git_utils.rb +29 -0
- data/lib/railbow/init.rb +112 -0
- data/lib/railbow/logo.rb +27 -0
- data/lib/railbow/migration_formatter.rb +64 -0
- data/lib/railbow/migration_parser.rb +87 -0
- data/lib/railbow/name_collision_resolver.rb +155 -0
- data/lib/railbow/name_formatter.rb +82 -0
- data/lib/railbow/notes_formatter.rb +311 -0
- data/lib/railbow/params.rb +229 -0
- data/lib/railbow/railtie.rb +44 -0
- data/lib/railbow/routes_formatter.rb +163 -0
- data/lib/railbow/stats_formatter.rb +99 -0
- data/lib/railbow/table/column.rb +24 -0
- data/lib/railbow/table/renderer.rb +441 -0
- data/lib/railbow/table/theme.rb +54 -0
- data/lib/railbow/table.rb +5 -0
- data/lib/railbow/tasks/init.rake +10 -0
- data/lib/railbow/tasks/migrate_status.rake +694 -0
- data/lib/railbow/tasks/stats.rake +85 -0
- data/lib/railbow/text_utils.rb +30 -0
- data/lib/railbow/version.rb +5 -0
- data/lib/railbow.rb +22 -0
- data/sig/railbow.rbs +4 -0
- metadata +137 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "date"
|
|
5
|
+
require_relative "git_utils"
|
|
6
|
+
require_relative "formatters/base"
|
|
7
|
+
require_relative "logo"
|
|
8
|
+
require_relative "color_assigner"
|
|
9
|
+
|
|
10
|
+
module Railbow
|
|
11
|
+
module NotesFormatter
|
|
12
|
+
RESET = Formatters::Base::RESET
|
|
13
|
+
BOLD = Formatters::Base::BOLD
|
|
14
|
+
DIM = Formatters::Base::DIM
|
|
15
|
+
RED = Formatters::Base::RED
|
|
16
|
+
YELLOW = Formatters::Base::YELLOW
|
|
17
|
+
CYAN = Formatters::Base::CYAN
|
|
18
|
+
GREEN = Formatters::Base::GREEN
|
|
19
|
+
WHITE = Formatters::Base::BRIGHT_WHITE
|
|
20
|
+
|
|
21
|
+
BAR = "\u258e" # ▎
|
|
22
|
+
|
|
23
|
+
TAG_COLORS = {
|
|
24
|
+
"TODO" => YELLOW,
|
|
25
|
+
"FIXME" => RED,
|
|
26
|
+
"OPTIMIZE" => CYAN,
|
|
27
|
+
"HACK" => RED,
|
|
28
|
+
"NOTE" => GREEN
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
PALETTE = Formatters::Base::TABLE_PALETTE
|
|
32
|
+
|
|
33
|
+
def display(results, options = {})
|
|
34
|
+
return super if Railbow.plain?
|
|
35
|
+
|
|
36
|
+
if Railbow::Params.help?
|
|
37
|
+
print_help
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
author_mode = Railbow::Params.git_author
|
|
42
|
+
since_val = Railbow::Params.since
|
|
43
|
+
sort_mode = Railbow::Params.sort
|
|
44
|
+
|
|
45
|
+
since_date = Railbow::Params.parse_since(since_val, context: "annotations")
|
|
46
|
+
git_needed = %w[all me].include?(author_mode) || since_date || sort_mode == "date"
|
|
47
|
+
|
|
48
|
+
# Build blame cache for all files if git features are needed
|
|
49
|
+
blame_cache = {}
|
|
50
|
+
if git_needed
|
|
51
|
+
results.each_key do |source_file|
|
|
52
|
+
blame_cache[source_file] ||= blame_file(source_file)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Flatten annotations into a list for filtering/sorting
|
|
57
|
+
entries = []
|
|
58
|
+
results.each do |source_file, annotations|
|
|
59
|
+
annotations.each do |annotation|
|
|
60
|
+
blame_info = blame_cache.dig(source_file, annotation.line)
|
|
61
|
+
entries << {
|
|
62
|
+
file: source_file,
|
|
63
|
+
annotation: annotation,
|
|
64
|
+
blame: blame_info
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Filter by SINCE
|
|
70
|
+
skipped = 0
|
|
71
|
+
if since_date
|
|
72
|
+
before = entries.size
|
|
73
|
+
entries = entries.select do |entry|
|
|
74
|
+
entry[:blame] && entry[:blame][:date] && entry[:blame][:date] >= since_date
|
|
75
|
+
end
|
|
76
|
+
skipped = before - entries.size
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Sort by date (newest first) when requested
|
|
80
|
+
if sort_mode == "date"
|
|
81
|
+
entries.sort_by! do |entry|
|
|
82
|
+
d = entry.dig(:blame, :date)
|
|
83
|
+
d ? -d.to_time.to_i : 0
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Determine "me" identity
|
|
88
|
+
git_email = nil
|
|
89
|
+
if author_mode == "me"
|
|
90
|
+
git_email = current_git_email
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Determine max location width for alignment when showing metadata
|
|
94
|
+
show_date = git_needed
|
|
95
|
+
max_loc_width = 0
|
|
96
|
+
author_colors = nil
|
|
97
|
+
author_display = {}
|
|
98
|
+
if author_mode == "all"
|
|
99
|
+
author_names = entries.filter_map { |e| e.dig(:blame, :author) }.uniq
|
|
100
|
+
author_colors = ColorAssigner.new(author_names)
|
|
101
|
+
author_display = Railbow::Params.format_authors(author_names)
|
|
102
|
+
end
|
|
103
|
+
if show_date || author_mode == "all"
|
|
104
|
+
entries.each do |entry|
|
|
105
|
+
loc = "#{entry[:file]}:#{entry[:annotation].line}"
|
|
106
|
+
max_loc_width = loc.length if loc.length > max_loc_width
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Group entries back by file for bar coloring
|
|
111
|
+
file_counts = Hash.new(0)
|
|
112
|
+
entries.each { |e| file_counts[e[:file]] += 1 }
|
|
113
|
+
|
|
114
|
+
# Display
|
|
115
|
+
entries.each do |entry|
|
|
116
|
+
source_file = entry[:file]
|
|
117
|
+
annotation = entry[:annotation]
|
|
118
|
+
blame_info = entry[:blame]
|
|
119
|
+
|
|
120
|
+
is_mine = author_mode == "me" && git_email && blame_info &&
|
|
121
|
+
blame_info[:email] == git_email
|
|
122
|
+
|
|
123
|
+
bar_color = if file_counts[source_file] > 1
|
|
124
|
+
color_code = PALETTE[Zlib.crc32(source_file.to_s) % PALETTE.size]
|
|
125
|
+
"\e[38;5;#{color_code}m"
|
|
126
|
+
else
|
|
127
|
+
DIM
|
|
128
|
+
end
|
|
129
|
+
colored_bar = "#{bar_color}#{BAR}#{RESET}"
|
|
130
|
+
# When highlighting "me", use bright white bar
|
|
131
|
+
colored_bar = "#{WHITE}#{BAR}#{RESET}" if is_mine
|
|
132
|
+
|
|
133
|
+
tag = annotation.tag
|
|
134
|
+
tag_color = TAG_COLORS[tag] || DIM
|
|
135
|
+
location = "#{source_file}:#{annotation.line}"
|
|
136
|
+
colored_tag = "#{tag_color}#{BOLD}#{tag}#{RESET}"
|
|
137
|
+
note = annotation.text.to_s.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
138
|
+
|
|
139
|
+
# Build the file:line row with optional author+date
|
|
140
|
+
loc_line = " #{colored_bar} #{location}"
|
|
141
|
+
|
|
142
|
+
if author_mode == "all" && blame_info
|
|
143
|
+
raw_author = blame_info[:author] || ""
|
|
144
|
+
author_name = author_display[raw_author] || Railbow::Params.format_author(raw_author)
|
|
145
|
+
blame_date = blame_info[:date] ? blame_info[:date].strftime("%Y-%m-%d") : ""
|
|
146
|
+
padding = " " * [(max_loc_width - location.length + 2), 2].max
|
|
147
|
+
colored_author = "#{author_colors.color_for(raw_author)}#{author_name}#{RESET}"
|
|
148
|
+
meta = "#{colored_author} #{DIM}#{blame_date}#{RESET}"
|
|
149
|
+
loc_line = " #{colored_bar} #{location}#{padding}#{meta}"
|
|
150
|
+
elsif author_mode == "me" && is_mine && blame_info
|
|
151
|
+
blame_date = blame_info[:date] ? blame_info[:date].strftime("%Y-%m-%d") : ""
|
|
152
|
+
loc_line = " #{colored_bar} #{WHITE}#{location}#{RESET} #{DIM}#{blame_date}#{RESET}"
|
|
153
|
+
elsif author_mode != "all" && show_date && blame_info
|
|
154
|
+
blame_date = blame_info[:date] ? blame_info[:date].strftime("%Y-%m-%d") : ""
|
|
155
|
+
padding = " " * [(max_loc_width - location.length + 2), 2].max
|
|
156
|
+
loc_line = " #{colored_bar} #{location}#{padding}#{DIM}#{blame_date}#{RESET}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
puts loc_line
|
|
160
|
+
|
|
161
|
+
# Wrap note text so continuation lines keep the bar and align after TAG
|
|
162
|
+
prefix = " #{colored_bar} "
|
|
163
|
+
tag_str = "#{colored_tag} "
|
|
164
|
+
indent_width = 1 + 1 + 1 + 3 + tag.length + 1 # " " + BAR + " " + " " + TAG + " "
|
|
165
|
+
continuation_prefix = " #{colored_bar} #{" " * (tag.length + 1)}"
|
|
166
|
+
|
|
167
|
+
# Apply "me" highlighting to note text
|
|
168
|
+
if is_mine
|
|
169
|
+
tag_str = "#{WHITE}#{tag}#{RESET} "
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
term_w = begin
|
|
173
|
+
($stdout.tty? && $stdout.respond_to?(:winsize)) ? $stdout.winsize[1] : 120
|
|
174
|
+
rescue
|
|
175
|
+
120
|
|
176
|
+
end
|
|
177
|
+
max_note_width = [term_w - indent_width, 20].max
|
|
178
|
+
|
|
179
|
+
if note.length <= max_note_width
|
|
180
|
+
note_text = is_mine ? "#{WHITE}#{note}#{RESET}" : note
|
|
181
|
+
puts "#{prefix}#{tag_str}#{note_text}"
|
|
182
|
+
else
|
|
183
|
+
lines = word_wrap_simple(note, max_note_width)
|
|
184
|
+
first_line = is_mine ? "#{WHITE}#{lines.first}#{RESET}" : lines.first
|
|
185
|
+
puts "#{prefix}#{tag_str}#{first_line}"
|
|
186
|
+
lines[1..].each do |line|
|
|
187
|
+
line_text = is_mine ? "#{WHITE}#{line}#{RESET}" : line
|
|
188
|
+
puts "#{continuation_prefix}#{line_text}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
if skipped > 0
|
|
194
|
+
puts
|
|
195
|
+
puts " #{DIM}#{skipped} annotation(s) older than #{since_val} hidden#{RESET}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
puts
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def blame_file(path)
|
|
204
|
+
return {} unless File.exist?(path)
|
|
205
|
+
|
|
206
|
+
output, status = Railbow::GitUtils.capture2("blame", "--porcelain", path)
|
|
207
|
+
return {} unless status.success?
|
|
208
|
+
|
|
209
|
+
result = {}
|
|
210
|
+
current_line = nil
|
|
211
|
+
current_author = nil
|
|
212
|
+
current_email = nil
|
|
213
|
+
current_time = nil
|
|
214
|
+
|
|
215
|
+
output.each_line do |line|
|
|
216
|
+
line = line.chomp
|
|
217
|
+
if line.match?(/\A[0-9a-f]{40}\s/)
|
|
218
|
+
parts = line.split
|
|
219
|
+
current_line = parts[2]&.to_i
|
|
220
|
+
current_author = nil
|
|
221
|
+
current_email = nil
|
|
222
|
+
current_time = nil
|
|
223
|
+
elsif line.start_with?("author ")
|
|
224
|
+
current_author = line.sub("author ", "")
|
|
225
|
+
elsif line.start_with?("author-mail ")
|
|
226
|
+
current_email = line.sub("author-mail ", "").delete("<>").strip.downcase
|
|
227
|
+
elsif line.start_with?("author-time ")
|
|
228
|
+
current_time = line.sub("author-time ", "").to_i
|
|
229
|
+
elsif line.start_with?("\t") && current_line
|
|
230
|
+
# End of this blame entry — store it
|
|
231
|
+
date = current_time ? Time.at(current_time).to_date : nil
|
|
232
|
+
result[current_line] = {
|
|
233
|
+
author: current_author,
|
|
234
|
+
email: current_email,
|
|
235
|
+
date: date
|
|
236
|
+
}
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
result
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def current_git_email
|
|
244
|
+
output, _status = Railbow::GitUtils.capture2("config", "user.email")
|
|
245
|
+
output.strip.downcase
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def print_help
|
|
249
|
+
Railbow.print_logo
|
|
250
|
+
puts <<~HELP
|
|
251
|
+
|
|
252
|
+
Enhanced rails notes
|
|
253
|
+
|
|
254
|
+
\e[1mUsage:\e[0m
|
|
255
|
+
[RBW_*=value ...] rails notes
|
|
256
|
+
|
|
257
|
+
\e[1mOptions:\e[0m
|
|
258
|
+
RBW_GIT=<options> Git integration (comma-separated):
|
|
259
|
+
author — show all authors (same as author:all)
|
|
260
|
+
author:all — show author + date on each annotation
|
|
261
|
+
author:me — highlight your own annotations
|
|
262
|
+
|
|
263
|
+
RBW_SINCE=<period> Filter annotations by blame date (default: all)
|
|
264
|
+
Values: all, 2mo, 1w, 30d, 1y, etc.
|
|
265
|
+
Units: d (days), w (weeks), mo/m (months), y (years)
|
|
266
|
+
|
|
267
|
+
RBW_SORT=<mode> Sort order (default: file)
|
|
268
|
+
file — group by file (default Rails order)
|
|
269
|
+
date — sort by blame date (newest first)
|
|
270
|
+
|
|
271
|
+
RBW_PLAIN=1 Disable Railbow formatting (plain Rails output)
|
|
272
|
+
|
|
273
|
+
RBW_HELP=1 Show this help message
|
|
274
|
+
|
|
275
|
+
\e[2mAuto-disabled when piped, in CI, or when called by an LLM agent.\e[0m
|
|
276
|
+
|
|
277
|
+
\e[1mExamples:\e[0m
|
|
278
|
+
rails notes
|
|
279
|
+
RBW_GIT=author rails notes
|
|
280
|
+
RBW_GIT=author:me rails notes
|
|
281
|
+
RBW_SINCE=1mo RBW_GIT=author rails notes
|
|
282
|
+
RBW_SORT=date RBW_GIT=author rails notes
|
|
283
|
+
RBW_SINCE=2w RBW_GIT=author:me RBW_SORT=date rails notes
|
|
284
|
+
|
|
285
|
+
HELP
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def word_wrap_simple(text, max_width)
|
|
289
|
+
lines = []
|
|
290
|
+
current = +""
|
|
291
|
+
current_width = 0
|
|
292
|
+
|
|
293
|
+
text.split(/(\s+)/).each do |token|
|
|
294
|
+
if current_width + token.length <= max_width
|
|
295
|
+
current << token
|
|
296
|
+
current_width += token.length
|
|
297
|
+
elsif current_width.zero?
|
|
298
|
+
lines << token
|
|
299
|
+
else
|
|
300
|
+
lines << current.rstrip
|
|
301
|
+
token = token.lstrip
|
|
302
|
+
current = +token
|
|
303
|
+
current_width = token.length
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
lines << current.rstrip unless current.strip.empty?
|
|
308
|
+
lines
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require_relative "config"
|
|
5
|
+
require_relative "name_formatter"
|
|
6
|
+
require_relative "name_collision_resolver"
|
|
7
|
+
|
|
8
|
+
module Railbow
|
|
9
|
+
module Params
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Parse compound ENV value: "author:me,diff,base:develop"
|
|
13
|
+
# → { "author" => "me", "diff" => true, "base" => "develop" }
|
|
14
|
+
# Repeated keys collect into arrays: "hide:date,hide:author"
|
|
15
|
+
# → { "hide" => ["date", "author"] }
|
|
16
|
+
def parse_compound(value)
|
|
17
|
+
return {} if value.nil? || value.strip.empty?
|
|
18
|
+
|
|
19
|
+
result = {}
|
|
20
|
+
value.strip.split(",").each do |token|
|
|
21
|
+
token = token.strip
|
|
22
|
+
next if token.empty?
|
|
23
|
+
|
|
24
|
+
key, val = token.split(":", 2)
|
|
25
|
+
key = key.strip
|
|
26
|
+
val = val ? val.strip : true
|
|
27
|
+
|
|
28
|
+
result[key] = if result.key?(key)
|
|
29
|
+
Array(result[key]) << val
|
|
30
|
+
else
|
|
31
|
+
val
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def truthy?(value)
|
|
38
|
+
%w[1 true yes on].include?(value.to_s.strip.downcase)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Parse SINCE value like "2mo", "30d", "1w", "1y" into a Date cutoff.
|
|
42
|
+
# Returns nil for "all" or unrecognized values.
|
|
43
|
+
def parse_since(value, context: nil)
|
|
44
|
+
return nil if value.nil? || value.strip.downcase == "all"
|
|
45
|
+
|
|
46
|
+
match = value.strip.match(/^(\d+)(d|w|mo|m|y)$/i)
|
|
47
|
+
unless match
|
|
48
|
+
label = context ? " for #{context}" : ""
|
|
49
|
+
warn " Warning: unrecognized RBW_SINCE=#{value}#{label}, showing all"
|
|
50
|
+
return nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
amount = match[1].to_i
|
|
54
|
+
unit = match[2].downcase
|
|
55
|
+
|
|
56
|
+
case unit
|
|
57
|
+
when "d" then Date.today - amount
|
|
58
|
+
when "w" then Date.today - (amount * 7)
|
|
59
|
+
when "mo", "m" then Date.today.prev_month(amount)
|
|
60
|
+
when "y" then Date.today.prev_year(amount)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# --- Global accessors ---
|
|
65
|
+
|
|
66
|
+
def help?
|
|
67
|
+
truthy?(ENV["RBW_HELP"])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def plain?
|
|
71
|
+
truthy?(ENV["RBW_PLAIN"])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def since
|
|
75
|
+
(ENV["RBW_SINCE"] || Config.load["since"] || "all").strip.downcase
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def sort
|
|
79
|
+
(ENV["RBW_SORT"] || Config.load["sort"] || "file").strip.downcase
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# --- Compound: RBW_COMPACT ---
|
|
83
|
+
|
|
84
|
+
def compact
|
|
85
|
+
parse_compound(ENV["RBW_COMPACT"] || Config.load["compact"])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def compact_oneline?
|
|
89
|
+
compact["oneline"] == true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def compact_strip_format?
|
|
93
|
+
compact["strip-format"] == true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def compact_dense?
|
|
97
|
+
compact["dense"] == true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def compact_noheader?
|
|
101
|
+
compact["noheader"] == true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def compact_maxw
|
|
105
|
+
val = compact["maxw"]
|
|
106
|
+
val.is_a?(String) ? val.to_i : nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def compact_hidden_columns
|
|
110
|
+
val = compact["hide"]
|
|
111
|
+
return [] if val.nil?
|
|
112
|
+
Array(val)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def compact_options
|
|
116
|
+
c = compact
|
|
117
|
+
maxw_val = c["maxw"]
|
|
118
|
+
hide_val = c["hide"]
|
|
119
|
+
{
|
|
120
|
+
oneline: c["oneline"] == true,
|
|
121
|
+
dense: c["dense"] == true,
|
|
122
|
+
noheader: c["noheader"] == true,
|
|
123
|
+
maxw: maxw_val.is_a?(String) ? maxw_val.to_i : nil,
|
|
124
|
+
hidden_columns: hide_val.nil? ? [] : Array(hide_val)
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def verb
|
|
129
|
+
ENV["RBW_VERB"] || Config.load["verb"]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# --- Compound: RBW_GIT ---
|
|
133
|
+
|
|
134
|
+
def git
|
|
135
|
+
parse_compound(ENV["RBW_GIT"] || Config.load["git"])
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def git_author
|
|
139
|
+
val = git["author"]
|
|
140
|
+
return "off" if val.nil?
|
|
141
|
+
(val == true) ? "all" : val.strip.downcase
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def author_format
|
|
145
|
+
ENV["RBW_AUTHOR_FORMAT"] || Config.load["author_format"] || "short"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Format an author name using NameFormatter.
|
|
149
|
+
# Accepts preset names ("initials", "first_name", etc.) or
|
|
150
|
+
# custom patterns ("FF L", "FFFF L.", "LLLL, FFFF").
|
|
151
|
+
def format_author(name)
|
|
152
|
+
NameFormatter.format(name, author_format)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Format a batch of author names with collision resolution.
|
|
156
|
+
# Returns Hash{String => String} mapping raw names to disambiguated formatted names.
|
|
157
|
+
def format_authors(names)
|
|
158
|
+
NameCollisionResolver.resolve(names, author_format)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def git_diff?
|
|
162
|
+
git["diff"] == true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def git_base
|
|
166
|
+
val = git["base"]
|
|
167
|
+
val.is_a?(String) ? val : ""
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def git_mask
|
|
171
|
+
val = git["mask"]
|
|
172
|
+
val.is_a?(String) ? val : ""
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Built-in regex for extracting ticket/task identifiers from branch names.
|
|
176
|
+
# Matches patterns like: ws-123, 123, abc123, ab-123-456, feat/ws-1234, feat/123-some-feature
|
|
177
|
+
TICKET_RE = /(?:^|\/)([a-z]{1,5}-?\d+(?:-\d+)*|\d+(?:-\d+)*)/i
|
|
178
|
+
|
|
179
|
+
# Extract a ticket identifier from a branch name.
|
|
180
|
+
# Returns the matched identifier or the original branch name if no match.
|
|
181
|
+
def extract_branch_ticket(branch)
|
|
182
|
+
m = branch.match(TICKET_RE)
|
|
183
|
+
m ? m[1] : branch
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# --- RBW_DATE ---
|
|
187
|
+
|
|
188
|
+
def date_format
|
|
189
|
+
val = ENV["RBW_DATE"] || Config.load["date"]
|
|
190
|
+
return "full" if val.nil? || val.to_s.strip.empty?
|
|
191
|
+
|
|
192
|
+
val.to_s.strip
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# --- Compound: RBW_VIEW ---
|
|
196
|
+
|
|
197
|
+
def view
|
|
198
|
+
parse_compound(ENV["RBW_VIEW"] || Config.load["view"])
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def view_calendar?
|
|
202
|
+
view["calendar"] == true
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def view_tables?
|
|
206
|
+
val = view["tables"]
|
|
207
|
+
if val == "nowrap"
|
|
208
|
+
warn " Warning: RBW_VIEW=tables:nowrap is deprecated. Use RBW_COMPACT=oneline instead."
|
|
209
|
+
end
|
|
210
|
+
!val.nil?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# --- Compound: RBW_CALENDAR ---
|
|
214
|
+
|
|
215
|
+
def calendar
|
|
216
|
+
parse_compound(ENV["RBW_CALENDAR"] || Config.load["calendar"])
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def calendar_wticks?
|
|
220
|
+
return false unless view_calendar?
|
|
221
|
+
|
|
222
|
+
calendar["wticks"] == true
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def calendar_label
|
|
226
|
+
calendar["label"] || "%b %Y W%V"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module Railbow
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
# Initialize after ActiveRecord is loaded
|
|
8
|
+
initializer "railbow.enhance_migrations" do
|
|
9
|
+
ActiveSupport.on_load(:active_record) do
|
|
10
|
+
require_relative "migration_formatter"
|
|
11
|
+
|
|
12
|
+
# Prepend our formatter module to ActiveRecord::Migration
|
|
13
|
+
ActiveRecord::Migration.prepend(Railbow::MigrationFormatter)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Enhance routes output with colors
|
|
18
|
+
initializer "railbow.enhance_routes", after: :load_config_initializers do
|
|
19
|
+
require "action_dispatch/routing/inspector"
|
|
20
|
+
require_relative "routes_formatter"
|
|
21
|
+
ActionDispatch::Routing::ConsoleFormatter::Sheet.prepend(Railbow::RoutesFormatter)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Enhance `rails about` output
|
|
25
|
+
initializer "railbow.enhance_about", after: :load_config_initializers do
|
|
26
|
+
require_relative "about_formatter"
|
|
27
|
+
Rails::Info.singleton_class.prepend(Railbow::AboutFormatter)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Enhance `rails notes` output
|
|
31
|
+
initializer "railbow.enhance_notes", after: :load_config_initializers do
|
|
32
|
+
require "rails/source_annotation_extractor"
|
|
33
|
+
require_relative "notes_formatter"
|
|
34
|
+
Rails::SourceAnnotationExtractor.prepend(Railbow::NotesFormatter)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Load custom Rake tasks
|
|
38
|
+
rake_tasks do
|
|
39
|
+
load "railbow/tasks/migrate_status.rake"
|
|
40
|
+
load "railbow/tasks/stats.rake"
|
|
41
|
+
load "railbow/tasks/init.rake"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|