railbow 0.2.0 → 0.3.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 +27 -0
- data/README.md +6 -1
- data/lib/railbow/params.rb +4 -0
- data/lib/railbow/tasks/migrate_status.rake +120 -23
- data/lib/railbow/version.rb +1 -1
- data/lib/railbow.rb +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c42f0f6d8162662344a6ef8fb12388e8dd6a2a579384cab3829a85d806cb1571
|
|
4
|
+
data.tar.gz: 064fb908f8cd7567e9d8be53800e6972aa48faf65348aff7338c6088aa7427dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7af392359a26db27919f6bac5d31ce22d6736615bbbe8d514c794e5114a2a71ff61081160340a3177fd4b172207b8c07ae2948fcfdaeaccbc0498b28370fb1c
|
|
7
|
+
data.tar.gz: e5a9364c62c93fa530cbde3222c2249b3f251043514fda4d4cf51a33cfaf27ad54bffed5d5abae43befe065634faee0d96ec60581adc22004cd45790e7f87534
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-07-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Ghost rows in `db:migrate:status` now surface mighost 0.5's classification:
|
|
13
|
+
a superseded ghost (re-timestamped migration) shows a calmer 🪦 status with
|
|
14
|
+
a `≡ <version>` badge pointing at its successor, and a ghost deleted with no
|
|
15
|
+
surviving branch shows `✂ deleted in:<sha>`. Branch badges are unchanged and
|
|
16
|
+
take precedence right after supersession.
|
|
17
|
+
- `RBW_FORCE=1` forces Railbow formatting past every auto-detection
|
|
18
|
+
(`NO_COLOR`, CI, piped output, LLM agent detection). An explicit
|
|
19
|
+
`RBW_PLAIN=1` still wins.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Ghost data now comes from `Mighost::API.orphaned_migrations`, so
|
|
24
|
+
`db:migrate:status` honors mighost dismissals and `hide_superseded` for the
|
|
25
|
+
first time: a dismissed or hidden ghost renders as plain `NO FILE` instead
|
|
26
|
+
of 👻. Live git/worktree recovery still kicks in for versions without a
|
|
27
|
+
stored snapshot, so fresh clones keep working with zero setup.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- A ghost row without a branch no longer inherits the previous ghost row's
|
|
32
|
+
branch badge (a stale local leaked across loop iterations; unreachable
|
|
33
|
+
before mighost 0.5 made branchless ghosts the normal case).
|
|
34
|
+
|
|
8
35
|
## [0.2.0] - 2026-07-23
|
|
9
36
|
|
|
10
37
|
### Added
|
data/README.md
CHANGED
|
@@ -106,7 +106,7 @@ Out of the box you get:
|
|
|
106
106
|
- **Time filtering** - only the last 70 days shown by default (`since: 70d`)
|
|
107
107
|
- **Your migrations highlighted** - rows authored by you are visually distinct
|
|
108
108
|
|
|
109
|
-
If the [mighost](https://github.com/amberpixels/mighost) gem is installed (`gem "mighost", group: [:development, :test]`), migrations whose files were deleted (e.g. after switching branches) show up with a 👻 status and their recovered name
|
|
109
|
+
If the [mighost](https://github.com/amberpixels/mighost) gem is installed (`gem "mighost", group: [:development, :test]`), migrations whose files were deleted (e.g. after switching branches) show up with a 👻 status and their recovered name instead of a bare `********** NO FILE **********` row - Railbow talks to it through the stable `Mighost::API`. Each ghost row carries the most informative badge mighost can provide: `≡ <version>` when the migration was re-timestamped and lives on under another version (shown with a calmer 🪦 status), `⌥ <branch>` when a branch still holds the file, or `✂ deleted in:<sha>` pointing at the commit that removed it. Ghosts dismissed via `mighost:dismiss` (or hidden by `hide_superseded`) render as plain `NO FILE`.
|
|
110
110
|
|
|
111
111
|
### `rails db:migrate:down`
|
|
112
112
|
|
|
@@ -176,6 +176,7 @@ Every option can also be set via `RBW_*` environment variables, which override c
|
|
|
176
176
|
| Variable | Example | Description |
|
|
177
177
|
|---|---|---|
|
|
178
178
|
| `RBW_PLAIN` | `1` | Disable all formatting |
|
|
179
|
+
| `RBW_FORCE` | `1` | Force formatting even when piped, in CI, or run by an LLM agent (`RBW_PLAIN=1` still wins) |
|
|
179
180
|
| `RBW_SINCE` | `2mo`, `70d`, `1y`, `all` | Filter migrations by time period |
|
|
180
181
|
| `RBW_DATE` | `full`, `rel`, `short`, `custom(%b %d)` | Date display format |
|
|
181
182
|
| `RBW_GIT` | `author:me,diff,mask:auto` | Git integration options |
|
|
@@ -221,6 +222,10 @@ Formatting auto-disables when:
|
|
|
221
222
|
- Output is piped or redirected (non-TTY)
|
|
222
223
|
- Running inside an LLM agent (`CLAUDECODE` env var)
|
|
223
224
|
|
|
225
|
+
`RBW_FORCE=1` overrides all of the auto-detection above (useful for capturing
|
|
226
|
+
formatted output to a file, or letting an agent inspect the real rendering).
|
|
227
|
+
An explicit `RBW_PLAIN=1` always wins over `RBW_FORCE`.
|
|
228
|
+
|
|
224
229
|
## Requirements
|
|
225
230
|
|
|
226
231
|
- Ruby >= 3.1.0
|
data/lib/railbow/params.rb
CHANGED
|
@@ -12,8 +12,113 @@ require_relative "../logo"
|
|
|
12
12
|
# db:migrate:status and db:migrate:status:<database_name> tasks.
|
|
13
13
|
module Railbow
|
|
14
14
|
module MigrateStatusFormatter
|
|
15
|
+
# Ghost migration data normalized for rendering, whether it came from
|
|
16
|
+
# mighost's orphan classification or a live snapshot recovery.
|
|
17
|
+
class GhostRow
|
|
18
|
+
attr_reader :filename, :branch_name, :source, :superseded_by, :deleted_in_sha,
|
|
19
|
+
:author_name, :author_email, :content
|
|
20
|
+
|
|
21
|
+
def initialize(filename: nil, branch_name: nil, source: nil, superseded_by: nil,
|
|
22
|
+
deleted_in_sha: nil, author_name: nil, author_email: nil, content: nil)
|
|
23
|
+
@filename = filename
|
|
24
|
+
@branch_name = branch_name
|
|
25
|
+
@source = source
|
|
26
|
+
@superseded_by = superseded_by
|
|
27
|
+
@deleted_in_sha = deleted_in_sha
|
|
28
|
+
@author_name = author_name
|
|
29
|
+
@author_email = author_email
|
|
30
|
+
@content = content
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
15
34
|
private
|
|
16
35
|
|
|
36
|
+
def mighost_attr(obj, name)
|
|
37
|
+
obj.respond_to?(name) ? obj.public_send(name) : nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mighost_snapshot_content(version)
|
|
41
|
+
Mighost::API.find_snapshot(version)&.content
|
|
42
|
+
rescue
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def load_ghost_rows(versions, with_content: false)
|
|
47
|
+
# Detect once: OrphanedMigration carries the classification (supersession,
|
|
48
|
+
# deletion commit) that a bare snapshot doesn't, and already honors
|
|
49
|
+
# dismissals and hide_superseded.
|
|
50
|
+
orphans = begin
|
|
51
|
+
Mighost::API.orphaned_migrations.to_h { |o| [o.version.to_s, o] }
|
|
52
|
+
rescue
|
|
53
|
+
return {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
rows = {}
|
|
57
|
+
versions.each do |v|
|
|
58
|
+
# Absent from detect = deliberately suppressed (dismissed, or superseded
|
|
59
|
+
# with hide_superseded on) - render as plain NO FILE, don't re-recover.
|
|
60
|
+
next unless (orphan = orphans[v])
|
|
61
|
+
|
|
62
|
+
if orphan.filename && !orphan.filename.empty?
|
|
63
|
+
rows[v] = GhostRow.new(
|
|
64
|
+
filename: orphan.filename,
|
|
65
|
+
branch_name: orphan.branch_name,
|
|
66
|
+
source: mighost_attr(orphan, :source),
|
|
67
|
+
superseded_by: mighost_attr(orphan, :superseded_by),
|
|
68
|
+
deleted_in_sha: mighost_attr(orphan, :deleted_in_sha),
|
|
69
|
+
author_name: mighost_attr(orphan, :author_name),
|
|
70
|
+
author_email: mighost_attr(orphan, :author_email),
|
|
71
|
+
content: with_content ? mighost_snapshot_content(v) : nil
|
|
72
|
+
)
|
|
73
|
+
else
|
|
74
|
+
# Detect reads stored snapshots only. A version it lists without a
|
|
75
|
+
# filename has no snapshot yet, so fall back to live git/worktree
|
|
76
|
+
# recovery - keeps fresh clones working with zero setup.
|
|
77
|
+
snapshot = begin
|
|
78
|
+
Mighost::API.find_or_recover_snapshot(v)
|
|
79
|
+
rescue
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
next unless snapshot&.filename && !snapshot.filename.empty?
|
|
83
|
+
|
|
84
|
+
rows[v] = GhostRow.new(
|
|
85
|
+
filename: snapshot.filename,
|
|
86
|
+
branch_name: snapshot.branch_name,
|
|
87
|
+
source: mighost_attr(snapshot, :source),
|
|
88
|
+
superseded_by: api_superseded_by(v),
|
|
89
|
+
deleted_in_sha: mighost_attr(snapshot, :deleted_in_sha),
|
|
90
|
+
author_name: mighost_attr(snapshot, :author_name),
|
|
91
|
+
author_email: mighost_attr(snapshot, :author_email),
|
|
92
|
+
content: with_content ? snapshot.content : nil
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
rows
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def api_superseded_by(version)
|
|
100
|
+
return nil unless Mighost::API.respond_to?(:superseded_by)
|
|
101
|
+
|
|
102
|
+
Mighost::API.superseded_by(version)
|
|
103
|
+
rescue
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# One tag slot per ghost row; most informative wins.
|
|
108
|
+
def ghost_tag(ghost)
|
|
109
|
+
if ghost.superseded_by
|
|
110
|
+
"\e[38;5;245m≡ #{ghost.superseded_by}\e[38;5;217m"
|
|
111
|
+
elsif ghost.branch_name
|
|
112
|
+
if ghost.source == "worktree"
|
|
113
|
+
"\e[38;5;222m⌥ₜ#{ghost.branch_name}\e[38;5;217m"
|
|
114
|
+
else
|
|
115
|
+
"\e[38;5;222m⌥ #{ghost.branch_name}\e[38;5;217m"
|
|
116
|
+
end
|
|
117
|
+
elsif ghost.deleted_in_sha
|
|
118
|
+
"\e[38;5;245m✂ deleted in:#{ghost.deleted_in_sha[0, 8]}\e[38;5;217m"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
17
122
|
def git_migration_authors(migrate_dir)
|
|
18
123
|
output, _status = Railbow::GitUtils.capture2(
|
|
19
124
|
"log", "--format=COMMIT:%aN\t%aE", "--diff-filter=AR", "--name-status", "--", migrate_dir
|
|
@@ -304,6 +409,9 @@ module Railbow
|
|
|
304
409
|
|
|
305
410
|
RBW_PLAIN=1 Disable Railbow formatting (plain Rails output)
|
|
306
411
|
|
|
412
|
+
RBW_FORCE=1 Force Railbow formatting even when piped, in CI,
|
|
413
|
+
or called by an LLM agent (RBW_PLAIN=1 still wins)
|
|
414
|
+
|
|
307
415
|
RBW_HELP=1 Show this help message
|
|
308
416
|
|
|
309
417
|
\e[2mAuto-disabled when piped, in CI, or when called by an LLM agent.\e[0m
|
|
@@ -428,19 +536,12 @@ module Railbow
|
|
|
428
536
|
uncommitted_files.each { |f| branch_origins[f] ||= current_branch }
|
|
429
537
|
end
|
|
430
538
|
|
|
431
|
-
# Load mighost
|
|
539
|
+
# Load mighost ghost data for "NO FILE" migrations (if mighost gem is available)
|
|
432
540
|
mighost_snapshots = {}
|
|
433
541
|
mighost_available = defined?(Mighost::API) && Mighost.enabled?
|
|
434
542
|
if mighost_available
|
|
435
543
|
no_file_versions = db_list.select { |_, _, n| n.include?("NO FILE") }.map { |_, v, _| v.to_s }
|
|
436
|
-
no_file_versions
|
|
437
|
-
snapshot = begin
|
|
438
|
-
Mighost::API.find_or_recover_snapshot(v)
|
|
439
|
-
rescue
|
|
440
|
-
nil
|
|
441
|
-
end
|
|
442
|
-
mighost_snapshots[v] = snapshot if snapshot&.filename && !snapshot.filename.empty?
|
|
443
|
-
end
|
|
544
|
+
mighost_snapshots = load_ghost_rows(no_file_versions, with_content: tables_enabled) if no_file_versions.any?
|
|
444
545
|
end
|
|
445
546
|
|
|
446
547
|
# Build columns
|
|
@@ -501,29 +602,25 @@ module Railbow
|
|
|
501
602
|
ghost_snapshot = name.include?("NO FILE") ? mighost_snapshots[version.to_s] : nil
|
|
502
603
|
if name.include?("NO FILE") && ghost_snapshot
|
|
503
604
|
ghost_rows << idx
|
|
504
|
-
# Mighost recovered this ghost migration — show
|
|
505
|
-
|
|
605
|
+
# Mighost recovered this ghost migration — show ghost status + name + badge.
|
|
606
|
+
# A superseded ghost lives on under another version: stale bookkeeping,
|
|
607
|
+
# not a lost migration, so it gets a calmer glyph.
|
|
608
|
+
colored_status = ghost_snapshot.superseded_by ? "🪦" : "👻"
|
|
506
609
|
ghost_name = ghost_snapshot.filename
|
|
507
610
|
.sub(/\A\d+_/, "") # strip version prefix
|
|
508
611
|
.sub(/\.rb\z/, "") # strip extension
|
|
509
612
|
.tr("_", " ")
|
|
510
613
|
.gsub(/\b\w/, &:upcase) # titleize
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
else
|
|
515
|
-
"\e[38;5;222m⌥ #{ghost_snapshot.branch_name}\e[38;5;217m"
|
|
516
|
-
end
|
|
517
|
-
end
|
|
518
|
-
if branch_tag && name_col_width
|
|
519
|
-
tag_width = formatter.display_width(formatter.strip_ansi(branch_tag))
|
|
614
|
+
ghost_badge = ghost_tag(ghost_snapshot)
|
|
615
|
+
if ghost_badge && name_col_width
|
|
616
|
+
tag_width = formatter.display_width(formatter.strip_ansi(ghost_badge))
|
|
520
617
|
available = name_col_width - tag_width - 2
|
|
521
618
|
ghost_name = formatter.truncate_str(ghost_name, available)
|
|
522
619
|
name_width = formatter.display_width(ghost_name)
|
|
523
620
|
padding = name_col_width - name_width - tag_width
|
|
524
|
-
display_name = "#{ghost_name}#{" " * [padding, 2].max}#{
|
|
525
|
-
elsif
|
|
526
|
-
display_name = "#{ghost_name} #{
|
|
621
|
+
display_name = "#{ghost_name}#{" " * [padding, 2].max}#{ghost_badge}"
|
|
622
|
+
elsif ghost_badge
|
|
623
|
+
display_name = "#{ghost_name} #{ghost_badge}"
|
|
527
624
|
else
|
|
528
625
|
display_name = ghost_name
|
|
529
626
|
end
|
data/lib/railbow/version.rb
CHANGED
data/lib/railbow.rb
CHANGED
|
@@ -9,8 +9,10 @@ module Railbow
|
|
|
9
9
|
|
|
10
10
|
# Returns true when Railbow formatting should be disabled.
|
|
11
11
|
# Checks for explicit opt-out, standard conventions, CI, and LLM agents.
|
|
12
|
+
# RBW_PLAIN always wins; RBW_FORCE beats every auto-detection below it.
|
|
12
13
|
def self.plain?
|
|
13
14
|
return true if Params.plain?
|
|
15
|
+
return false if Params.force?
|
|
14
16
|
return true if ENV.key?("NO_COLOR")
|
|
15
17
|
return true if ENV.key?("CLAUDECODE")
|
|
16
18
|
return true if ENV.key?("CI")
|