Module: RailsNexus::SourceCodeHelper

Included in:
LoggedExceptionsHelper, SourceCodeController
Defined in:
app/helpers/rails_nexus/source_code_helper.rb

Instance Method Summary collapse

Instance Method Details

#git_blame_for_line(file_path, line_number) ⇒ Object

Get git blame for a specific line Returns { sha: String, author: String, date: String, message: String }



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'app/helpers/rails_nexus/source_code_helper.rb', line 64

def git_blame_for_line(file_path, line_number)
  resolved_path = resolve_source_path(file_path)
  line_number = Integer(line_number, exception: false)
  return nil unless resolved_path && line_number&.positive?

  directory = File.dirname(resolved_path)
  repository_root, _stderr, status = Open3.capture3("git", "-C", directory, "rev-parse", "--show-toplevel")
  return nil unless status.success?

  repository_root = Pathname.new(repository_root.strip).realpath
  relative_path = Pathname.new(resolved_path).relative_path_from(repository_root).to_s
  return nil if relative_path == ".." || relative_path.start_with?("../")

  # Get blame for single line
  blame_output, _stderr, status = Open3.capture3(
    "git", "-C", repository_root.to_s, "blame", "-L", "#{line_number},#{line_number}", "--porcelain", "--", relative_path
  )
  return nil unless status.success?

  # Parse porcelain blame output
  result = {}
  blame_output.each_line do |line|
    case line
    when /^([0-9a-f]{40})\s+(\d+)\s+(\d+)/
      result[:sha] = $1[0..6]
      result[:line] = $3.to_i
    when /^author\s+(.+)/
      result[:author] = $1.strip
    when /^author-time\s+(\d+)/
      result[:date] = Time.at($1.to_i).strftime("%Y-%m-%d")
    when /^summary\s+(.+)/
      result[:message] = $1.strip
    when /^filename\s+(.+)/
      result[:filename] = $1.strip
    end
  end

  result.presence
rescue ArgumentError, Errno::ENOENT, Errno::EACCES
  nil
end

#parse_backtrace_line(line) ⇒ Object

Parse a backtrace line into components Returns { file: String, line: Integer, method: String, gem: String } or nil



10
11
12
13
14
15
16
17
18
19
20
21
# File 'app/helpers/rails_nexus/source_code_helper.rb', line 10

def parse_backtrace_line(line)
  return nil if line.blank?

  # Match standard Ruby backtrace: "path/to/file.rb:123:in `method_name'"
  if line =~ %r{\A(.+?):(\d+)(?::in `(.+?)')?\z}
    {
      file: $1,
      line: $2.to_i,
      method: $3
    }
  end
end

#read_source_snippet(file_path, error_line, context_lines: 5) ⇒ Object

Read source code snippet around a specific line Returns { lines: Array, file: String, from_line: Integer, to_line: Integer, error_line: Integer }



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'app/helpers/rails_nexus/source_code_helper.rb', line 25

def read_source_snippet(file_path, error_line, context_lines: 5)
  resolved_path = resolve_source_path(file_path)
  return nil unless resolved_path

  error_line = Integer(error_line, exception: false)
  return nil unless error_line&.positive?

  context_lines = (Integer(context_lines, exception: false) || 5).clamp(0, 50)

  total_lines = File.foreach(resolved_path).count
  from_line = [error_line - context_lines, 1].max
  to_line = [error_line + context_lines, total_lines].min

  lines = []
  current_line = 0

  File.foreach(resolved_path) do |raw_line|
    current_line += 1
    break if current_line > to_line
    next if current_line < from_line

    lines << {
      number: current_line,
      content: raw_line.rstrip,
      is_error: current_line == error_line
    }
  end

  {
    lines: lines,
    file: resolved_path,
    from_line: from_line,
    to_line: to_line,
    error_line: error_line
  }
end

#resolve_source_path(file_path) ⇒ Object

Resolve a path and ensure its canonical location is a regular file inside Rails.root.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'app/helpers/rails_nexus/source_code_helper.rb', line 129

def resolve_source_path(file_path)
  return nil if file_path.blank?

  root = Pathname.new(Rails.root).realpath
  candidate = Pathname.new(file_path.to_s)
  candidate = root.join(candidate) unless candidate.absolute?
  resolved = candidate.realpath
  relative = resolved.relative_path_from(root).to_s

  return nil if relative == ".." || relative.start_with?("../")
  return nil unless resolved.file?

  resolved.to_s
rescue ArgumentError, Errno::ENOENT, Errno::EACCES
  nil
end

#source_with_blame(backtrace_lines, limit: 10) ⇒ Object

Get source code with git blame for multiple backtrace lines Returns array of { backtrace_line:, parsed:, source:, blame: }



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'app/helpers/rails_nexus/source_code_helper.rb', line 108

def source_with_blame(backtrace_lines, limit: 10)
  return [] if backtrace_lines.blank?

  backtrace_lines.first(limit).filter_map do |line|
    parsed = parse_backtrace_line(line)
    next nil unless parsed

    source = read_source_snippet(parsed[:file], parsed[:line])
    blame = git_blame_for_line(parsed[:file], parsed[:line])

    {
      backtrace_line: line,
      parsed: parsed,
      source: source,
      blame: blame,
      is_app_code: resolve_source_path(parsed[:file]).present?
    }
  end
end