{"thread":{"id":"35124","subject":"[PATCH v10 00/15] New git-related helper","startedAt":"2013-10-12T07:06:09Z","lastAt":"2013-10-12T07:06:24Z","messageCount":16,"participants":["Felipe Contreras"],"isPatch":true,"patchVersion":10,"patchTotal":15},"messages":[{"id":"228822","messageId":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":null,"subject":"[PATCH v10 14/15] contrib: related: add option to show commits","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:09Z","receivedAt":"2013-10-12T07:06:09Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Instead of showing the authors and signers, show the commits themselves.\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related    | 23 +++++++++++++++++++++++\n contrib/related/test-related.t | 10 ++++++++++\n 2 files changed, 33 insertions(+)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex d6b44c7..b9c8619 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -7,6 +7,7 @@ $since = '5-years-ago'\n $min_percent = 10\n $files = []\n $rev_args = []\n+$show_commits = false\n \n $mailmaps = {}\n $mailmaps_complex = {}\n@@ -128,6 +129,10 @@ opts.on('d', 'since', 'How far back to search for relevant commits') do |v|\n   $since = v\n end\n \n+opts.on('c', 'commits', 'List commits instead of persons') do |v|\n+  $show_commits = v\n+end\n+\n opts.parse\n \n class Person\n@@ -238,6 +243,10 @@ class Commits\n     @items.each(&block)\n   end\n \n+  def list\n+    @items.keys\n+  end\n+\n   def import\n     return if @items.empty?\n     File.popen(%w[git cat-file --batch], 'r+') do |p|\n@@ -339,6 +348,20 @@ else\n end\n commits.import\n \n+if $show_commits\n+  cmd = nil\n+  case $show_commits\n+  when 'raw'\n+    puts commits.list\n+  when 'full'\n+    cmd = %w[git log --patch --no-walk]\n+  else\n+    cmd = %w[git log --oneline --no-walk]\n+  end\n+  system(*cmd + commits.list) if cmd\n+  exit 0\n+end\n+\n persons = Persons.new\n \n persons.sort.reverse.each do |person|\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nindex fa8c1a7..f357e30 100755\n--- a/contrib/related/test-related.t\n+++ b/contrib/related/test-related.t\n@@ -94,4 +94,14 @@ test_expect_success \"mailmap\" \"\n \ttest_cmp expected actual\n \"\n \n+test_expect_success \"commits\" \"\n+\tgit related -craw -1 master | git log --format='%s' --no-walk --stdin > actual &&\n+\tcat > expected <<-EOF &&\n+\tfour\n+\tthree\n+\tone\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n test_done\n-- \n1.8.4-fc\n"},{"id":"228820","messageId":"1381561584-20529-2-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 00/15] New git-related helper","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:10Z","receivedAt":"2013-10-12T07:06:10Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"This tool finds people that might be interested in a patch, by going\nback through the history for each single hunk modified, and finding\npeople that reviewed, acknowledged, signed, or authored the code the\npatch is modifying.\n\nIt does this by running `git blame` incrementally on each hunk, and\nthen parsing the commit message. After gathering all the relevant\npeople, it groups them to show what exactly was their role when the\nparticipated in the development of the relevant commit, and on how\nmany relevant commits they participated. They are only displayed if\nthey pass a minimum threshold of participation.\n\nFor example:\n\n  % git related master..fc/transport/improv\n  Junio C Hamano <gitster@pobox.com> (signer: 90%, author: 5%)\n  Felipe Contreras <felipe.contreras@gmail.com> (author: 25%, reviewer: 2%)\n  Sverre Rabbelier <srabbelier@gmail.com> (author: 17%, acker: 2%, signer: 7%)\n  Jeff King <peff@peff.net> (acker: 17%, author: 10%)\n  Shawn O. Pearce <spearce@spearce.org> (author: 5%, signer: 2%, cced: 2%)\n  Elijah Newren <newren@gmail.com> (author: 10%)\n\nIn addition, it has an option to output the list of commits, instead\nof the contributors, which allows you to easily find out the previous\nchanges to the lines your patches modify.\n\n  % git related -c master..fc/transport/improv\n  99d9ec0 Merge branch 'fc/transport-helper-no-refspec'\n  67c9c78 transport-helper: barf when user tries old:new\n  0460ed2 documentation: trivial style cleanups\n  126aac5 transport-helper: fix remote helper namespace regression\n  21610d8 transport-helper: clarify pushing without refspecs\n  a93b4a0 transport-helper: warn when refspec is not used\n  664059f transport-helper: update remote helper namespace\n  c4458ec fast-export: Allow pruned-references in mark file\n  ...\n\nMoreover, when sending patches for review, you can configure `git\nsend-email` to use `git related` to find relevant people that should\nbe Cc'ed:\n\n  % git send-email --cc-cmd='git related' *.patch\n\nIt has way many more features than git-contacts which is a rewrite in\nPerl of this script, but only the initial bare minimum stage.\n\nI've also added tests, and this is how git-contacts fares in comparison:\n\n--- expected 2013-10-10 21:13:45.938291298 +0000\n+++ actual 2013-10-10 21:13:45.937291298 +0000\n@@ -1,2 +1,2 @@\n-Jon Stewart <jon@stewart.com> (author: 50%)\n-Pablo Escobar <pablo@escobar.com> (author: 50%)\n+Jon Stewart <jon@stewart.com>\n+Pablo Escobar <pablo@escobar.com>\nnot ok 1 - basic\n\ngit-contacts doesn't show the amount of involvement, but that's easy to fix.\n\n--- expected 2013-10-10 21:13:45.975291299 +0000\n+++ actual 2013-10-10 21:13:45.974291299 +0000\n@@ -1,2 +1,3 @@\n-John Poppins <john@doe.com> (author: 66%)\n-Jon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n+John Doe <john@doe.com>\n+John Poppins <john@doe.com>\n+Jon Stewart <jon@stewart.com>\nnot ok 2 - others\n\ngit-contacts doesn't show the kind of involvement (reviewer, author)\nnor does it group people by their email address.\n\n(skip other failures)\n\nexpecting success:\ngit related -craw -1 master | git log --format='%s' --no-walk --stdin\n> actual &&\ncat > expected <<-EOF &&\nfour\nthree\none\nEOF\ntest_cmp expected actual\n\nnot ok 7 - commits\n\nAlso, it doesn't have the option to show the commits themselves, which\nis useful to investigate the diffs. This feature alone is what makes\nthe make \"git-contacts\" not appropriate, because if this feature is\nimplemented, 'git contacts --commits' wouldn't have anything to do\nwith \"contacts\".\n\nFor that matter it doesn't support any options at all (e.g.\n--min-percent, --since), and doesn't have a usage helper (--help).\n\nChanges since v9:\n\ndiff --git a/contrib/related/Makefile b/contrib/related/Makefile\nnew file mode 100644\nindex 0000000..1224fce\n--- /dev/null\n+++ b/contrib/related/Makefile\n@@ -0,0 +1,17 @@\n+TESTS := $(wildcard test-*.t)\n+\n+export T := $(addprefix $(CURDIR)/,$(TESTS))\n+export MAKE := $(MAKE) -e\n+export PATH := $(CURDIR):$(PATH)\n+export TEST_LINT := test-lint-executable test-lint-shell-syntax\n+export TEST_DIRECTORY := $(CURDIR)/../../t\n+\n+all:\n+\n+test:\n+\t$(MAKE) -C ../../t $@\n+\n+$(TESTS): all\n+\t$(MAKE) -C ../../t $(CURDIR)/$@\n+\n+.PHONY: all test $(TESTS)\ndiff --git a/contrib/related/README b/contrib/related/README\nnew file mode 100644\nindex 0000000..26188ca\n--- /dev/null\n+++ b/contrib/related/README\n@@ -0,0 +1,65 @@\n+= git-related =\n+\n+This tool finds people that might be interested in a patch, by going\n+back through the history for each single hunk modified, and finding\n+people that reviewed, acknowledged, signed, or authored the code the\n+patch is modifying.\n+\n+It does this by running `git blame` incrementally on each hunk, and then\n+parsing the commit message. After gathering all the relevant people, it\n+groups them to show what exactly was their role when the participated in\n+the development of the relevant commit, and on how many relevant commits\n+they participated. They are only displayed if they pass a minimum\n+threshold of participation.\n+\n+For example:\n+\n+------------\n+% git related master..fc/transport/improv\n+Junio C Hamano <gitster@pobox.com> (signer: 90%, author: 5%)\n+Felipe Contreras <felipe.contreras@gmail.com> (author: 25%, reviewer: 2%)\n+Sverre Rabbelier <srabbelier@gmail.com> (author: 17%, acker: 2%, signer: 7%)\n+Jeff King <peff@peff.net> (acker: 17%, author: 10%)\n+Shawn O. Pearce <spearce@spearce.org> (author: 5%, signer: 2%, cced: 2%)\n+Elijah Newren <newren@gmail.com> (author: 10%)\n+------------\n+\n+In addition, it has an option to output the list of commits, instead of the\n+contributors, which allows you to easily find out the previous changes to the\n+lines your patches modify.\n+\n+------------\n+% git related -c master..fc/transport/improv\n+99d9ec0 Merge branch 'fc/transport-helper-no-refspec'\n+67c9c78 transport-helper: barf when user tries old:new\n+0460ed2 documentation: trivial style cleanups\n+126aac5 transport-helper: fix remote helper namespace regression\n+21610d8 transport-helper: clarify pushing without refspecs\n+a93b4a0 transport-helper: warn when refspec is not used\n+664059f transport-helper: update remote helper namespace\n+c4458ec fast-export: Allow pruned-references in mark file\n+...\n+------------\n+\n+Moreover, when sending patches for review, you can configure `git send-email`\n+to use `git related` to find relevant people that should be Cc'ed:\n+\n+------------\n+% git send-email --cc-cmd='git related' *.patch\n+------------\n+\n+== Installation ==\n+\n+To use this script, simply put `git-related` in your `$PATH` and make sure it\n+has executable permissions:\n+\n+------------\n+wget https://raw.github.com/felipec/git-related/master/git-related -O ~/bin/git-related\n+chmod +x ~/bin/git-related\n+------------\n+\n+And make sure `~/bin` is in your `$PATH`:\n+\n+------------\n+export PATH=\"$HOME/bin:$PATH\"\n+------------\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 7248f0f..b9c8619 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -17,9 +17,10 @@ $base_dir = File.dirname(git_dir)\n $cur_dir = Dir.pwd\n \n KNOWN_ROLES = {\n-  'Signed-off' => :signer,\n-  'Reviewed' => :reviewer,\n-  'Acked' => :acker,\n+  'Signed-off-by' => :signer,\n+  'Reviewed-by' => :reviewer,\n+  'Acked-by' => :acker,\n+  'Cc' => :cced,\n }\n \n def get_mailmap(filename)\n@@ -45,17 +46,16 @@ get_mailmap(File.join($base_dir, '.mailmap'))\n mailmap_file = %x[git config mailmap.file].chomp\n get_mailmap(mailmap_file)\n \n-class SimpleParser\n+class ParseOpt\n   attr_writer :usage\n \n   class Option\n-    attr_reader :short, :long, :values, :help\n+    attr_reader :short, :long, :help\n \n-    def initialize(short, long, values, help, &block)\n+    def initialize(short, long, help, &block)\n       @block = block\n       @short = short\n       @long = long\n-      @values = values\n       @help = help\n     end\n \n@@ -68,49 +68,49 @@ class SimpleParser\n     @list = {}\n   end\n \n-  def on(*args, &block)\n-    short = args.shift if args.first.is_a?(String)\n-    long = args.shift if args.first.is_a?(String)\n-    values = args.shift if args.first.is_a?(Array)\n-    help = args.shift if args.first.is_a?(String)\n-    opt = Option.new(short, long, values, help, &block)\n+  def on(short = nil, long = nil, help = nil, &block)\n+    opt = Option.new(short, long, help, &block)\n     @list[short] = opt if short\n     @list[long] = opt if long\n   end\n \n   def parse\n-    i = 0\n     if ARGV.member?('-h') or ARGV.member?('--help')\n       usage\n-      exit 1\n+      exit 0\n     end\n-    while cur = ARGV[i] do\n-      if cur =~ /^(-.+?)(?:=(.*))?$/\n+    seen_dash = false\n+    ARGV.delete_if do |cur|\n+      opt = val = nil\n+      next false if cur[0] != '-' or seen_dash\n+      case cur\n+      when '--'\n+        seen_dash = true\n+        next true\n+      when /^--no-(.+)$/\n         opt = @list[$1]\n-        if opt\n-          v = $2\n-          if not v\n-            if not opt.values\n-              extra = true\n-            else\n-              extra = !!opt.values.map(&:to_s).member?(ARGV[i + 1])\n-            end\n-            v = extra ? ARGV.delete_at(i + 1) : true\n-          end\n-          opt.call(v)\n-          ARGV.delete_at(i)\n-          next\n-        end\n+        val = false\n+      when /^-([^-])(.+)?$/, /^--(.+?)(?:=(.+))?$/\n+        opt = @list[$1]\n+        val = $2 || true\n+      end\n+      if opt\n+        opt.call(val)\n+        true\n       end\n-      i += 1\n     end\n   end\n \n   def usage\n+    def fmt(prefix, str)\n+      return str ? prefix + str : nil\n+    end\n     puts 'usage: %s' % @usage\n     @list.values.uniq.each do |opt|\n       s = '    '\n-      s << [opt.short, opt.long].compact.join(', ')\n+      s << ''\n+      s << [fmt('-', opt.short), fmt('--', opt.long)].compact.join(', ')\n+      s << ''\n       s << '%*s%s' % [26 - s.size, '', opt.help] if opt.help\n       puts s\n     end\n@@ -118,21 +118,18 @@ class SimpleParser\n \n end\n \n-opts = SimpleParser.new\n-opts.usage = 'usage: git related [options] <files | rev-list options>'\n+opts = ParseOpt.new\n+opts.usage = 'git related [options] <files | rev-list options>'\n \n-opts.on('-p', '--min-percent',\n-        'Minium percentage of role participation') do |v|\n+opts.on('p', 'min-percent', 'Minium percentage of role participation') do |v|\n   $min_percent = v.to_i\n end\n \n-opts.on('-d', '--since',\n-        'How far back to search for relevant commits') do |v|\n+opts.on('d', 'since', 'How far back to search for relevant commits') do |v|\n   $since = v\n end\n \n-opts.on('-c', '--commits', [:raw, :full],\n-        'List commits instead of persons') do |v|\n+opts.on('c', 'commits', 'List commits instead of persons') do |v|\n   $show_commits = v\n end\n \n@@ -207,19 +204,19 @@ class Commit\n   end\n \n   def parse(data)\n-    msg = author = nil\n+    in_body = author = nil\n     data.each_line do |line|\n-      if not msg\n+      if not in_body\n         case line\n         when /^author ([^<>]+) <(\\S+)> (.+)$/\n           author = Persons.get($1, $2)\n           author.add_role(@id, :author)\n         when /^$/\n-          msg = true\n+          in_body = true\n         end\n       else\n         role_regex = KNOWN_ROLES.keys.join('|')\n-        if line =~ /^(#{role_regex})-by: ([^<>]+) <(\\S+?)>$/\n+        if line =~ /^(#{role_regex}): ([^<>]+) <(\\S+?)>$/\n           person = Persons.get($2, $3)\n           role = KNOWN_ROLES[$1]\n           next if role == :signer and person == author\n@@ -276,29 +273,32 @@ class Commits\n       p.each do |line|\n         if line =~ /^\\h{40}/\n           id = $&\n-          @items[id] = Commit.new(id) if not @main_commits.include?(id)\n+          @items[id] ||= Commit.new(id) if not @main_commits.include?(id)\n         end\n       end\n     end\n     Dir.chdir($cur_dir)\n   end\n \n-  def from_patches(files)\n+  def scan_patch(f, id = nil)\n     source = nil\n+    f.each do |line|\n+      case line\n+      when /^From (\\h+) (.+)$/\n+        id = $1\n+        @main_commits[id] = true\n+      when /^---\\s+(\\S+)/\n+        source = $1 != '/dev/null' ? $1[2..-1] : nil\n+      when /^@@ -(\\d+)(?:,(\\d+))?/\n+        get_blame(source, $1, $2, id) if source and id\n+      end\n+    end\n+  end\n+\n+  def from_patches(files)\n     files.each do |file|\n-      from = nil\n       File.open(file) do |f|\n-        f.each do |line|\n-          case line\n-          when /^From (\\h+) (.+)$/\n-            from = $1\n-            @main_commits[from] = true\n-          when /^---\\s+(\\S+)/\n-            source = $1 != '/dev/null' ? $1[2..-1] : nil\n-          when /^@@ -(\\d+)(?:,(\\d+))?/\n-            get_blame(source, $1, $2, from) if source and from\n-          end\n-        end\n+        scan_patch(f)\n       end\n     end\n   end\n@@ -319,20 +319,12 @@ class Commits\n       args = revs\n     end\n \n-    source = nil\n     File.popen(%w[git rev-list --reverse] + args) do |p|\n       p.each do |e|\n         id = e.chomp\n         @main_commits[id] = true\n         File.popen(%w[git show -C --oneline] + [id]) do |p|\n-          p.each do |e|\n-            case e\n-            when /^---\\s+(\\S+)/\n-              source = $1 != '/dev/null' ? $1[2..-1] : nil\n-            when /^@@ -(\\d+)(?:,(\\d+))?/\n-              get_blame(source, $1, $2, id) if source\n-            end\n-          end\n+          scan_patch(p, id)\n         end\n       end\n     end\n@@ -359,9 +351,9 @@ commits.import\n if $show_commits\n   cmd = nil\n   case $show_commits\n-  when :raw\n+  when 'raw'\n     puts commits.list\n-  when :full\n+  when 'full'\n     cmd = %w[git log --patch --no-walk]\n   else\n     cmd = %w[git log --oneline --no-walk]\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nnew file mode 100755\nindex 0000000..f357e30\n--- /dev/null\n+++ b/contrib/related/test-related.t\n@@ -0,0 +1,107 @@\n+#!/bin/sh\n+\n+test_description=\"Test git related\"\n+\n+test -z \"$TEST_DIRECTORY\" && TEST_DIRECTORY=\"$PWD/../../t\"\n+. \"$TEST_DIRECTORY\"/test-lib.sh\n+\n+setup() {\n+\tgit init &&\n+\techo one > content &&\n+\tgit add content &&\n+\tgit commit -q -m one --author='Pablo Escobar <pablo@escobar.com>' &&\n+\techo two >> content &&\n+\tgit commit -q -a -m one --author='Jon Stewart <jon@stewart.com>' &&\n+\techo three >> content &&\n+\tgit commit -q -a -m three --author='John Doe <john@doe.com>' &&\n+\techo four >> content &&\n+\tgit branch basic &&\n+\tgit commit -q -a -F - --author='John Poppins <john@doe.com>' <<-EOF &&\n+\tfour\n+\n+\tReviewed-by: Jon Stewart <jon@stewart.com>\n+\tEOF\n+\techo five >> content &&\n+\tgit commit -q -a -m five --author='Mary Poppins <mary@yahoo.com.uk>'\n+\tgit checkout -b next &&\n+\techo six >> content &&\n+\tgit commit -q -a -m six --author='Ocatio Paz <octavio.paz@gmail.com>'\n+}\n+\n+setup\n+\n+test_expect_success \"basic\" \"\n+\tgit format-patch --stdout -1 basic > patch &&\n+\tgit related patch | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJon Stewart <jon@stewart.com> (author: 50%)\n+\tPablo Escobar <pablo@escobar.com> (author: 50%)\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n+test_expect_success \"others\" \"\n+\tgit format-patch --stdout -1 master > patch &&\n+\tgit related patch | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Poppins <john@doe.com> (author: 66%)\n+\tJon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n+test_expect_success \"multiple patches\" \"\n+\tgit format-patch --stdout -1 master > patch1 &&\n+\tgit format-patch --stdout -1 master^ > patch2 &&\n+\tgit related patch1 patch2 | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Poppins <john@doe.com> (author: 50%)\n+\tJon Stewart <jon@stewart.com> (reviewer: 25%, author: 25%)\n+\tPablo Escobar <pablo@escobar.com> (author: 25%)\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n+test_expect_success \"from committish\" \"\n+\tgit related -1 master | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Poppins <john@doe.com> (author: 66%)\n+\tJon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n+test_expect_success \"from single rev committish\" \"\n+\tgit related -1 master | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Poppins <john@doe.com> (author: 66%)\n+\tJon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n+test_expect_success \"mailmap\" \"\n+\ttest_when_finished 'rm -rf .mailmap' &&\n+\tcat > .mailmap <<-EOF &&\n+\tJon McAvoy <jon@stewart.com>\n+\tJohn Poppins <john@poppins.com> <john@doe.com>\n+\tEOF\n+\tgit related -1 master | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Poppins <john@poppins.com> (author: 66%)\n+\tJon McAvoy <jon@stewart.com> (reviewer: 33%, author: 33%)\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n+test_expect_success \"commits\" \"\n+\tgit related -craw -1 master | git log --format='%s' --no-walk --stdin > actual &&\n+\tcat > expected <<-EOF &&\n+\tfour\n+\tthree\n+\tone\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n+test_done\n\nFelipe Contreras (15):\n  Add new git-related helper to contrib\n  contrib: related: add tests\n  contrib: related: add support for multiple patches\n  contrib: related: add option to parse from committish\n  contrib: related: parse committish like format-patch\n  contrib: related: print the amount of involvement\n  contrib: related: add helper Person classes\n  contrib: related: show role count\n  contrib: related: add support for more roles\n  contrib: related: group persons with same email\n  contrib: related: allow usage on other directories\n  contrib: related: add mailmap support\n  contrib: related: add option parsing\n  contrib: related: add option to show commits\n  contrib: related: add README\n\n contrib/related/Makefile       |  17 ++\n contrib/related/README         |  65 +++++++\n contrib/related/git-related    | 377 +++++++++++++++++++++++++++++++++++++++++\n contrib/related/test-related.t | 107 ++++++++++++\n 4 files changed, 566 insertions(+)\n create mode 100644 contrib/related/Makefile\n create mode 100644 contrib/related/README\n create mode 100755 contrib/related/git-related\n create mode 100755 contrib/related/test-related.t\n\n-- \n1.8.4-fc\n"},{"id":"228827","messageId":"1381561584-20529-3-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 07/15] contrib: related: add helper Person classes","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:11Z","receivedAt":"2013-10-12T07:06:11Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"No functional changes.\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related | 71 +++++++++++++++++++++++++++++++++++----------\n 1 file changed, 56 insertions(+), 15 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex ffce839..3cac925 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -8,13 +8,59 @@ $min_percent = 10\n $files = []\n $rev_args = []\n \n-class Commit\n+class Person\n+\n+  attr_reader :roles\n+\n+  def initialize(name, email)\n+    @name = name\n+    @email = email\n+    @commits = {}\n+  end\n+\n+  def add_role(commit)\n+    @commits[commit] = true\n+  end\n+\n+  def <=>(b)\n+    self.size <=> b.size\n+  end\n+\n+  def size\n+    @commits.size\n+  end\n+\n+  def to_s\n+    '%s <%s>' % [@name, @email]\n+  end\n+\n+end\n \n-  attr_reader :persons\n+class Persons\n+\n+  @@index = {}\n+\n+  include Enumerable\n+\n+  def each(&block)\n+    @@index.values.each(&block)\n+  end\n+\n+  def self.get(name, email)\n+    id = [name, email]\n+    person = @@index[id]\n+    if not person\n+      person = @@index[id] = Person.new(name, email)\n+    end\n+    person\n+  end\n+\n+end\n+\n+class Commit\n \n   def initialize(id)\n     @id = id\n-    @persons = []\n   end\n \n   def parse(data)\n@@ -23,17 +69,18 @@ class Commit\n       if not in_body\n         case line\n         when /^author ([^<>]+) <(\\S+)> (.+)$/\n-          @persons << '%s <%s>' % [$1, $2]\n+          author = Persons.get($1, $2)\n+          author.add_role(@id)\n         when /^$/\n           in_body = true\n         end\n       else\n         if line =~ /^(Signed-off-by|Reviewed-by|Acked-by|Cc): ([^<>]+) <(\\S+?)>$/\n-          @persons << '%s <%s>' % [$2, $3]\n+          person = Persons.get($2, $3)\n+          person.add_role(@id)\n         end\n       end\n     end\n-    @persons.uniq!\n   end\n \n end\n@@ -152,16 +199,10 @@ else\n end\n commits.import\n \n-count_per_person = Hash.new(0)\n-\n-commits.each do |id, commit|\n-  commit.persons.each do |person|\n-    count_per_person[person] += 1\n-  end\n-end\n+persons = Persons.new\n \n-count_per_person.each do |person, count|\n-  percent = count.to_f * 100 / commits.size\n+persons.sort.reverse.each do |person|\n+  percent = person.size.to_f * 100 / commits.size\n   next if percent < $min_percent\n   puts '%s (involved: %u%%)' % [person, percent]\n end\n-- \n1.8.4-fc\n"},{"id":"228825","messageId":"1381561584-20529-4-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 10/15] contrib: related: group persons with same email","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:12Z","receivedAt":"2013-10-12T07:06:12Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Suggested-by: Duy Nguyen <pclouds@gmail.com>\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related    |  2 +-\n contrib/related/test-related.t | 12 ++++--------\n 2 files changed, 5 insertions(+), 9 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex f94f5f4..3399307 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -56,7 +56,7 @@ class Persons\n   end\n \n   def self.get(name, email)\n-    id = [name, email]\n+    id = email.downcase\n     person = @@index[id]\n     if not person\n       person = @@index[id] = Person.new(name, email)\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nindex 9da2693..a19ad32 100755\n--- a/contrib/related/test-related.t\n+++ b/contrib/related/test-related.t\n@@ -44,8 +44,7 @@ test_expect_success \"others\" \"\n \tgit format-patch --stdout -1 master > patch &&\n \tgit related patch | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com> (author: 33%)\n-\tJohn Poppins <john@doe.com> (author: 33%)\n+\tJohn Poppins <john@doe.com> (author: 66%)\n \tJon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n \tEOF\n \ttest_cmp expected actual\n@@ -56,8 +55,7 @@ test_expect_success \"multiple patches\" \"\n \tgit format-patch --stdout -1 master^ > patch2 &&\n \tgit related patch1 patch2 | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com> (author: 25%)\n-\tJohn Poppins <john@doe.com> (author: 25%)\n+\tJohn Poppins <john@doe.com> (author: 50%)\n \tJon Stewart <jon@stewart.com> (reviewer: 25%, author: 25%)\n \tPablo Escobar <pablo@escobar.com> (author: 25%)\n \tEOF\n@@ -67,8 +65,7 @@ test_expect_success \"multiple patches\" \"\n test_expect_success \"from committish\" \"\n \tgit related -1 master | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com> (author: 33%)\n-\tJohn Poppins <john@doe.com> (author: 33%)\n+\tJohn Poppins <john@doe.com> (author: 66%)\n \tJon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n \tEOF\n \ttest_cmp expected actual\n@@ -77,8 +74,7 @@ test_expect_success \"from committish\" \"\n test_expect_success \"from single rev committish\" \"\n \tgit related -1 master | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com> (author: 33%)\n-\tJohn Poppins <john@doe.com> (author: 33%)\n+\tJohn Poppins <john@doe.com> (author: 66%)\n \tJon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n \tEOF\n \ttest_cmp expected actual\n-- \n1.8.4-fc\n"},{"id":"228824","messageId":"1381561584-20529-5-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 02/15] contrib: related: add tests","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:13Z","receivedAt":"2013-10-12T07:06:13Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/Makefile       | 17 +++++++++++++\n contrib/related/test-related.t | 54 ++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 71 insertions(+)\n create mode 100644 contrib/related/Makefile\n create mode 100755 contrib/related/test-related.t\n\ndiff --git a/contrib/related/Makefile b/contrib/related/Makefile\nnew file mode 100644\nindex 0000000..1224fce\n--- /dev/null\n+++ b/contrib/related/Makefile\n@@ -0,0 +1,17 @@\n+TESTS := $(wildcard test-*.t)\n+\n+export T := $(addprefix $(CURDIR)/,$(TESTS))\n+export MAKE := $(MAKE) -e\n+export PATH := $(CURDIR):$(PATH)\n+export TEST_LINT := test-lint-executable test-lint-shell-syntax\n+export TEST_DIRECTORY := $(CURDIR)/../../t\n+\n+all:\n+\n+test:\n+\t$(MAKE) -C ../../t $@\n+\n+$(TESTS): all\n+\t$(MAKE) -C ../../t $(CURDIR)/$@\n+\n+.PHONY: all test $(TESTS)\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nnew file mode 100755\nindex 0000000..b69684d\n--- /dev/null\n+++ b/contrib/related/test-related.t\n@@ -0,0 +1,54 @@\n+#!/bin/sh\n+\n+test_description=\"Test git related\"\n+\n+test -z \"$TEST_DIRECTORY\" && TEST_DIRECTORY=\"$PWD/../../t\"\n+. \"$TEST_DIRECTORY\"/test-lib.sh\n+\n+setup() {\n+\tgit init &&\n+\techo one > content &&\n+\tgit add content &&\n+\tgit commit -q -m one --author='Pablo Escobar <pablo@escobar.com>' &&\n+\techo two >> content &&\n+\tgit commit -q -a -m one --author='Jon Stewart <jon@stewart.com>' &&\n+\techo three >> content &&\n+\tgit commit -q -a -m three --author='John Doe <john@doe.com>' &&\n+\techo four >> content &&\n+\tgit branch basic &&\n+\tgit commit -q -a -F - --author='John Poppins <john@doe.com>' <<-EOF &&\n+\tfour\n+\n+\tReviewed-by: Jon Stewart <jon@stewart.com>\n+\tEOF\n+\techo five >> content &&\n+\tgit commit -q -a -m five --author='Mary Poppins <mary@yahoo.com.uk>'\n+\tgit checkout -b next &&\n+\techo six >> content &&\n+\tgit commit -q -a -m six --author='Ocatio Paz <octavio.paz@gmail.com>'\n+}\n+\n+setup\n+\n+test_expect_success \"basic\" \"\n+\tgit format-patch --stdout -1 basic > patch &&\n+\tgit related patch | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJon Stewart <jon@stewart.com>\n+\tPablo Escobar <pablo@escobar.com>\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n+test_expect_success \"others\" \"\n+\tgit format-patch --stdout -1 master > patch &&\n+\tgit related patch | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Doe <john@doe.com>\n+\tJohn Poppins <john@doe.com>\n+\tJon Stewart <jon@stewart.com>\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n+test_done\n-- \n1.8.4-fc\n"},{"id":"228826","messageId":"1381561584-20529-6-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 06/15] contrib: related: print the amount of involvement","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:14Z","receivedAt":"2013-10-12T07:06:14Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"100% means the person was involved in all the commits, in one way or the\nother.\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related    |  2 +-\n contrib/related/test-related.t | 30 +++++++++++++++---------------\n 2 files changed, 16 insertions(+), 16 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 80e1f17..ffce839 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -163,5 +163,5 @@ end\n count_per_person.each do |person, count|\n   percent = count.to_f * 100 / commits.size\n   next if percent < $min_percent\n-  puts person\n+  puts '%s (involved: %u%%)' % [person, percent]\n end\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nindex 39b4fe9..ec2680a 100755\n--- a/contrib/related/test-related.t\n+++ b/contrib/related/test-related.t\n@@ -34,8 +34,8 @@ test_expect_success \"basic\" \"\n \tgit format-patch --stdout -1 basic > patch &&\n \tgit related patch | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJon Stewart <jon@stewart.com>\n-\tPablo Escobar <pablo@escobar.com>\n+\tJon Stewart <jon@stewart.com> (involved: 50%)\n+\tPablo Escobar <pablo@escobar.com> (involved: 50%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -44,9 +44,9 @@ test_expect_success \"others\" \"\n \tgit format-patch --stdout -1 master > patch &&\n \tgit related patch | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com>\n-\tJohn Poppins <john@doe.com>\n-\tJon Stewart <jon@stewart.com>\n+\tJohn Doe <john@doe.com> (involved: 33%)\n+\tJohn Poppins <john@doe.com> (involved: 33%)\n+\tJon Stewart <jon@stewart.com> (involved: 66%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -56,10 +56,10 @@ test_expect_success \"multiple patches\" \"\n \tgit format-patch --stdout -1 master^ > patch2 &&\n \tgit related patch1 patch2 | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com>\n-\tJohn Poppins <john@doe.com>\n-\tJon Stewart <jon@stewart.com>\n-\tPablo Escobar <pablo@escobar.com>\n+\tJohn Doe <john@doe.com> (involved: 25%)\n+\tJohn Poppins <john@doe.com> (involved: 25%)\n+\tJon Stewart <jon@stewart.com> (involved: 50%)\n+\tPablo Escobar <pablo@escobar.com> (involved: 25%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -67,9 +67,9 @@ test_expect_success \"multiple patches\" \"\n test_expect_success \"from committish\" \"\n \tgit related -1 master | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com>\n-\tJohn Poppins <john@doe.com>\n-\tJon Stewart <jon@stewart.com>\n+\tJohn Doe <john@doe.com> (involved: 33%)\n+\tJohn Poppins <john@doe.com> (involved: 33%)\n+\tJon Stewart <jon@stewart.com> (involved: 66%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -77,9 +77,9 @@ test_expect_success \"from committish\" \"\n test_expect_success \"from single rev committish\" \"\n \tgit related -1 master | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com>\n-\tJohn Poppins <john@doe.com>\n-\tJon Stewart <jon@stewart.com>\n+\tJohn Doe <john@doe.com> (involved: 33%)\n+\tJohn Poppins <john@doe.com> (involved: 33%)\n+\tJon Stewart <jon@stewart.com> (involved: 66%)\n \tEOF\n \ttest_cmp expected actual\n \"\n-- \n1.8.4-fc\n"},{"id":"228831","messageId":"1381561584-20529-7-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 04/15] contrib: related: add option to parse from committish","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:15Z","receivedAt":"2013-10-12T07:06:15Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"For example master..feature-a.\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related    | 57 +++++++++++++++++++++++++++++++-----------\n contrib/related/test-related.t | 10 ++++++++\n 2 files changed, 53 insertions(+), 14 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 7e79d78..4f78304 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -1,10 +1,12 @@\n #!/usr/bin/env ruby\n \n # This script finds people that might be interested in a patch\n-# usage: git related <files>\n+# usage: git related <files | rev-list options>\n \n $since = '5-years-ago'\n $min_percent = 10\n+$files = []\n+$rev_args = []\n \n class Commit\n \n@@ -82,21 +84,36 @@ class Commits\n     end\n   end\n \n-  def from_patches(files)\n+  def scan_patch(f, id = nil)\n     source = nil\n+    f.each do |line|\n+      case line\n+      when /^From (\\h+) (.+)$/\n+        id = $1\n+        @main_commits[id] = true\n+      when /^---\\s+(\\S+)/\n+        source = $1 != '/dev/null' ? $1[2..-1] : nil\n+      when /^@@ -(\\d+)(?:,(\\d+))?/\n+        get_blame(source, $1, $2, id) if source and id\n+      end\n+    end\n+  end\n+\n+  def from_patches(files)\n     files.each do |file|\n-      from = nil\n       File.open(file) do |f|\n-        f.each do |line|\n-          case line\n-          when /^From (\\h+) (.+)$/\n-            from = $1\n-            @main_commits[from] = true\n-          when /^---\\s+(\\S+)/\n-            source = $1 != '/dev/null' ? $1[2..-1] : nil\n-          when /^@@ -(\\d+)(?:,(\\d+))?/\n-            get_blame(source, $1, $2, from) if source and from\n-          end\n+        scan_patch(f)\n+      end\n+    end\n+  end\n+\n+  def from_rev_args(args)\n+    File.popen(%w[git rev-list --reverse] + args) do |p|\n+      p.each do |e|\n+        id = e.chomp\n+        @main_commits[id] = true\n+        File.popen(%w[git show -C --oneline] + [id]) do |p|\n+          scan_patch(p, id)\n         end\n       end\n     end\n@@ -104,8 +121,20 @@ class Commits\n \n end\n \n+ARGV.each do |e|\n+  if File.exists?(e)\n+    $files << e\n+  else\n+    $rev_args << e\n+  end\n+end\n+\n commits = Commits.new\n-commits.from_patches(ARGV)\n+if $files.empty?\n+  commits.from_rev_args($rev_args)\n+else\n+  commits.from_patches($files)\n+end\n commits.import\n \n count_per_person = Hash.new(0)\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nindex 8102b3c..b623d69 100755\n--- a/contrib/related/test-related.t\n+++ b/contrib/related/test-related.t\n@@ -64,4 +64,14 @@ test_expect_success \"multiple patches\" \"\n \ttest_cmp expected actual\n \"\n \n+test_expect_success \"from committish\" \"\n+\tgit related -1 master | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Doe <john@doe.com>\n+\tJohn Poppins <john@doe.com>\n+\tJon Stewart <jon@stewart.com>\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n test_done\n-- \n1.8.4-fc\n"},{"id":"228828","messageId":"1381561584-20529-8-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 15/15] contrib: related: add README","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:16Z","receivedAt":"2013-10-12T07:06:16Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/README | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 65 insertions(+)\n create mode 100644 contrib/related/README\n\ndiff --git a/contrib/related/README b/contrib/related/README\nnew file mode 100644\nindex 0000000..26188ca\n--- /dev/null\n+++ b/contrib/related/README\n@@ -0,0 +1,65 @@\n+= git-related =\n+\n+This tool finds people that might be interested in a patch, by going\n+back through the history for each single hunk modified, and finding\n+people that reviewed, acknowledged, signed, or authored the code the\n+patch is modifying.\n+\n+It does this by running `git blame` incrementally on each hunk, and then\n+parsing the commit message. After gathering all the relevant people, it\n+groups them to show what exactly was their role when the participated in\n+the development of the relevant commit, and on how many relevant commits\n+they participated. They are only displayed if they pass a minimum\n+threshold of participation.\n+\n+For example:\n+\n+------------\n+% git related master..fc/transport/improv\n+Junio C Hamano <gitster@pobox.com> (signer: 90%, author: 5%)\n+Felipe Contreras <felipe.contreras@gmail.com> (author: 25%, reviewer: 2%)\n+Sverre Rabbelier <srabbelier@gmail.com> (author: 17%, acker: 2%, signer: 7%)\n+Jeff King <peff@peff.net> (acker: 17%, author: 10%)\n+Shawn O. Pearce <spearce@spearce.org> (author: 5%, signer: 2%, cced: 2%)\n+Elijah Newren <newren@gmail.com> (author: 10%)\n+------------\n+\n+In addition, it has an option to output the list of commits, instead of the\n+contributors, which allows you to easily find out the previous changes to the\n+lines your patches modify.\n+\n+------------\n+% git related -c master..fc/transport/improv\n+99d9ec0 Merge branch 'fc/transport-helper-no-refspec'\n+67c9c78 transport-helper: barf when user tries old:new\n+0460ed2 documentation: trivial style cleanups\n+126aac5 transport-helper: fix remote helper namespace regression\n+21610d8 transport-helper: clarify pushing without refspecs\n+a93b4a0 transport-helper: warn when refspec is not used\n+664059f transport-helper: update remote helper namespace\n+c4458ec fast-export: Allow pruned-references in mark file\n+...\n+------------\n+\n+Moreover, when sending patches for review, you can configure `git send-email`\n+to use `git related` to find relevant people that should be Cc'ed:\n+\n+------------\n+% git send-email --cc-cmd='git related' *.patch\n+------------\n+\n+== Installation ==\n+\n+To use this script, simply put `git-related` in your `$PATH` and make sure it\n+has executable permissions:\n+\n+------------\n+wget https://raw.github.com/felipec/git-related/master/git-related -O ~/bin/git-related\n+chmod +x ~/bin/git-related\n+------------\n+\n+And make sure `~/bin` is in your `$PATH`:\n+\n+------------\n+export PATH=\"$HOME/bin:$PATH\"\n+------------\n-- \n1.8.4-fc\n"},{"id":"228829","messageId":"1381561584-20529-9-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 09/15] contrib: related: add support for more roles","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:17Z","receivedAt":"2013-10-12T07:06:17Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related    | 14 ++++++++++++--\n contrib/related/test-related.t |  8 ++++----\n 2 files changed, 16 insertions(+), 6 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 04c56f5..f94f5f4 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -8,6 +8,13 @@ $min_percent = 10\n $files = []\n $rev_args = []\n \n+KNOWN_ROLES = {\n+  'Signed-off-by' => :signer,\n+  'Reviewed-by' => :reviewer,\n+  'Acked-by' => :acker,\n+  'Cc' => :cced,\n+}\n+\n class Person\n \n   attr_reader :roles\n@@ -77,9 +84,12 @@ class Commit\n           in_body = true\n         end\n       else\n-        if line =~ /^(Signed-off-by|Reviewed-by|Acked-by|Cc): ([^<>]+) <(\\S+?)>$/\n+        role_regex = KNOWN_ROLES.keys.join('|')\n+        if line =~ /^(#{role_regex}): ([^<>]+) <(\\S+?)>$/\n           person = Persons.get($2, $3)\n-          person.add_role(@id, :signer) if person != author\n+          role = KNOWN_ROLES[$1]\n+          next if role == :signer and person == author\n+          person.add_role(@id, role)\n         end\n       end\n     end\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nindex 90cc516..9da2693 100755\n--- a/contrib/related/test-related.t\n+++ b/contrib/related/test-related.t\n@@ -46,7 +46,7 @@ test_expect_success \"others\" \"\n \tcat > expected <<-EOF &&\n \tJohn Doe <john@doe.com> (author: 33%)\n \tJohn Poppins <john@doe.com> (author: 33%)\n-\tJon Stewart <jon@stewart.com> (signer: 33%, author: 33%)\n+\tJon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -58,7 +58,7 @@ test_expect_success \"multiple patches\" \"\n \tcat > expected <<-EOF &&\n \tJohn Doe <john@doe.com> (author: 25%)\n \tJohn Poppins <john@doe.com> (author: 25%)\n-\tJon Stewart <jon@stewart.com> (signer: 25%, author: 25%)\n+\tJon Stewart <jon@stewart.com> (reviewer: 25%, author: 25%)\n \tPablo Escobar <pablo@escobar.com> (author: 25%)\n \tEOF\n \ttest_cmp expected actual\n@@ -69,7 +69,7 @@ test_expect_success \"from committish\" \"\n \tcat > expected <<-EOF &&\n \tJohn Doe <john@doe.com> (author: 33%)\n \tJohn Poppins <john@doe.com> (author: 33%)\n-\tJon Stewart <jon@stewart.com> (signer: 33%, author: 33%)\n+\tJon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -79,7 +79,7 @@ test_expect_success \"from single rev committish\" \"\n \tcat > expected <<-EOF &&\n \tJohn Doe <john@doe.com> (author: 33%)\n \tJohn Poppins <john@doe.com> (author: 33%)\n-\tJon Stewart <jon@stewart.com> (signer: 33%, author: 33%)\n+\tJon Stewart <jon@stewart.com> (reviewer: 33%, author: 33%)\n \tEOF\n \ttest_cmp expected actual\n \"\n-- \n1.8.4-fc\n"},{"id":"228830","messageId":"1381561584-20529-10-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 03/15] contrib: related: add support for multiple patches","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:18Z","receivedAt":"2013-10-12T07:06:18Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related    | 35 +++++++++++++++++++----------------\n contrib/related/test-related.t | 13 +++++++++++++\n 2 files changed, 32 insertions(+), 16 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 66c1009..7e79d78 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -1,7 +1,7 @@\n #!/usr/bin/env ruby\n \n # This script finds people that might be interested in a patch\n-# usage: git related <file>\n+# usage: git related <files>\n \n $since = '5-years-ago'\n $min_percent = 10\n@@ -40,6 +40,7 @@ class Commits\n \n   def initialize\n     @items = {}\n+    @main_commits = {}\n   end\n \n   def size\n@@ -75,23 +76,27 @@ class Commits\n       p.each do |line|\n         if line =~ /^\\h{40}/\n           id = $&\n-          @items[id] ||= Commit.new(id)\n+          @items[id] ||= Commit.new(id) if not @main_commits.include?(id)\n         end\n       end\n     end\n   end\n \n-  def from_patch(file)\n-    from = source = nil\n-    File.open(file) do |f|\n-      f.each do |line|\n-        case line\n-        when /^From (\\h+) (.+)$/\n-          from = $1\n-        when /^---\\s+(\\S+)/\n-          source = $1 != '/dev/null' ? $1[2..-1] : nil\n-        when /^@@ -(\\d+)(?:,(\\d+))?/\n-          get_blame(source, $1, $2, from) if source and from\n+  def from_patches(files)\n+    source = nil\n+    files.each do |file|\n+      from = nil\n+      File.open(file) do |f|\n+        f.each do |line|\n+          case line\n+          when /^From (\\h+) (.+)$/\n+            from = $1\n+            @main_commits[from] = true\n+          when /^---\\s+(\\S+)/\n+            source = $1 != '/dev/null' ? $1[2..-1] : nil\n+          when /^@@ -(\\d+)(?:,(\\d+))?/\n+            get_blame(source, $1, $2, from) if source and from\n+          end\n         end\n       end\n     end\n@@ -99,10 +104,8 @@ class Commits\n \n end\n \n-exit 1 if ARGV.size != 1\n-\n commits = Commits.new\n-commits.from_patch(ARGV[0])\n+commits.from_patches(ARGV)\n commits.import\n \n count_per_person = Hash.new(0)\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nindex b69684d..8102b3c 100755\n--- a/contrib/related/test-related.t\n+++ b/contrib/related/test-related.t\n@@ -51,4 +51,17 @@ test_expect_success \"others\" \"\n \ttest_cmp expected actual\n \"\n \n+test_expect_success \"multiple patches\" \"\n+\tgit format-patch --stdout -1 master > patch1 &&\n+\tgit format-patch --stdout -1 master^ > patch2 &&\n+\tgit related patch1 patch2 | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Doe <john@doe.com>\n+\tJohn Poppins <john@doe.com>\n+\tJon Stewart <jon@stewart.com>\n+\tPablo Escobar <pablo@escobar.com>\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n test_done\n-- \n1.8.4-fc\n"},{"id":"228832","messageId":"1381561584-20529-11-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 12/15] contrib: related: add mailmap support","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:19Z","receivedAt":"2013-10-12T07:06:19Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related    | 37 +++++++++++++++++++++++++++++++++++++\n contrib/related/test-related.t | 14 ++++++++++++++\n 2 files changed, 51 insertions(+)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex f15e4e7..6ab74c7 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -8,6 +8,9 @@ $min_percent = 10\n $files = []\n $rev_args = []\n \n+$mailmaps = {}\n+$mailmaps_complex = {}\n+\n git_dir = %x[git rev-parse --git-dir].chomp\n $base_dir = File.dirname(git_dir)\n $cur_dir = Dir.pwd\n@@ -19,6 +22,29 @@ KNOWN_ROLES = {\n   'Cc' => :cced,\n }\n \n+def get_mailmap(filename)\n+  return unless File.exists?(filename)\n+  File.open(filename) do |f|\n+    f.each do |line|\n+      case line.gsub(/\\s*#.*$/, '')\n+      when /^([^<>]+)\\s+<(\\S+)>$/\n+        $mailmaps[$2] = [ $1, nil ]\n+      when /^<(\\S+)>\\s+<(\\S+)>$/\n+        $mailmaps[$2] = [ nil, $1 ]\n+      when /^([^<>]+)\\s+<(\\S+)>\\s+<(\\S+)>$/\n+        $mailmaps[$3] = [ $1, $2 ]\n+      when /^([^<>]+)\\s+<(\\S+)>\\s+([^<>]+)\\s+<(\\S+)>$/\n+        $mailmaps_complex[[$3, $4]] = [ $1, $2 ]\n+      end\n+    end\n+  end\n+end\n+\n+get_aliases if $get_aliases\n+get_mailmap(File.join($base_dir, '.mailmap'))\n+mailmap_file = %x[git config mailmap.file].chomp\n+get_mailmap(mailmap_file)\n+\n class Person\n \n   attr_reader :roles\n@@ -60,6 +86,17 @@ class Persons\n   end\n \n   def self.get(name, email)\n+\n+    # fix with mailmap\n+    person = [name, email]\n+    new = nil\n+    new = $mailmaps_complex[person] if not new and $mailmaps_complex.include?(person)\n+    new = $mailmaps[email] if not new and $mailmaps.include?(email)\n+    if new\n+      name = new[0] if new[0]\n+      email = new[1] if new[1]\n+    end\n+\n     id = email.downcase\n     person = @@index[id]\n     if not person\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nindex a19ad32..fa8c1a7 100755\n--- a/contrib/related/test-related.t\n+++ b/contrib/related/test-related.t\n@@ -80,4 +80,18 @@ test_expect_success \"from single rev committish\" \"\n \ttest_cmp expected actual\n \"\n \n+test_expect_success \"mailmap\" \"\n+\ttest_when_finished 'rm -rf .mailmap' &&\n+\tcat > .mailmap <<-EOF &&\n+\tJon McAvoy <jon@stewart.com>\n+\tJohn Poppins <john@poppins.com> <john@doe.com>\n+\tEOF\n+\tgit related -1 master | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Poppins <john@poppins.com> (author: 66%)\n+\tJon McAvoy <jon@stewart.com> (reviewer: 33%, author: 33%)\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n test_done\n-- \n1.8.4-fc\n"},{"id":"228833","messageId":"1381561584-20529-12-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 13/15] contrib: related: add option parsing","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:20Z","receivedAt":"2013-10-12T07:06:20Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related | 85 +++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 85 insertions(+)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 6ab74c7..d6b44c7 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -45,6 +45,91 @@ get_mailmap(File.join($base_dir, '.mailmap'))\n mailmap_file = %x[git config mailmap.file].chomp\n get_mailmap(mailmap_file)\n \n+class ParseOpt\n+  attr_writer :usage\n+\n+  class Option\n+    attr_reader :short, :long, :help\n+\n+    def initialize(short, long, help, &block)\n+      @block = block\n+      @short = short\n+      @long = long\n+      @help = help\n+    end\n+\n+    def call(v)\n+      @block.call(v)\n+    end\n+  end\n+\n+  def initialize\n+    @list = {}\n+  end\n+\n+  def on(short = nil, long = nil, help = nil, &block)\n+    opt = Option.new(short, long, help, &block)\n+    @list[short] = opt if short\n+    @list[long] = opt if long\n+  end\n+\n+  def parse\n+    if ARGV.member?('-h') or ARGV.member?('--help')\n+      usage\n+      exit 0\n+    end\n+    seen_dash = false\n+    ARGV.delete_if do |cur|\n+      opt = val = nil\n+      next false if cur[0] != '-' or seen_dash\n+      case cur\n+      when '--'\n+        seen_dash = true\n+        next true\n+      when /^--no-(.+)$/\n+        opt = @list[$1]\n+        val = false\n+      when /^-([^-])(.+)?$/, /^--(.+?)(?:=(.+))?$/\n+        opt = @list[$1]\n+        val = $2 || true\n+      end\n+      if opt\n+        opt.call(val)\n+        true\n+      end\n+    end\n+  end\n+\n+  def usage\n+    def fmt(prefix, str)\n+      return str ? prefix + str : nil\n+    end\n+    puts 'usage: %s' % @usage\n+    @list.values.uniq.each do |opt|\n+      s = '    '\n+      s << ''\n+      s << [fmt('-', opt.short), fmt('--', opt.long)].compact.join(', ')\n+      s << ''\n+      s << '%*s%s' % [26 - s.size, '', opt.help] if opt.help\n+      puts s\n+    end\n+  end\n+\n+end\n+\n+opts = ParseOpt.new\n+opts.usage = 'git related [options] <files | rev-list options>'\n+\n+opts.on('p', 'min-percent', 'Minium percentage of role participation') do |v|\n+  $min_percent = v.to_i\n+end\n+\n+opts.on('d', 'since', 'How far back to search for relevant commits') do |v|\n+  $since = v\n+end\n+\n+opts.parse\n+\n class Person\n \n   attr_reader :roles\n-- \n1.8.4-fc\n"},{"id":"228834","messageId":"1381561584-20529-13-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 01/15] Add new git-related helper to contrib","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:21Z","receivedAt":"2013-10-12T07:06:21Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"This script find people that might be interested in a patch, by going\nback through the history for each single hunk modified, and finding\npeople that reviewed, acknowledge, signed, or authored the code the\npatch is modifying.\n\nIt does this by running 'git blame' incrementally on each hunk, and then\nparsing the commit message. After gathering all the relevant people, it\ngroups them to show what exactly was their role when the participated in\nthe development of the relevant commit, and on how many relevant commits\nthey participated. They are only displayed if they pass a minimum\nthreshold of participation.\n\nFor example:\n\n  % git related 0001-remote-hg-trivial-cleanups.patch\n  Felipe Contreras <felipe.contreras@gmail.com>\n  Jeff King <peff@peff.net>\n  Max Horn <max@quendi.de>\n  Junio C Hamano <gitster@pobox.com>\n\nThus it can be used for 'git send-email' as a cc-cmd.\n\nThere might be some other related functions to this script, not just to\nbe used as a cc-cmd.\n\nComments-by: Ramkumar Ramachandra <artagnon@gmail.com>\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related | 120 ++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 120 insertions(+)\n create mode 100755 contrib/related/git-related\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nnew file mode 100755\nindex 0000000..66c1009\n--- /dev/null\n+++ b/contrib/related/git-related\n@@ -0,0 +1,120 @@\n+#!/usr/bin/env ruby\n+\n+# This script finds people that might be interested in a patch\n+# usage: git related <file>\n+\n+$since = '5-years-ago'\n+$min_percent = 10\n+\n+class Commit\n+\n+  attr_reader :persons\n+\n+  def initialize(id)\n+    @id = id\n+    @persons = []\n+  end\n+\n+  def parse(data)\n+    in_body = nil\n+    data.each_line do |line|\n+      if not in_body\n+        case line\n+        when /^author ([^<>]+) <(\\S+)> (.+)$/\n+          @persons << '%s <%s>' % [$1, $2]\n+        when /^$/\n+          in_body = true\n+        end\n+      else\n+        if line =~ /^(Signed-off-by|Reviewed-by|Acked-by|Cc): ([^<>]+) <(\\S+?)>$/\n+          @persons << '%s <%s>' % [$2, $3]\n+        end\n+      end\n+    end\n+    @persons.uniq!\n+  end\n+\n+end\n+\n+class Commits\n+\n+  def initialize\n+    @items = {}\n+  end\n+\n+  def size\n+    @items.size\n+  end\n+\n+  def each(&block)\n+    @items.each(&block)\n+  end\n+\n+  def import\n+    return if @items.empty?\n+    File.popen(%w[git cat-file --batch], 'r+') do |p|\n+      p.write(@items.keys.join(\"\\n\"))\n+      p.close_write\n+      p.each do |line|\n+        if line =~ /^(\\h{40}) commit (\\d+)/\n+          id, len = $1, $2\n+          data = p.read($2.to_i)\n+          @items[id].parse(data)\n+        end\n+      end\n+    end\n+  end\n+\n+  def get_blame(source, start, len, from)\n+    return if len == 0\n+    len ||= 1\n+    File.popen(['git', 'blame', '--incremental', '-C', '-C',\n+               '-L', '%u,+%u' % [start, len],\n+               '--since', $since, from + '^',\n+               '--', source]) do |p|\n+      p.each do |line|\n+        if line =~ /^\\h{40}/\n+          id = $&\n+          @items[id] ||= Commit.new(id)\n+        end\n+      end\n+    end\n+  end\n+\n+  def from_patch(file)\n+    from = source = nil\n+    File.open(file) do |f|\n+      f.each do |line|\n+        case line\n+        when /^From (\\h+) (.+)$/\n+          from = $1\n+        when /^---\\s+(\\S+)/\n+          source = $1 != '/dev/null' ? $1[2..-1] : nil\n+        when /^@@ -(\\d+)(?:,(\\d+))?/\n+          get_blame(source, $1, $2, from) if source and from\n+        end\n+      end\n+    end\n+  end\n+\n+end\n+\n+exit 1 if ARGV.size != 1\n+\n+commits = Commits.new\n+commits.from_patch(ARGV[0])\n+commits.import\n+\n+count_per_person = Hash.new(0)\n+\n+commits.each do |id, commit|\n+  commit.persons.each do |person|\n+    count_per_person[person] += 1\n+  end\n+end\n+\n+count_per_person.each do |person, count|\n+  percent = count.to_f * 100 / commits.size\n+  next if percent < $min_percent\n+  puts person\n+end\n-- \n1.8.4-fc\n"},{"id":"228835","messageId":"1381561584-20529-14-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 11/15] contrib: related: allow usage on other directories","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:22Z","receivedAt":"2013-10-12T07:06:22Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Not just the root one (of the project).\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 3399307..f15e4e7 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -8,6 +8,10 @@ $min_percent = 10\n $files = []\n $rev_args = []\n \n+git_dir = %x[git rev-parse --git-dir].chomp\n+$base_dir = File.dirname(git_dir)\n+$cur_dir = Dir.pwd\n+\n KNOWN_ROLES = {\n   'Signed-off-by' => :signer,\n   'Reviewed-by' => :reviewer,\n@@ -130,6 +134,7 @@ class Commits\n   def get_blame(source, start, len, from)\n     return if len == 0\n     len ||= 1\n+    Dir.chdir($base_dir)\n     File.popen(['git', 'blame', '--incremental', '-C', '-C',\n                '-L', '%u,+%u' % [start, len],\n                '--since', $since, from + '^',\n@@ -141,6 +146,7 @@ class Commits\n         end\n       end\n     end\n+    Dir.chdir($cur_dir)\n   end\n \n   def scan_patch(f, id = nil)\n-- \n1.8.4-fc\n"},{"id":"228836","messageId":"1381561584-20529-15-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 08/15] contrib: related: show role count","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:23Z","receivedAt":"2013-10-12T07:06:23Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Instead of showing the total involvement, show it per role: author, or\nsigner.\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related    | 18 +++++++++++++-----\n contrib/related/test-related.t | 30 +++++++++++++++---------------\n 2 files changed, 28 insertions(+), 20 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 3cac925..04c56f5 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -16,10 +16,12 @@ class Person\n     @name = name\n     @email = email\n     @commits = {}\n+    @roles = Hash.new(0)\n   end\n \n-  def add_role(commit)\n+  def add_role(commit, role)\n     @commits[commit] = true\n+    @roles[role] += 1\n   end\n \n   def <=>(b)\n@@ -64,20 +66,20 @@ class Commit\n   end\n \n   def parse(data)\n-    in_body = nil\n+    in_body = author = nil\n     data.each_line do |line|\n       if not in_body\n         case line\n         when /^author ([^<>]+) <(\\S+)> (.+)$/\n           author = Persons.get($1, $2)\n-          author.add_role(@id)\n+          author.add_role(@id, :author)\n         when /^$/\n           in_body = true\n         end\n       else\n         if line =~ /^(Signed-off-by|Reviewed-by|Acked-by|Cc): ([^<>]+) <(\\S+?)>$/\n           person = Persons.get($2, $3)\n-          person.add_role(@id)\n+          person.add_role(@id, :signer) if person != author\n         end\n       end\n     end\n@@ -204,5 +206,11 @@ persons = Persons.new\n persons.sort.reverse.each do |person|\n   percent = person.size.to_f * 100 / commits.size\n   next if percent < $min_percent\n-  puts '%s (involved: %u%%)' % [person, percent]\n+\n+  roles = person.roles.map do |role, role_count|\n+    role_percent = role_count.to_f * 100 / commits.size\n+    '%s: %u%%' % [role, role_percent]\n+  end\n+\n+  puts '%s (%s)' % [person, roles.join(', ')]\n end\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nindex ec2680a..90cc516 100755\n--- a/contrib/related/test-related.t\n+++ b/contrib/related/test-related.t\n@@ -34,8 +34,8 @@ test_expect_success \"basic\" \"\n \tgit format-patch --stdout -1 basic > patch &&\n \tgit related patch | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJon Stewart <jon@stewart.com> (involved: 50%)\n-\tPablo Escobar <pablo@escobar.com> (involved: 50%)\n+\tJon Stewart <jon@stewart.com> (author: 50%)\n+\tPablo Escobar <pablo@escobar.com> (author: 50%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -44,9 +44,9 @@ test_expect_success \"others\" \"\n \tgit format-patch --stdout -1 master > patch &&\n \tgit related patch | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com> (involved: 33%)\n-\tJohn Poppins <john@doe.com> (involved: 33%)\n-\tJon Stewart <jon@stewart.com> (involved: 66%)\n+\tJohn Doe <john@doe.com> (author: 33%)\n+\tJohn Poppins <john@doe.com> (author: 33%)\n+\tJon Stewart <jon@stewart.com> (signer: 33%, author: 33%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -56,10 +56,10 @@ test_expect_success \"multiple patches\" \"\n \tgit format-patch --stdout -1 master^ > patch2 &&\n \tgit related patch1 patch2 | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com> (involved: 25%)\n-\tJohn Poppins <john@doe.com> (involved: 25%)\n-\tJon Stewart <jon@stewart.com> (involved: 50%)\n-\tPablo Escobar <pablo@escobar.com> (involved: 25%)\n+\tJohn Doe <john@doe.com> (author: 25%)\n+\tJohn Poppins <john@doe.com> (author: 25%)\n+\tJon Stewart <jon@stewart.com> (signer: 25%, author: 25%)\n+\tPablo Escobar <pablo@escobar.com> (author: 25%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -67,9 +67,9 @@ test_expect_success \"multiple patches\" \"\n test_expect_success \"from committish\" \"\n \tgit related -1 master | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com> (involved: 33%)\n-\tJohn Poppins <john@doe.com> (involved: 33%)\n-\tJon Stewart <jon@stewart.com> (involved: 66%)\n+\tJohn Doe <john@doe.com> (author: 33%)\n+\tJohn Poppins <john@doe.com> (author: 33%)\n+\tJon Stewart <jon@stewart.com> (signer: 33%, author: 33%)\n \tEOF\n \ttest_cmp expected actual\n \"\n@@ -77,9 +77,9 @@ test_expect_success \"from committish\" \"\n test_expect_success \"from single rev committish\" \"\n \tgit related -1 master | sort > actual &&\n \tcat > expected <<-EOF &&\n-\tJohn Doe <john@doe.com> (involved: 33%)\n-\tJohn Poppins <john@doe.com> (involved: 33%)\n-\tJon Stewart <jon@stewart.com> (involved: 66%)\n+\tJohn Doe <john@doe.com> (author: 33%)\n+\tJohn Poppins <john@doe.com> (author: 33%)\n+\tJon Stewart <jon@stewart.com> (signer: 33%, author: 33%)\n \tEOF\n \ttest_cmp expected actual\n \"\n-- \n1.8.4-fc\n"},{"id":"228837","messageId":"1381561584-20529-16-git-send-email-felipe.contreras@gmail.com","threadId":"35124","inReplyTo":"1381561584-20529-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v10 05/15] contrib: related: parse committish like format-patch","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-10-12T07:06:24Z","receivedAt":"2013-10-12T07:06:24Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related    | 15 +++++++++++++++\n contrib/related/test-related.t | 10 ++++++++++\n 2 files changed, 25 insertions(+)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 4f78304..80e1f17 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -108,6 +108,21 @@ class Commits\n   end\n \n   def from_rev_args(args)\n+    revs = []\n+\n+    File.popen(%w[git rev-parse --revs-only --default HEAD --symbolic] + args).each do |rev|\n+      revs << rev.chomp\n+    end\n+\n+    case revs.size\n+    when 1\n+      r = revs[0]\n+      r = '^' + r if r[0] != '-'\n+      args = [ r, 'HEAD' ]\n+    else\n+      args = revs\n+    end\n+\n     File.popen(%w[git rev-list --reverse] + args) do |p|\n       p.each do |e|\n         id = e.chomp\ndiff --git a/contrib/related/test-related.t b/contrib/related/test-related.t\nindex b623d69..39b4fe9 100755\n--- a/contrib/related/test-related.t\n+++ b/contrib/related/test-related.t\n@@ -74,4 +74,14 @@ test_expect_success \"from committish\" \"\n \ttest_cmp expected actual\n \"\n \n+test_expect_success \"from single rev committish\" \"\n+\tgit related -1 master | sort > actual &&\n+\tcat > expected <<-EOF &&\n+\tJohn Doe <john@doe.com>\n+\tJohn Poppins <john@doe.com>\n+\tJon Stewart <jon@stewart.com>\n+\tEOF\n+\ttest_cmp expected actual\n+\"\n+\n test_done\n-- \n1.8.4-fc\n"}]}