Module: Railbow::Status::Ghosts

Defined in:
lib/railbow/status/ghosts.rb

Overview

Recovery of "NO FILE" migrations through the optional mighost gem: rows applied to the database whose migration file is no longer on disk.

Defined Under Namespace

Classes: Row

Constant Summary collapse

GHOST_FG =

matches Table::Renderer::GHOST_FG, the row it sits in

"\e[38;5;217m"
MUTED =
"\e[38;5;245m"
BRANCH =
"\e[38;5;222m"

Class Method Summary collapse

Class Method Details

.api_superseded_by(version) ⇒ Object



109
110
111
112
113
114
115
# File 'lib/railbow/status/ghosts.rb', line 109

def api_superseded_by(version)
  return nil unless Mighost::API.respond_to?(:superseded_by)

  Mighost::API.superseded_by(version)
rescue
  nil
end

.available?Boolean

Returns:

  • (Boolean)


33
34
35
# File 'lib/railbow/status/ghosts.rb', line 33

def available?
  defined?(Mighost::API) && Mighost.enabled?
end

.display_name(ghost) ⇒ Object

The name a ghost row displays, derived from its recovered filename.



135
136
137
138
139
140
141
# File 'lib/railbow/status/ghosts.rb', line 135

def display_name(ghost)
  ghost.filename
    .sub(/\A\d+_/, "")      # strip version prefix
    .sub(/\.rb\z/, "")      # strip extension
    .tr("_", " ")
    .gsub(/\b\w/, &:upcase) # titleize
end

.from_live_recovery(version, with_content) ⇒ Object

Detect reads stored snapshots only. A version it lists without a filename has no snapshot yet, so fall back to live git/worktree recovery - that keeps fresh clones working with zero setup.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/railbow/status/ghosts.rb', line 79

def from_live_recovery(version, with_content)
  snapshot = begin
    Mighost::API.find_or_recover_snapshot(version)
  rescue
    nil
  end
  return nil unless snapshot&.filename && !snapshot.filename.empty?

  Row.new(
    filename: snapshot.filename,
    branch_name: snapshot.branch_name,
    source: read_attr(snapshot, :source),
    superseded_by: api_superseded_by(version),
    deleted_in_sha: read_attr(snapshot, :deleted_in_sha),
    author_name: read_attr(snapshot, :author_name),
    author_email: read_attr(snapshot, :author_email),
    content: with_content ? snapshot.content : nil
  )
end

.from_orphan(orphan, version, with_content) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/railbow/status/ghosts.rb', line 63

def from_orphan(orphan, version, with_content)
  Row.new(
    filename: orphan.filename,
    branch_name: orphan.branch_name,
    source: read_attr(orphan, :source),
    superseded_by: read_attr(orphan, :superseded_by),
    deleted_in_sha: read_attr(orphan, :deleted_in_sha),
    author_name: read_attr(orphan, :author_name),
    author_email: read_attr(orphan, :author_email),
    content: with_content ? snapshot_content(version) : nil
  )
end

.load(versions, with_content: false) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/railbow/status/ghosts.rb', line 37

def load(versions, with_content: false)
  # Detect once: OrphanedMigration carries the classification (supersession,
  # deletion commit) that a bare snapshot does not, and already honors
  # dismissals and hide_superseded.
  orphans = begin
    Mighost::API.orphaned_migrations.to_h { |o| [o.version.to_s, o] }
  rescue
    return {}
  end

  rows = {}
  versions.each do |v|
    # Absent from detect = deliberately suppressed (dismissed, or superseded
    # with hide_superseded on) - render as plain NO FILE, do not re-recover.
    next unless (orphan = orphans[v])

    row = if orphan.filename && !orphan.filename.empty?
      from_orphan(orphan, v, with_content)
    else
      from_live_recovery(v, with_content)
    end
    rows[v] = row if row
  end
  rows
end

.read_attr(obj, name) ⇒ Object



99
100
101
# File 'lib/railbow/status/ghosts.rb', line 99

def read_attr(obj, name)
  obj.respond_to?(name) ? obj.public_send(name) : nil
end

.snapshot_content(version) ⇒ Object



103
104
105
106
107
# File 'lib/railbow/status/ghosts.rb', line 103

def snapshot_content(version)
  Mighost::API.find_snapshot(version)&.content
rescue
  nil
end

.tag(ghost) ⇒ Object

One tag slot per ghost row; the most informative wins. Each ends in the ghost foreground rather than a reset, so the rest of the row keeps its ghost styling.



120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/railbow/status/ghosts.rb', line 120

def tag(ghost)
  if ghost.superseded_by
    "#{MUTED}#{ghost.superseded_by}#{GHOST_FG}"
  elsif ghost.branch_name
    if ghost.source == "worktree"
      "#{BRANCH}⌥ₜ#{ghost.branch_name}#{GHOST_FG}"
    else
      "#{BRANCH}#{ghost.branch_name}#{GHOST_FG}"
    end
  elsif ghost.deleted_in_sha
    "#{MUTED}✂ deleted in:#{ghost.deleted_in_sha[0, 8]}#{GHOST_FG}"
  end
end