Skip to content

Instantly share code, notes, and snippets.

@aalin
Created March 2, 2015 19:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aalin/6a69ac4ffda4645482d5 to your computer and use it in GitHub Desktop.
Save aalin/6a69ac4ffda4645482d5 to your computer and use it in GitHub Desktop.
Git blame with different colors depending when each line was changed. Darker = older, lighter = newer.
require 'shellwords'
class Blamify
PORCELAIN_COMMIT_HASH_RE = /^(?<hash>\h{40})\s/
PORCELAIN_LINE_RE = /^\t(?<line>.*)$/
COLORS = (237..255).to_a # Greyscale gradient
def initialize(filename)
@filename = filename
end
def print(output = $stdout)
committer_times = blames.map { |blame| blame[:committer_time].to_i }
min_time = committer_times.min
max_time = committer_times.max
timespan = max_time - min_time
mid_time = min_time + timespan / 2
blames.each do |blame|
committer_time = blame[:committer_time].to_i
offset = committer_time - min_time
Time.at(committer_time)
color = color_at(offset / timespan.to_f)
author = blame.fetch(:author, "")
output.puts format("\e[38;5;%dm%-14s: %s\e[0m", color, author[0, 14], blame[:line])
end
end
private
def color_at(x)
x = 0 if x.nan?
x = [[x, 0.0].max, 1.0].min # Clamp to (0.0, 1.0)
COLORS[(x * COLORS.size.pred).floor]
end
def blames
@blames ||= load_blames
end
def load_blames
blame_data = {}
load_porcelain.lines.each_with_object([]) do |line, blames|
case line
when PORCELAIN_COMMIT_HASH_RE
blame_data[:commit] = $~[:hash]
when PORCELAIN_LINE_RE
blames << blame_data.merge(line: $~[:line])
else # Key/value
key, value = line.chomp.split(' ', 2)
key.tr!('-', '_')
blame_data[key.to_sym] = value
end
end
end
def load_porcelain
`#{ Shellwords.shelljoin(['git', 'blame', '-p', @filename]) }`
end
end
if file = ARGV.first
dir = File.dirname(File.expand_path(file))
Dir.chdir(dir) do
Blamify.new(File.basename(file)).print
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment