Module: RailsNexus::SourceCodeHelper
- Included in:
- LoggedExceptionsHelper, SourceCodeController
- Defined in:
- app/helpers/rails_nexus/source_code_helper.rb
Instance Method Summary collapse
-
#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 }.
-
#parse_backtrace_line(line) ⇒ Object
Parse a backtrace line into components Returns { file: String, line: Integer, method: String, gem: String } or nil.
-
#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 }.
-
#resolve_source_path(file_path) ⇒ Object
Resolve a path and ensure its canonical location is a regular file inside Rails.root.
-
#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: }.
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 |