From 386ee4547784dc6a3161f26375f17cdeb736f225 Mon Sep 17 00:00:00 2001 From: Zach Holman Date: Tue, 29 Dec 2009 14:51:58 -0800 Subject: [PATCH] Add a few git scripts from http://git-wt-commit.rubyforge.org --- gitconfig | 5 +- zsh/functions/git-rank-contributers | 60 +++++ zsh/functions/git-wtf | 364 ++++++++++++++++++++++++++++ 3 files changed, 428 insertions(+), 1 deletion(-) create mode 100755 zsh/functions/git-rank-contributers create mode 100755 zsh/functions/git-wtf diff --git a/gitconfig b/gitconfig index 14db5448ff..94b9a11791 100644 --- a/gitconfig +++ b/gitconfig @@ -3,6 +3,9 @@ email = git@email [alias] co = checkout + promote = !~/.dotfiles/zsh/functions/git-promote + wtf = !~/.dotfiles/zsh/functions/git-wtf + rank-contributers = !~/.dotfiles/zsh/functions/git-rank-contributers [color] diff = auto status = auto @@ -11,4 +14,4 @@ excludesfile = /Users/zach/.gitignore editor = mate -w [apply] - whitespace = nowarn + whitespace = nowarn \ No newline at end of file diff --git a/zsh/functions/git-rank-contributers b/zsh/functions/git-rank-contributers new file mode 100755 index 0000000000..de1b612e61 --- /dev/null +++ b/zsh/functions/git-rank-contributers @@ -0,0 +1,60 @@ +#!/usr/bin/env ruby + +## 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 . +## 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("&", "&").gsub("<", "<").gsub(">", ">") 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`.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 + +lines.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 a + end +end \ No newline at end of file diff --git a/zsh/functions/git-wtf b/zsh/functions/git-wtf new file mode 100755 index 0000000000..0d2cba8b62 --- /dev/null +++ b/zsh/functions/git-wtf @@ -0,0 +1,364 @@ +#!/usr/bin/env ruby + +HELP = < +.git-wtfrc" and edit it. The config file is a YAML file that specifies the +integration branches, any branches to ignore, and the max number of commits to +display when --all-commits isn't used. git-wtf will look for a .git-wtfrc file +starting in the current directory, and recursively up to the root. + +IMPORTANT NOTE: all local branches referenced in .git-wtfrc must be prefixed +with heads/, e.g. "heads/master". Remote branches must be of the form +remotes//. +EOS + +COPYRIGHT = <. +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/ +EOS + +require 'yaml' +CONFIG_FN = ".git-wtfrc" + +class Numeric; def pluralize s; "#{to_s} #{s}" + (self != 1 ? "s" : "") end end + +if ARGV.delete("--help") || ARGV.delete("-h") + puts USAGE + exit +end + +## poor man's trollop +$long = ARGV.delete("--long") || ARGV.delete("-l") +$short = ARGV.delete("--short") || ARGV.delete("-s") +$all = ARGV.delete("--all") || ARGV.delete("-a") +$all_commits = ARGV.delete("--all-commits") || ARGV.delete("-A") +$dump_config = ARGV.delete("--dump-config") +$key = ARGV.delete("--key") || ARGV.delete("-k") +$show_relations = ARGV.delete("--relations") || ARGV.delete("-r") +ARGV.each { |a| abort "Error: unknown argument #{a}." if a =~ /^--/ } + +## search up the path for a file +def find_file fn + while true + return fn if File.exist? fn + fn2 = File.join("..", fn) + return nil if File.expand_path(fn2) == File.expand_path(fn) + fn = fn2 + end +end + +want_color = `git config color.wtf` +want_color = `git config color.ui` if want_color.empty? +$color = case want_color.chomp + when "true"; true + when "auto"; $stdout.tty? +end + +def red s; $color ? "\033[31m#{s}\033[0m" : s end +def green s; $color ? "\033[32m#{s}\033[0m" : s end +def yellow s; $color ? "\033[33m#{s}\033[0m" : s end +def cyan s; $color ? "\033[36m#{s}\033[0m" : s end +def grey s; $color ? "\033[1;30m#{s}\033[0m" : s end +def purple s; $color ? "\033[35m#{s}\033[0m" : s end + +## the set of commits in 'to' that aren't in 'from'. +## if empty, 'to' has been merged into 'from'. +def commits_between from, to + if $long + `git log --pretty=format:"- %s [#{yellow "%h"}] (#{purple "%ae"}; %ar)" #{from}..#{to}` + else + `git log --pretty=format:"- %s [#{yellow "%h"}]" #{from}..#{to}` + end.split(/[\r\n]+/) +end + +def show_commits commits, prefix=" " + if commits.empty? + puts "#{prefix} none" + else + max = $all_commits ? commits.size : $config["max_commits"] + max -= 1 if max == commits.size - 1 # never show "and 1 more" + commits[0 ... max].each { |c| puts "#{prefix}#{c}" } + puts grey("#{prefix}... and #{commits.size - max} more (use -A to see all).") if commits.size > max + end +end + +def ahead_behind_string ahead, behind + [ahead.empty? ? nil : "#{ahead.size.pluralize 'commit'} ahead", + behind.empty? ? nil : "#{behind.size.pluralize 'commit'} behind"]. + compact.join("; ") +end + +def widget merged_in, remote_only=false, local_only=false, local_only_merge=false + left, right = case + when remote_only; %w({ }) + when local_only; %w{( )} + else %w([ ]) + end + middle = case + when merged_in && local_only_merge; green("~") + when merged_in; green("x") + else " " + end + print left, middle, right +end + +def show b + have_both = b[:local_branch] && b[:remote_branch] + + pushc, pullc, oosync = if have_both + [x = commits_between(b[:remote_branch], b[:local_branch]), + y = commits_between(b[:local_branch], b[:remote_branch]), + !x.empty? && !y.empty?] + end + + if b[:local_branch] + puts "Local branch: " + green(b[:local_branch].sub(/^heads\//, "")) + + if have_both + if pushc.empty? + puts "#{widget true} in sync with remote" + else + action = oosync ? "push after rebase / merge" : "push" + puts "#{widget false} NOT in sync with remote (you should #{action})" + show_commits pushc unless $short + end + end + end + + if b[:remote_branch] + puts "Remote branch: #{cyan b[:remote_branch]} (#{b[:remote_url]})" + + if have_both + if pullc.empty? + puts "#{widget true} in sync with local" + else + action = pushc.empty? ? "merge" : "rebase / merge" + puts "#{widget false} NOT in sync with local (you should #{action})" + show_commits pullc unless $short + end + end + end + + puts "\n#{red "WARNING"}: local and remote branches have diverged. A merge will occur unless you rebase." if oosync +end + +def show_relations b, all_branches + ibs, fbs = all_branches.partition { |name, br| $config["integration-branches"].include?(br[:local_branch]) || $config["integration-branches"].include?(br[:remote_branch]) } + if $config["integration-branches"].include? b[:local_branch] + puts "\nFeature branches:" unless fbs.empty? + fbs.each do |name, br| + next if $config["ignore"].member?(br[:local_branch]) || $config["ignore"].member?(br[:remote_branch]) + next if br[:ignore] + local_only = br[:remote_branch].nil? + remote_only = br[:local_branch].nil? + name = if local_only + purple br[:name] + elsif remote_only + cyan br[:name] + else + green br[:name] + end + + ## for remote_only branches, we'll compute wrt the remote branch head. otherwise, we'll + ## use the local branch head. + head = remote_only ? br[:remote_branch] : br[:local_branch] + + remote_ahead = b[:remote_branch] ? commits_between(b[:remote_branch], head) : [] + local_ahead = b[:local_branch] ? commits_between(b[:local_branch], head) : [] + + if local_ahead.empty? && remote_ahead.empty? + puts "#{widget true, remote_only, local_only} #{name} #{local_only ? "(local-only) " : ""}is merged in" + elsif local_ahead.empty? + puts "#{widget true, remote_only, local_only, true} #{name} merged in (only locally)" + else + behind = commits_between head, (br[:local_branch] || br[:remote_branch]) + ahead = remote_only ? remote_ahead : local_ahead + puts "#{widget false, remote_only, local_only} #{name} #{local_only ? "(local-only) " : ""}is NOT merged in (#{ahead_behind_string ahead, behind})" + show_commits ahead unless $short + end + end + else + puts "\nIntegration branches:" unless ibs.empty? # unlikely + ibs.sort_by { |v, br| v }.each do |v, br| + next if $config["ignore"].member?(br[:local_branch]) || $config["ignore"].member?(br[:remote_branch]) + next if br[:ignore] + local_only = br[:remote_branch].nil? + remote_only = br[:local_branch].nil? + name = remote_only ? cyan(br[:name]) : green(br[:name]) + + ahead = commits_between v, (b[:local_branch] || b[:remote_branch]) + if ahead.empty? + puts "#{widget true, local_only} merged into #{name}" + else + #behind = commits_between b[:local_branch], v + puts "#{widget false, local_only} NOT merged into #{name} (#{ahead.size.pluralize 'commit'} ahead)" + show_commits ahead unless $short + end + end + end +end + +#### EXECUTION STARTS HERE #### + +## find config file and load it +$config = { "integration-branches" => %w(heads/master heads/next heads/edge), "ignore" => [], "max_commits" => 5 }.merge begin + fn = find_file CONFIG_FN + if fn && (h = YAML::load_file(fn)) # yaml turns empty files into false + h["integration-branches"] ||= h["versions"] # support old nomenclature + h + else + {} + end +end + +if $dump_config + puts $config.to_yaml + exit +end + +## first, index registered remotes +remotes = `git config --get-regexp ^remote\.\*\.url`.split(/[\r\n]+/).inject({}) do |hash, l| + l =~ /^remote\.(.+?)\.url (.+)$/ or next hash + hash[$1] ||= $2 + hash +end + +## next, index followed branches +branches = `git config --get-regexp ^branch\.`.split(/[\r\n]+/).inject({}) do |hash, l| + case l + when /branch\.(.*?)\.remote (.+)/ + name, remote = $1, $2 + + hash[name] ||= {} + hash[name].merge! :remote => remote, :remote_url => remotes[remote] + when /branch\.(.*?)\.merge ((refs\/)?heads\/)?(.+)/ + name, remote_branch = $1, $4 + hash[name] ||= {} + hash[name].merge! :remote_mergepoint => remote_branch + end + hash +end + +## finally, index all branches +remote_branches = {} +`git show-ref`.split(/[\r\n]+/).each do |l| + sha1, ref = l.chomp.split " refs/" + + if ref =~ /^heads\/(.+)$/ # local branch + name = $1 + next if name == "HEAD" + branches[name] ||= {} + branches[name].merge! :name => name, :local_branch => ref + elsif ref =~ /^remotes\/(.+?)\/(.+)$/ # remote branch + remote, name = $1, $2 + remote_branches["#{remote}/#{name}"] = true + next if name == "HEAD" + ignore = !($all || remote == "origin") + + branch = name + if branches[name] && branches[name][:remote] == remote + # nothing + else + name = "#{remote}/#{branch}" + end + + branches[name] ||= {} + branches[name].merge! :name => name, :remote => remote, :remote_branch => "#{remote}/#{branch}", :remote_url => remotes[remote], :ignore => ignore + end +end + +## assemble remotes +branches.each do |k, b| + next unless b[:remote] && b[:remote_mergepoint] + b[:remote_branch] = if b[:remote] == "." + b[:remote_mergepoint] + else + t = "#{b[:remote]}/#{b[:remote_mergepoint]}" + remote_branches[t] && t # only if it's still alive + end +end + +show_dirty = ARGV.empty? +targets = if ARGV.empty? + [`git symbolic-ref HEAD`.chomp.sub(/^refs\/heads\//, "")] +else + ARGV.map { |x| x.sub(/^heads\//, "") } +end.map { |t| branches[t] or abort "Error: can't find branch #{t.inspect}." } + +targets.each do |t| + show t + show_relations t, branches if $show_relations || t[:remote_branch].nil? +end + +modified = show_dirty && `git ls-files -m` != "" +uncommitted = show_dirty && `git diff-index --cached HEAD` != "" + +if $key + puts + puts KEY +end + +puts if modified || uncommitted +puts "#{red "NOTE"}: working directory contains modified files." if modified +puts "#{red "NOTE"}: staging area contains staged but uncommitted files." if uncommitted + +# the end! \ No newline at end of file