Skip to content

Instantly share code, notes, and snippets.

@andynu
Created August 26, 2022 18:56
Show Gist options
  • Save andynu/dc732ceaf16d34dbbeb2bac97c34b28d to your computer and use it in GitHub Desktop.
Save andynu/dc732ceaf16d34dbbeb2bac97c34b28d to your computer and use it in GitHub Desktop.
Given a ruby file and method name shows you all the different versions across the git history.
#!/usr/bin/env ruby
# Given a file and method_name
# Show all the different implementations across the git history (first commit per implementation).
#
# show_method_history <file> <method_name> --html
#
# e.g. show_method_history test/test_helper.rb sign_in --html
#
# WARNING: the --html output just dumps html files into your current folder.
#
require 'parser/current'
require 'digest'
require 'stringio'
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'coderay', '~> 1.1.3'
end
require 'coderay'
Commit = Struct.new(:hash, :author, :email, :date, :message)
Implementation = Struct.new(:file, :name, :ast, :src, :sha1, :commit) do
def to_terminal
<<~STR
# method sha1: #{sha1}
# commit : #{commit.hash}
# author: #{commit.author}
# date: #{commit.date}
# message: #{commit.message}
#{CodeRay.highlight(src, :ruby, {}, :term)}
STR
end
end
def find_methods_ast(ast)
return unless ast.is_a?(Parser::AST::Node)
methods = []
case ast.type
when :class, :module, :begin
methods.concat(ast.children.flat_map {|child| find_methods_ast(child) })
when :def
methods << ast
end
methods
end
def find_methods_file(file, file_content)
code = file_content.read
#p code: code
ast = Parser::CurrentRuby.parse(code)
find_methods_ast(ast)
end
def implementation_from_method_ast(file, file_content, method_ast, commit)
name = method_ast.to_a.first
first_line = method_ast.location.first_line - 1
last_line = method_ast.location.last_line
file_content.rewind
src = file_content.readlines[first_line...last_line].join
sha1 = Digest::SHA1.hexdigest src
Implementation.new(file, name, method_ast, src, sha1, commit)
end
def implementation_from_file(file, file_content, method_name, commit)
methods = find_methods_file(file, file_content)
method_ast = methods.find{|meth| meth.to_a.first == method_name }
return nil if method_ast.nil?
implementation_from_method_ast(file, file_content, method_ast, commit)
end
def commits_from_file(file)
results = `git log --pretty="%H\t%aN\t%aE\t%aD\t%s" #{file}`
results.lines.reverse.map{|line| Commit.new(*line.split("\t")) }
end
def file_at_version(filename, version_hash)
# puts "git show #{version_hash}:#{filename}"
StringIO.new(`git show #{version_hash}:#{filename}`)
end
if __FILE__ == $0
if ARGV.size.zero?
puts <<~USAGE
Search files for specific method, group by hash of method src.
show_method_history <file> <method_name>
show_method_history <file> <method_name> --html
e.g. show_method_history test/test_helper.rb sign_in
USAGE
exit
end
implementations = []
filename, method_name, opt, = ARGV
method_name = method_name.to_sym
seen_method_hashes = Set.new
commits_from_file(filename).each do |commit|
file_content = file_at_version(filename, commit.hash)
implementation = implementation_from_file(filename, file_content, method_name, commit)
next if implementation.nil?
next if seen_method_hashes.include?(implementation.sha1)
implementations << implementation
seen_method_hashes << implementation.sha1
end
if opt == '--html'
html_file_name = ->(impl, i){ "#{i}_#{impl.name}.html" }
prev_impl = nil
implementations.each_with_index do |impl, i|
next_impl = implementations[i + 1]
html = StringIO.new
html << "<html><head>"
html << '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">'
html << "</head><body>"
html << "<table><tr>"
html << "<td><a href='#{html_file_name.call(implementations.first, 0)}'>first</a>"
html << "<td>prev" if prev_impl.nil?
html << "<td><a href='#{html_file_name.call(prev_impl, i - 1)}'>prev</a>" unless prev_impl.nil?
html << "<td>next" if next_impl.nil?
html << "<td><a href='#{html_file_name.call(next_impl, i + 1)}'>next</a>" unless next_impl.nil?
html << "<td><a href='#{html_file_name.call(implementations.last, implementations.count - 1)}'>last</a>"
html << "</table>"
commit = impl.commit
html << <<~STR
<dl>
<dt>method sha1 <dd>#{impl.sha1}
<dt>commit <dd>#{commit.hash}
<dt>author <dd>#{commit.author}
<dt>date <dd>#{commit.date}
<dt>message <dd>#{commit.message}
</dl>
<div class='CodeRay'>
#{CodeRay.scan(impl.src, :ruby).html(wrap: :div, line_numbers: :inline, css: :style)}
</div>
STR
current_filename = html_file_name.call(impl, i)
File.write current_filename, html.string
puts "file://#{File.expand_path(current_filename)}"
prev_impl = impl
end
else
implementations.each do |impl|
puts impl.to_terminal
puts
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment