Created
August 26, 2022 18:56
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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