Class: RailsWayback::Git

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_wayback/git.rb

Overview

Thin wrapper around shell git for the host application repository.

We shell out to git instead of pulling in a dependency to keep the gem light. All commands are scoped to the host app root and return plain Ruby data structures.

NOTE: Maybe we should need to use a git library instead of shelling out to git. Only if this functionality is not enough for the gem on the future.

Defined Under Namespace

Classes: Commit, ExecutableNotFoundError, GitError, Reference

Constant Summary collapse

REFERENCE_PREFIXES =
{
  "refs/heads/" => :branch,
  "refs/remotes/" => :remote,
  "refs/tags/" => :tag
}.freeze
REFERENCE_KIND_ORDER =
{ branch: 0, remote: 1, tag: 2 }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root: nil) ⇒ Git

Returns a new instance of Git.



28
29
30
# File 'lib/rails_wayback/git.rb', line 28

def initialize(root: nil)
  @root = Pathname.new(root || RailsWayback.configuration.app_root_path)
end

Instance Attribute Details

#root ⇒ Object (readonly)

Returns the value of attribute root.



188
189
190
# File 'lib/rails_wayback/git.rb', line 188

def root
  @root
end

Instance Method Details

#branches ⇒ Object



46
47
48
49
# File 'lib/rails_wayback/git.rb', line 46

def branches
  output = run("for-each-ref", "--format=%(refname:short)", "refs/heads/")
  output.split("\n").map(&:strip).reject(&:empty?)
end

#branches_containing(sha) ⇒ Object

Returns the local branch names that contain sha in their history. Used by the bar to reflect the branch you are RENDERING with when you travel, instead of the branch that is checked out on disk (which never moves — the gem never touches your working tree).



127
128
129
130
131
132
# File 'lib/rails_wayback/git.rb', line 127

def branches_containing(sha)
  output = run("branch", "--contains", sha, "--format=%(refname:short)")
  output.split("\n").map(&:strip).reject(&:empty?)
rescue GitError
  []
end

#checkout_ref(ref, into:) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/rails_wayback/git.rb', line 175

def checkout_ref(ref, into:)
  target = Pathname.new(into)
  FileUtils.mkdir_p(target)
  # `git --work-tree` + `checkout` writes the ref's contents into a
  # detached directory without touching the developer's HEAD or index.
  env = { "GIT_INDEX_FILE" => target.join(".rails_wayback_index").to_s }
  run_with_env(env, "--work-tree=#{target}", "read-tree", ref)
  run_with_env(env, "--work-tree=#{target}", "checkout-index", "--all", "--force")
  target
ensure
  FileUtils.rm_f(target.join(".rails_wayback_index")) if target
end

#commits(branch, limit: RailsWayback.configuration.max_commits) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/rails_wayback/git.rb', line 89

def commits(branch, limit: RailsWayback.configuration.max_commits)
  output = run(
    "log",
    branch,
    "--max-count=#{limit.to_i}",
    "--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ad",
    "--date=iso-strict"
  )
  output.split("\n").reject(&:empty?).map do |line|
    sha, short, subject, author, date = line.split("\x1f", 5)
    Commit.new(sha: sha, short_sha: short, subject: subject, author: author, date: date)
  end
end

#current_branch ⇒ Object



38
39
40
# File 'lib/rails_wayback/git.rb', line 38

def current_branch
  run("rev-parse", "--abbrev-ref", "HEAD").strip
end

#current_commit ⇒ Object



42
43
44
# File 'lib/rails_wayback/git.rb', line 42

def current_commit
  run("rev-parse", "HEAD").strip
end

#diff_paths(ref, paths: []) ⇒ Object

Returns the list of files that differ between ref and the current working tree (unstaged changes included). Optionally scoped to a set of pathspecs so we only report files inside the paths the gem actually swaps for rendering. Never raises: on git failure we log and return an empty list so a missing/broken ref never breaks the bar.



163
164
165
166
167
168
169
170
171
172
173
# File 'lib/rails_wayback/git.rb', line 163

def diff_paths(ref, paths: [])
  args = ["diff", "--name-only", ref]
  args += ["--", *paths] unless paths.empty?
  output = run(*args)
  output.split("\n").map(&:strip).reject(&:empty?)
rescue GitError => e
  if defined?(Rails) && Rails.respond_to?(:logger)
    Rails.logger.warn("[rails-wayback] diff_paths(#{ref.inspect}) failed: #{e.message}")
  end
  []
end

#reference(selector, patterns: RailsWayback.configuration.trusted_ref_patterns) ⇒ Object

Resolves either a canonical full ref name (preferred) or a legacy short branch name, but only from the configured trusted discovery set.

Raises:



75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/rails_wayback/git.rb', line 75

def reference(selector, patterns: RailsWayback.configuration.trusted_ref_patterns)
  available = references(patterns: patterns)
  input = selector.to_s
  exact = available.find { |candidate| candidate.full_name == input }
  return exact if exact

  compatible = available.find do |candidate|
    candidate.name == input || candidate.label == input
  end
  return compatible if compatible

  raise GitError, "Git ref #{input.inspect} is not trusted or is unavailable locally"
end

#references(patterns: RailsWayback.configuration.trusted_ref_patterns) ⇒ Object

Lists refs that are already present in the local repository and match the same trust patterns used to authorize historical rendering. This never fetches or mutates Git state.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/rails_wayback/git.rb', line 54

def references(patterns: RailsWayback.configuration.trusted_ref_patterns)
  output = run(
    "for-each-ref",
    "--format=%(refname)%09%(symref)",
    "refs/heads/",
    "refs/remotes/",
    "refs/tags/"
  )

  discovered = output.split("\n").filter_map do |line|
    full_name, symbolic_target = line.split("\t", 2)
    next unless symbolic_target.to_s.empty?
    next unless trusted_reference?(full_name, patterns)

    build_reference(full_name)
  end
  discovered.sort_by { |reference| [REFERENCE_KIND_ORDER.fetch(reference.kind), reference.label] }
end

#refs_containing(sha) ⇒ Object



134
135
136
137
# File 'lib/rails_wayback/git.rb', line 134

def refs_containing(sha)
  output = run("for-each-ref", "--contains=#{sha}", "--format=%(refname)")
  output.split("\n").map(&:strip).reject(&:empty?)
end

#repository? ⇒ Boolean

Returns:

  • (Boolean)


32
33
34
35
36
# File 'lib/rails_wayback/git.rb', line 32

def repository?
  run("rev-parse", "--is-inside-work-tree").strip == "true"
rescue GitError
  false
end

#resolve_branch_for(sha) ⇒ Object

Same as branches_containing, but picks the most contextual one for display in the bar when the developer did NOT provide an explicit branch alongside the sha:

  • if the current branch contains the sha, prefer it (keeps you in your own context — the sha is on your history line),
  • otherwise pick the first non-current local branch that contains it (typical case of traveling AWAY from your branch),
  • returns nil if no local branch contains the sha (detached ref).


147
148
149
150
151
152
153
154
155
# File 'lib/rails_wayback/git.rb', line 147

def resolve_branch_for(sha)
  candidates = branches_containing(sha)
  return nil if candidates.empty?

  here = current_branch
  return here if candidates.include?(here)

  candidates.first
end

#resolve_ref(ref) ⇒ Object



103
104
105
106
107
108
109
# File 'lib/rails_wayback/git.rb', line 103

def resolve_ref(ref)
  run("rev-parse", "--verify", "#{ref}^{commit}").strip
rescue ExecutableNotFoundError
  raise
rescue GitError
  raise RefNotFoundError, "Unknown git ref: #{ref.inspect}"
end

#show(ref, path) ⇒ Object



111
112
113
# File 'lib/rails_wayback/git.rb', line 111

def show(ref, path)
  run("show", "#{ref}:#{path}")
end

#tree?(ref, path) ⇒ Boolean

Returns:

  • (Boolean)


115
116
117
118
119
120
121
# File 'lib/rails_wayback/git.rb', line 115

def tree?(ref, path)
  run("cat-file", "-t", "#{ref}:#{path}").strip == "tree"
rescue ExecutableNotFoundError
  raise
rescue GitError
  false
end