Skip to content

Instantly share code, notes, and snippets.

@bf4
Last active December 26, 2015 20:29
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 bf4/7209165 to your computer and use it in GitHub Desktop.
Save bf4/7209165 to your computer and use it in GitHub Desktop.
git ranking and erd
#!/usr/bin/ruby
# from https://github.com/sferik/twitter/blob/master/etc/erd.rb
# under MIT license
#
COLON = ':'.freeze
UNDERSCORE = '_'.freeze
TAB = "\t".freeze
# Usage:
# ruby erd.rb [gem_name] [Namespace1, Namespace2, etc]
# Example Usage:
# ruby -Ilib etc/erd.rb metric_fu MetricFu
args = ARGV.to_a
gem_name = args.shift
fail "No require path specified" if gem_name.to_s.size.zero?
require gem_name
namespaces = args
fail "No namespaces specified" if namespaces.size.zero?
NAMESPACES = namespaces.map{|namespace| "#{namespace}::".freeze }
# Colons are invalid characters in DOT nodes.
# Replace them with underscores.
# http://www.graphviz.org/doc/info/lang.html
def nodize(klass)
klass.name.tr(COLON, UNDERSCORE)
end
nodes = Hash.new
edges = Hash.new
library_objects = ObjectSpace.each_object(Class).select do |klass|
klass_name = klass.name.to_s
NAMESPACES.any?{|namespace| klass_name.start_with?(namespace) }
end
library_objects.each do |klass|
begin
unless klass.nil? || klass.superclass.nil? || klass.name.empty?
nodes[nodize(klass)] = klass.name
edges[nodize(klass)] = nodize(klass.superclass)
end
klass = klass.superclass
end until klass.nil?
end
edges.delete(nil)
def puts(object, indent = 0, tab = TAB)
super(tab * indent + object)
end
File.open('erd.dot', 'w') do |file|
begin
original_stdout = $stdout
$stdout = file
puts 'digraph classes {'
# Add or remove DOT formatting options here
puts "graph [rotate=0, rankdir=\"LR\"]", 1
puts "node [fillcolor=\"#c4ddec\", style=\"filled\", fontname=\"HelveticaNeue\"]", 1
puts "edge [color=\"#444444\"]", 1
nodes.sort.each do |node, label|
puts "#{node} [label=\"#{label}\"]", 1
end
edges.sort.each do |child, parent|
puts "#{child} -> #{parent}", 1
end
puts "}"
ensure
$stdout = original_stdout
end
end
dot2png = "dot erd.dot -Tpng -o erd.png"
puts "Now running: #{dot2png}"
system(dot2png)
#!/usr/bin/env ruby
# from http://git-wt-commit.rubyforge.org/git-rank-contributors
## git-rank-contributors: a simple script to trace through the logs and
## rank contributors by the total size of the diffs they're responsible for.
## A change counts twice as much as a plain addition or deletion.
##
## Output may or may not be suitable for inclusion in a CREDITS file.
## Probably not without some editing, because people often commit from more
## than one address.
##
## git-rank-contributors Copyright 2008 William Morgan <wmorgan-git-wt-add@masanjin.net>.
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or (at
## your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You can find the GNU General Public License at:
## http://www.gnu.org/licenses/
class String
def obfuscate; gsub(/@/, " at the ").gsub(/\.(\w+)(>|$)/, ' dot \1s\2') end
def htmlize; gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") end
end
lines = {}
verbose = ARGV.delete("-v")
obfuscate = ARGV.delete("-o")
htmlize = ARGV.delete("-h")
author = nil
state = :pre_author
`git log -M -C -C -p --no-color`.split(/\n\r?/).each do |l|
case
when (state == :pre_author || state == :post_author) && l =~ /Author: (.*)$/
author = $1
state = :post_author
lines[author] ||= 0
when state == :post_author && l =~ /^\+\+\+/
state = :in_diff
when state == :in_diff && l =~ /^[\+\-]/
lines[author] += 1
when state == :in_diff && l =~ /^commit /
state = :pre_author
end
end
author_name_mapping = {}
lines_joined_by_name = lines.each_with_object({}) do |author_number_of_commits, result|
author, number_of_commits = author_number_of_commits
stripped_author = author.strip[/\A[^@|<]+/]
comparable_author = stripped_author.downcase.gsub(/\s+/, '')
previous = result.fetch(comparable_author) { result[comparable_author] = 0 }
result[comparable_author] = previous + number_of_commits
previous_author = author_name_mapping.fetch(comparable_author) do
author_name_mapping[comparable_author] = stripped_author
end
if (stripped_author.size > previous_author.size) ||
(previous_author.downcase == stripped_author.downcase &&
previous_author.downcase == previous_author)
author_name_mapping[comparable_author] = stripped_author
end
end
lines_joined_by_name.sort_by { |a, c| -c }.each do |a, c|
a = a.obfuscate if obfuscate
a = a.htmlize if htmlize
if verbose
puts "#{a}: #{c} lines of diff"
else
puts author_name_mapping[a].strip
end
end
#!/usr/bin/env ruby
# from https://github.com/mmrobins/git-rank/blob/master/lib/git-rank.rb
# require 'git-rank/log'
require 'digest/md5'
module GitRank
module Log
class << self
def calculate(options = {})
authors = Hash.new {|h, k| h[k] = h[k] = Hash.new {|h, k| h[k] = Hash.new(0)}}
options_digest = Digest::MD5.hexdigest(options[:additions_only].to_s + options[:deletions_only].to_s)
author = nil
file = nil
state = :pre_author
git_log(options).each_line do |line|
case
when (state == :pre_author || state == :post_author) && line =~ /Author: (.*)\s</
author = $1
state = :post_author
when line =~ /^(\d+)\s+(\d+)\s+(.*)/
additions = $1.to_i
deletions = $2.to_i
file = $3
authors[author][file][:additions] += additions
authors[author][file][:deletions] += deletions
authors[author][file][:net] += additions - deletions
authors[author][file][:total] += additions + deletions
state = :in_diff
when state == :in_diff && line =~ /^commit /
state = :pre_author
end
end
authors
end
private
def git_log(options)
`git log -M -C -C -w --no-color --numstat #{options[:range]}`
end
end
end
end
# require 'git-rank/blame'
require 'digest/md5'
module GitRank
module Blame
class << self
def calculate(options = {})
options[:exline] ||= []
authors = Hash.new {|h, k| h[k] = h[k] = Hash.new(0)}
options_digest = Digest::MD5.hexdigest(options[:exline].to_s)
get_files_to_blame.each do |file|
lines = blame(file)
lines.each do |line|
next if options[:exline].any? { |exline| line =~ /#{exline}/ }
# Get author info out of the line
# This will probably need improvements if people try to
# use this with weird names
line =~ / \((.*?)\d/
raise line unless $1
authors[$1.strip][file] += 1
end
end
authors
end
private
def blame(file)
lines = `git blame -w #{file}`.lines
puts "git blame failed on #{file}" unless $?.exitstatus == 0
lines
end
def get_files_to_blame
Dir.glob("**/*").reject { |f| !File.file? f or f =~ /\.git/ or File.binary? f }
end
end
end
end
# require 'yaml'
# require 'fileutils'
#
# module GitRank
# module Cache
# class << self
# def cache_file(prefix, options)
# File.join(cache_dir, prefix + options_digest)
# end
#
# def cache_dir
# cache_dir = File.expand_path("~/.git_rank/#{git_head_or_exit}")
# FileUtils.mkdir_p(cache_dir)
# cache_dir
# end
#
# def save(data, file)
# File.open(file, 'w') do |f|
# f.puts data.to_yaml
# end
# end
#
# def retrieve(file)
# return nil
# YAML::load( File.open(file) ) if File.exist? file
# end
#
# def git_head_or_exit
# git_head = `git rev-parse HEAD`.chomp
# exit unless $?.exitstatus == 0
# end
# end
# end
# end
#
# from ptools https://github.com/djberg96/ptools/blob/master/lib/ptools.rb
class File
def self.binary?(file)
s = (File.read(file, File.stat(file).blksize) || "").split(//)
((s.size - s.grep(" ".."~").size) / s.size.to_f) > 0.30
end
end
class Array
def sum
inject(:+)
end
end
module GitRank
class << self
def calculate(options = {})
authors = if options[:blame]
GitRank::Blame.calculate(options)
else
GitRank::Log.calculate(options)
end
authors
end
end
end
# require 'git-rank/options'
require 'optparse'
module GitRank::Options
def self.parse
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: git-rank [options]"
options[:exfile] = []
options[:exline] = []
options[:exauthor] = []
opts.on("-a", "--author [AUTHOR]", "Author breakdown by file") do |author|
options[:author] ||= []
options[:author] << author
end
opts.on("-e", "--exclude-author [EXCLUDE]", "Exclude authors") do |exauthor|
options[:exauthor] << exauthor
end
opts.on("-b", "--blame", "Rank by blame of files not by git log") do
options[:blame] = true
end
opts.on("-z", "--all-authors-breakdown", "All authors breakdown by file") do |author|
options[:all_authors] ||= []
options[:all_authors] << author
end
opts.on("-x", "--exclude-file [EXCLUDE]", "Exclude files or directories") do |exfile|
options[:exfile] << exfile
end
opts.on("-y", "--exclude-line [EXCLUDE]", "Exclude lines matching a string") do |exline|
options[:exline] << exline
end
opts.on("--additions-only", "Only count additions") do
options[:additions_only] = true
end
opts.on("--deletions-only", "Only count deltions") do
options[:deletions_only] = true
end
opts.on_tail("-h", "--help", "Show this message") do
puts opts
puts <<-HEREDOC
Examples:
# Shows authors and how many lines they're
# blamed for in all files in this directory
git-rank
# Shows file breakdown for all authors
# and excludes files in a few directories
git-rank -z -x spec/fixtures -x vendor
# Shows file breakdown for just a few authors
git-rank-contributors -a "Bob Johnson" -a prince
HEREDOC
exit
end
end.parse!
if !ARGV.empty?
if ARGV.size == 1
options[:range] = ARGV.first
else
raise OptionParser::InvalidArgument, 'Only one range can be specified'
end
end
options
end
end
# require 'git-rank/printer'
module GitRank
module Printer
class << self
def print(authors, options = {})
options[:exfile] ||= []
options[:exauthor] ||= []
authors = delete_excluded_files(authors, options[:exfile])
if options[:author] and !options[:all_authors]
options[:author].each do |author_name|
puts "#{author_name} #{authors[author_name].values.sum}"
print_author_breakdown(author_name, authors[author_name])
end
else
authors.reject! {|k, v| options[:exauthor].include? k}
longest_author_name = authors.keys.max {|a,b| a.length <=> b.length }.length
sorted_authors = authors.sort_by {|k, v| -v.values.inject(0) {|sum, counts| sum += counts[:total]} }
sorted_authors.each do |author, line_counts|
padding = ' ' * (longest_author_name - author.size + 1)
total = line_counts.values.inject(0) {|sum, counts| sum += counts[:total]}
additions = line_counts.values.inject(0) {|sum, counts| sum += counts[:additions]}
deletions = line_counts.values.inject(0) {|sum, counts| sum += counts[:deletions]}
output = "#{author}#{padding}"
if options[:additions_only]
output << "+#{additions}"
elsif options[:deletions_only]
output << "-#{deletions}"
else
output << "#{total} (+#{additions} -#{deletions})"
end
puts output
if options[:all_authors]
print_author_breakdown(author, line_counts, longest_author_name, options)
puts output
end
end
end
end
private
def print_author_breakdown(author_name, author_data, padding_size=nil, options = {})
padding_size ||= author_name.size
padding = ' ' * (padding_size + 1)
author_data.sort_by {|k, v| v[:total] }.each do |file, count|
next unless count[:total] > 100
output = "#{padding}"
if options[:additions_only]
output << "+#{count[:additions]}"
elsif options[:deletions_only]
output << "-#{count[:deletions]}"
else
output << "#{count[:total]} (+#{count[:additions]} -#{count[:deletions]})"
end
puts "#{output} #{file}"
end
end
def delete_excluded_files(authors, excluded_files)
excluded_files ||= []
authors.each do |author, line_counts|
line_counts.each do |file, count|
line_counts.delete(file) if excluded_files.any? {|ex| file =~ /^#{ex}/}
end
end
end
end
end
end
options = GitRank::Options.parse
authors = GitRank.calculate(options)
GitRank::Printer.print(authors, options)
#!/bin/sh
# from https://raw.github.com/visionmedia/git-extras/master/bin/git-summary
commit=""
test $# -ne 0 && commit=$@
project=${PWD##*/}
#
# get date for the given <commit>
#
date() {
git log --pretty='format: %ai' $1 | cut -d ' ' -f 2
}
#
# get active days for the given <commit>
#
active_days() {
date $1 | uniq | awk '
{ sum += 1 }
END { print sum }
'
}
#
# get the commit total
#
commit_count() {
git log --oneline $commit | wc -l | tr -d ' '
}
#
# total file count
#
file_count() {
git ls-files | wc -l | tr -d ' '
}
#
# list authors
#
authors() {
git shortlog -n -s $commit | awk '
{ args[NR] = $0; sum += $0 }
END {
for (i = 1; i <= NR; ++i) {
printf "%-30s %2.1f%%\n", args[i], 100 * args[i] / sum
}
}
'
}
#
# fetch repository age from oldest commit
#
repository_age() {
git log --reverse --pretty=oneline --format="%ar" | head -n 1 | sed 's/ago//'
}
# summary
echo
echo " project : $project"
echo " repo age :" $(repository_age)
echo " active :" $(active_days) days
echo " commits :" $(commit_count)
if test "$commit" = ""; then
echo " files :" $(file_count)
fi
echo " authors : "
authors
echo
#!/usr/bin/env bash
git shortlog -s -n
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment