{"thread":{"id":"33547","subject":"[PATCH v3 00/11] Add new git-cc-cmd helper to contrib","startedAt":"2013-04-19T19:30:20Z","lastAt":"2013-04-20T01:34:14Z","messageCount":17,"participants":["Felipe Contreras","Junio C Hamano","Duy Nguyen"],"isPatch":true,"patchVersion":3,"patchTotal":11},"messages":[{"id":"214865","messageId":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":null,"subject":"[PATCH v3 00/11] Add new git-cc-cmd helper to contrib","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:20Z","receivedAt":"2013-04-19T19:30:20Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Hi,\n\nHere goes another try, I made some changes based on comments, and should be\nsimpler now. Plus a few improvements. The interdiff will be on the patches, but\nonly the first one changed substantially.\n\nThis script allows you to get a list of relevant persons to Cc when sending a\npatch series.\n\n  % git cc-cmd v1.8.1.6^^1..v1.8.1.6^^2\n  \"Henrik Grubbström\" <grubba@grubba.org> (author: 7%)\n  junio (signer: 84%, author: 15%)\n  \"Nguyễn Thái Ngọc Duy\" <pclouds@gmail.com> (author: 30%, signer: 7%)\n  \"Jean-Noël AVILA\" <avila.jn@gmail.com> (author: 7%)\n  Jean-Noel Avila <jn.avila@free.fr> (signer: 7%)\n  Duy Nguyen <pclouds@gmail.com> (author: 7%)\n  Michael Haggerty <mhagger@alum.mit.edu> (author: 15%)\n  Clemens Buchacher <drizzd@aon.at> (author: 7%)\n  Joshua Jensen <jjensen@workspacewhiz.com> (author: 7%)\n  Johannes Sixt <j6t@kdbg.org> (signer: 7%)\n\nIt find people that might be interesting in a patch, by going back through the\nhistory for each single hunk modified, and finding people that reviewed,\nacknowledge, signed, or authored the code the patch 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 groups\nthem to show what exactly was their role when the participated in the\ndevelopment of the relevant commit, and on how many relevant commits they\nparticipated. They are only displayed if they pass a minimum threshold of\nparticipation.\n\nThe code finds the changes in each commit in the list, runs 'git blame'\nto see which other commits are relevant to those lines, and then adds\nthe author and signer to the list.\n\nFinally, it calculates what percentage of the total relevant commits\neach person was involved in, and if it passes the threshold, it goes in.\n\nYou can also choose to show the commits themselves:\n\n  % git cc-cmd --commits v1.8.1.6^^1..v1.8.1.6^^2\n  9db9eec attr: avoid calling find_basename() twice per path\n  94bc671 Add directory pattern matching to attributes\n  82dce99 attr: more matching optimizations from .gitignore\n  593cb88 exclude: split basename matching code into a separate function\n  b559263 exclude: split pathname matching code into a separate function\n  4742d13 attr: avoid searching for basename on every match\n  f950eb9 rename pathspec_prefix() to common_prefix() and move to dir.[ch]\n  4a085b1 consolidate pathspec_prefix and common_prefix\n  d932f4e Rename git_checkattr() to git_check_attr()\n  2d72174 Extract a function collect_all_attrs()\n  8cf2a84 Add string comparison functions that respect the ignore_case variable.\n  407a963 Merge branch 'rr/remote-helper-doc'\n  ec775c4 attr: Expand macros immediately when encountered.\n\nBut wait, there's more: you can also specify a list of patch files, which means\nthis can be used for git send-emails --cc-cmd option.\n\nFelipe Contreras (11):\n  Add new git-cc-cmd helper to contrib\n  contrib: cc-cmd: add option parsing\n  contrib: cc-cmd: add support for multiple patches\n  contrib: cc-cmd: add option to show commits\n  contrib: cc-cmd: add option to parse from committish\n  contrib: cc-cmd: parse committish like format-patch\n  contrib: cc-cmd: fix parsing of rev-list args\n  contrib: cc-cmd: add option to fetch aliases\n  contrib: cc-cmd: support multiple roles\n  contrib: cc-cmd: sort by participation\n  contrib: cc-cmd: ignore chunks with no original lines\n\n contrib/cc-cmd/git-cc-cmd | 258 ++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 258 insertions(+)\n create mode 100755 contrib/cc-cmd/git-cc-cmd\n\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214878","messageId":"1366399831-5964-2-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 01/11] Add new git-cc-cmd helper to contrib","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:21Z","receivedAt":"2013-04-19T19:30: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 interesting 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 cc-cmd 0001-remote-hg-trivial-cleanups.patch\n  Felipe Contreras <felipe.contreras@gmail.com> (author: 100%)\n  Jeff King <peff@peff.net> (signer: 83%)\n  Max Horn <max@quendi.de> (signer: 16%)\n  Junio C Hamano <gitster@pobox.com> (signer: 16%)\n\nThus it can be used for 'git send-email' as a cc-cmd.\n\nComments-by: Ramkumar Ramachandra <artagnon@gmail.com>\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/cc-cmd/git-cc-cmd | 131 ++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 131 insertions(+)\n create mode 100755 contrib/cc-cmd/git-cc-cmd\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nnew file mode 100755\nindex 0000000..aa83a1a\n--- /dev/null\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -0,0 +1,131 @@\n+#!/usr/bin/env ruby\n+\n+$since = '3-years-ago'\n+$min_percent = 5\n+\n+class Commit\n+\n+  attr_reader :id, :roles\n+\n+  def initialize(id)\n+    @id = id\n+    @roles = []\n+  end\n+\n+  def parse(data)\n+    author = msg = nil\n+    roles = {}\n+    data.each_line do |line|\n+      if not msg\n+        case line\n+        when /^author ([^<>]+) <(\\S+)>$/\n+          author = $1, $2\n+          roles[author] = :author\n+        when /^$/\n+          msg = true\n+        end\n+      else\n+        if line =~ /^(Signed-off|Reviewed|Acked)-by: ([^<>]+) <(\\S+?)>$/\n+          person = $2, $3\n+          roles[person] = :signer if person != author\n+        end\n+      end\n+    end\n+    @roles = roles.map do |person, role|\n+      [person, role]\n+    end\n+  end\n+\n+end\n+\n+class Commits\n+\n+  attr_reader :items\n+\n+  def initialize()\n+    @items = {}\n+  end\n+\n+  def size\n+    @items.size\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 |l|\n+        if l =~ /^(\\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, offset, from)\n+    return unless source\n+    File.popen(['git', 'blame', '--incremental', '-C',\n+               '-L', '%u,+%u' % [start, offset],\n+               '--since', $since, from + '^',\n+               '--', source]) do |p|\n+      p.each do |line|\n+        if line =~ /^(\\h{40})/\n+          id = $1\n+          @items[id] = Commit.new(id)\n+        end\n+      end\n+    end\n+  end\n+\n+  def from_patch(file)\n+    source = nil\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+        when /^---\\s+(\\S+)/\n+          source = $1 != '/dev/null' ? $1[2..-1] : nil\n+        when /^@@\\s-(\\d+),(\\d+)/\n+          get_blame(source, $1, $2, 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+# hash of hashes\n+persons = Hash.new { |hash, key| hash[key] = {} }\n+\n+commits.items.values.each do |commit|\n+  commit.roles.each do |person, role|\n+    persons[person][role] ||= 0\n+    persons[person][role] += 1\n+  end\n+end\n+\n+persons.each do |person, roles|\n+  roles = roles.map do |role, count|\n+    percent = count.to_f * 100 / commits.size\n+    next if percent < $min_percent\n+    '%s: %u%%' % [role, percent]\n+  end.compact\n+  next if roles.empty?\n+\n+  name, email = person\n+  # must quote chars?\n+  name = '\"%s\"' % name if name =~ /[^\\w \\-]/i\n+  person = name ? '%s <%s>' % [name, email] : email\n+  puts '%s (%s)' % [person, roles.join(', ')]\n+end\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214868","messageId":"1366399831-5964-3-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 02/11] contrib: cc-cmd: add option parsing","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:22Z","receivedAt":"2013-04-19T19:30:22Z","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/cc-cmd/git-cc-cmd | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex aa83a1a..d78759d 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -1,8 +1,25 @@\n #!/usr/bin/env ruby\n \n+require 'optparse'\n+\n $since = '3-years-ago'\n $min_percent = 5\n \n+begin\n+  OptionParser.new do |opts|\n+    opts.program_name = 'git cc-cmd'\n+    opts.banner = 'usage: git cc-cmd [options] <file>'\n+\n+    opts.on('-p', '--min-percent N', Integer, 'Minium percentage of role participation') do |v|\n+      $min_percent = v\n+    end\n+    opts.on('-d', '--since DATE', 'How far back to search for relevant commits') do |v|\n+      $since = v\n+    end\n+  end.parse!\n+rescue OptionParser::InvalidOption\n+end\n+\n class Commit\n \n   attr_reader :id, :roles\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214866","messageId":"1366399831-5964-4-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 03/11] contrib: cc-cmd: add support for multiple patches","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:23Z","receivedAt":"2013-04-19T19:30:23Z","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/cc-cmd/git-cc-cmd | 34 ++++++++++++++++++----------------\n 1 file changed, 18 insertions(+), 16 deletions(-)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex d78759d..3a9c70e 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -8,7 +8,7 @@ $min_percent = 5\n begin\n   OptionParser.new do |opts|\n     opts.program_name = 'git cc-cmd'\n-    opts.banner = 'usage: git cc-cmd [options] <file>'\n+    opts.banner = 'usage: git cc-cmd [options] <files>'\n \n     opts.on('-p', '--min-percent N', Integer, 'Minium percentage of role participation') do |v|\n       $min_percent = v\n@@ -61,6 +61,7 @@ class Commits\n \n   def initialize()\n     @items = {}\n+    @main_commits = {}\n   end\n \n   def size\n@@ -91,24 +92,27 @@ class Commits\n       p.each do |line|\n         if line =~ /^(\\h{40})/\n           id = $1\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+  def from_patches(files)\n     source = nil\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-        when /^---\\s+(\\S+)/\n-          source = $1 != '/dev/null' ? $1[2..-1] : nil\n-        when /^@@\\s-(\\d+),(\\d+)/\n-          get_blame(source, $1, $2, from)\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 /^@@\\s-(\\d+),(\\d+)/\n+            get_blame(source, $1, $2, from)\n+          end\n         end\n       end\n     end\n@@ -116,10 +120,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 # hash of hashes\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214869","messageId":"1366399831-5964-5-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 04/11] contrib: cc-cmd: add option to show commits","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:24Z","receivedAt":"2013-04-19T19:30:24Z","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/cc-cmd/git-cc-cmd | 18 ++++++++++++++++++\n 1 file changed, 18 insertions(+)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex 3a9c70e..c49225f 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -4,6 +4,7 @@ require 'optparse'\n \n $since = '3-years-ago'\n $min_percent = 5\n+$show_commits = false\n \n begin\n   OptionParser.new do |opts|\n@@ -16,6 +17,9 @@ begin\n     opts.on('-d', '--since DATE', 'How far back to search for relevant commits') do |v|\n       $since = v\n     end\n+    opts.on('-c', '--commits[=FORMAT]', [:raw, :full], 'List commits instead of persons') do |v|\n+      $show_commits = v || true\n+    end\n   end.parse!\n rescue OptionParser::InvalidOption\n end\n@@ -124,6 +128,20 @@ commits = Commits.new\n commits.from_patches(ARGV)\n commits.import\n \n+if $show_commits\n+  cmd = nil\n+  case $show_commits\n+  when :raw\n+    puts commits.items.keys\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.items.keys) if cmd\n+  exit 0\n+end\n+\n # hash of hashes\n persons = Hash.new { |hash, key| hash[key] = {} }\n \n-- \n1.8.2.1.790.g4588561\n"},{"id":"214867","messageId":"1366399831-5964-6-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 05/11] contrib: cc-cmd: add option to parse from committish","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:25Z","receivedAt":"2013-04-19T19:30:25Z","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/cc-cmd/git-cc-cmd | 36 ++++++++++++++++++++++++++++++++++--\n 1 file changed, 34 insertions(+), 2 deletions(-)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex c49225f..aecfa45 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -5,11 +5,13 @@ require 'optparse'\n $since = '3-years-ago'\n $min_percent = 5\n $show_commits = false\n+$files = []\n+$rev_args = []\n \n begin\n   OptionParser.new do |opts|\n     opts.program_name = 'git cc-cmd'\n-    opts.banner = 'usage: git cc-cmd [options] <files>'\n+    opts.banner = 'usage: git cc-cmd [options] <files | rev-list options>'\n \n     opts.on('-p', '--min-percent N', Integer, 'Minium percentage of role participation') do |v|\n       $min_percent = v\n@@ -122,10 +124,40 @@ class Commits\n     end\n   end\n \n+  def from_rev_args(args)\n+    return if args.empty?\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 /^@@\\s-(\\d+),(\\d+)/\n+              get_blame(source, $1, $2, id)\n+            end\n+          end\n+        end\n+      end\n+    end\n+  end\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+commits.from_patches($files)\n+commits.from_rev_args($rev_args)\n commits.import\n \n if $show_commits\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214871","messageId":"1366399831-5964-7-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 06/11] contrib: cc-cmd: parse committish like format-patch","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:26Z","receivedAt":"2013-04-19T19:30:26Z","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/cc-cmd/git-cc-cmd | 14 ++++++++++++++\n 1 file changed, 14 insertions(+)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex aecfa45..4bca7f1 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -126,6 +126,20 @@ class Commits\n \n   def from_rev_args(args)\n     return if args.empty?\n+\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+      committish = [ '%s..HEAD' % revs[0] ]\n+    else\n+      committish = revs\n+    end\n+\n     source = nil\n     File.popen(%w[git rev-list --reverse] + args) do |p|\n       p.each do |e|\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214872","messageId":"1366399831-5964-8-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 07/11] contrib: cc-cmd: fix parsing of rev-list args","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:27Z","receivedAt":"2013-04-19T19:30:27Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"For example '-1'.\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/cc-cmd/git-cc-cmd | 9 ++++++---\n 1 file changed, 6 insertions(+), 3 deletions(-)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex 4bca7f1..4fad030 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -23,7 +23,8 @@ begin\n       $show_commits = v || true\n     end\n   end.parse!\n-rescue OptionParser::InvalidOption\n+rescue OptionParser::InvalidOption => e\n+  $rev_args += e.args\n end\n \n class Commit\n@@ -135,9 +136,11 @@ class Commits\n \n     case revs.size\n     when 1\n-      committish = [ '%s..HEAD' % revs[0] ]\n+      r = revs[0]\n+      r = '^' + r if r[0] != '-'\n+      args = [ r, 'HEAD' ]\n     else\n-      committish = revs\n+      args = revs\n     end\n \n     source = nil\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214870","messageId":"1366399831-5964-9-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 08/11] contrib: cc-cmd: add option to fetch aliases","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:28Z","receivedAt":"2013-04-19T19:30:28Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Only the mutt format is supported for now.\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/cc-cmd/git-cc-cmd | 25 +++++++++++++++++++++++++\n 1 file changed, 25 insertions(+)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex 4fad030..200da0d 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -7,6 +7,8 @@ $min_percent = 5\n $show_commits = false\n $files = []\n $rev_args = []\n+$get_aliases = false\n+$aliases = {}\n \n begin\n   OptionParser.new do |opts|\n@@ -22,11 +24,32 @@ begin\n     opts.on('-c', '--commits[=FORMAT]', [:raw, :full], 'List commits instead of persons') do |v|\n       $show_commits = v || true\n     end\n+    opts.on('-a', '--aliases', 'Use aliases') do |v|\n+      $get_aliases = v\n+    end\n   end.parse!\n rescue OptionParser::InvalidOption => e\n   $rev_args += e.args\n end\n \n+def get_aliases\n+  type = %x[git config sendemail.aliasfiletype].chomp\n+  return if type != 'mutt'\n+  file = %x[git config sendemail.aliasesfile].chomp\n+  File.open(File.expand_path(file)) do |f|\n+    f.each do |line|\n+      if line =~ /^\\s*alias\\s+(?:-group\\s+\\S+\\s+)*(\\S+)\\s+(.*)$/\n+        key, addresses = $1, $2.split(', ')\n+        addresses.each do |address|\n+          $aliases[address] = key\n+        end\n+      end\n+    end\n+  end\n+end\n+\n+get_aliases if $get_aliases\n+\n class Commit\n \n   attr_reader :id, :roles\n@@ -56,6 +79,8 @@ class Commit\n       end\n     end\n     @roles = roles.map do |person, role|\n+      address = \"%s <%s>\" % person\n+      person = nil, $aliases[address] if $aliases.include?(address)\n       [person, role]\n     end\n   end\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214875","messageId":"1366399831-5964-10-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 09/11] contrib: cc-cmd: support multiple roles","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:29Z","receivedAt":"2013-04-19T19:30:29Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Currently only the roles of 'author' and 'signer' and handler, but now\nthere's also 'reviewer' and 'acker'.\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/cc-cmd/git-cc-cmd | 29 ++++++++++++++++++++---------\n 1 file changed, 20 insertions(+), 9 deletions(-)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex 200da0d..67a276d 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -50,6 +50,12 @@ end\n \n get_aliases if $get_aliases\n \n+KNOWN_ROLES = {\n+  'Signed-off' => :signer,\n+  'Reviewed' => :reviewer,\n+  'Acked' => :acker,\n+}\n+\n class Commit\n \n   attr_reader :id, :roles\n@@ -61,27 +67,30 @@ class Commit\n \n   def parse(data)\n     author = msg = nil\n-    roles = {}\n+    # hash of arrays\n+    roles = Hash.new { |hash, key| hash[key] = [] }\n     data.each_line do |line|\n       if not msg\n         case line\n         when /^author ([^<>]+) <(\\S+)>$/\n           author = $1, $2\n-          roles[author] = :author\n+          roles[author] << :author\n         when /^$/\n           msg = true\n         end\n       else\n-        if line =~ /^(Signed-off|Reviewed|Acked)-by: ([^<>]+) <(\\S+?)>$/\n+        role_regex = KNOWN_ROLES.keys.join('|')\n+        if line =~ /^(#{role_regex})-by: ([^<>]+) <(\\S+?)>$/\n           person = $2, $3\n-          roles[person] = :signer if person != author\n+          role = KNOWN_ROLES[$1]\n+          roles[person] << role if person != author\n         end\n       end\n     end\n-    @roles = roles.map do |person, role|\n+    @roles = roles.map do |person, roles|\n       address = \"%s <%s>\" % person\n       person = nil, $aliases[address] if $aliases.include?(address)\n-      [person, role]\n+      [person, roles]\n     end\n   end\n \n@@ -220,9 +229,11 @@ end\n persons = Hash.new { |hash, key| hash[key] = {} }\n \n commits.items.values.each do |commit|\n-  commit.roles.each do |person, role|\n-    persons[person][role] ||= 0\n-    persons[person][role] += 1\n+  commit.roles.each do |person, roles|\n+    roles.each do |role|\n+      persons[person][role] ||= 0\n+      persons[person][role] += 1\n+    end\n   end\n end\n \n-- \n1.8.2.1.790.g4588561\n"},{"id":"214873","messageId":"1366399831-5964-11-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 10/11] contrib: cc-cmd: sort by participation","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:30Z","receivedAt":"2013-04-19T19:30:30Z","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/cc-cmd/git-cc-cmd | 7 ++++++-\n 1 file changed, 6 insertions(+), 1 deletion(-)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex 67a276d..9a2d2fd 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -237,7 +237,12 @@ commits.items.values.each do |commit|\n   end\n end\n \n-persons.each do |person, roles|\n+# sort by number of participations\n+count_sort = lambda do |a, b|\n+  b[1].values.reduce(:+) <=> a[1].values.reduce(:+)\n+end\n+\n+persons.sort(&count_sort).each do |person, roles|\n   roles = roles.map do |role, count|\n     percent = count.to_f * 100 / commits.size\n     next if percent < $min_percent\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214874","messageId":"1366399831-5964-12-git-send-email-felipe.contreras@gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v3 11/11] contrib: cc-cmd: ignore chunks with no original lines","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:30:31Z","receivedAt":"2013-04-19T19:30:31Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Suggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/cc-cmd/git-cc-cmd | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex 9a2d2fd..02841c4 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -125,7 +125,7 @@ class Commits\n   end\n \n   def get_blame(source, start, offset, from)\n-    return unless source\n+    return unless source and offset\n     File.popen(['git', 'blame', '--incremental', '-C',\n                '-L', '%u,+%u' % [start, offset],\n                '--since', $since, from + '^',\n-- \n1.8.2.1.790.g4588561\n"},{"id":"214877","messageId":"CAMP44s0H-2nafJiV33_6xPH2WzT+=c0ULGg8XqB-mHOc2cHomg@mail.gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-2-git-send-email-felipe.contreras@gmail.com","subject":"Re: [PATCH v3 01/11] Add new git-cc-cmd helper to contrib","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-19T19:38:42Z","receivedAt":"2013-04-19T19:38:42Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"On Fri, Apr 19, 2013 at 2:30 PM, Felipe Contreras\n<felipe.contreras@gmail.com> wrote:\n> This script find people that might be interesting in a patch, by going\n> back through the history for each single hunk modified, and finding\n> people that reviewed, acknowledge, 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>   % git cc-cmd 0001-remote-hg-trivial-cleanups.patch\n>   Felipe Contreras <felipe.contreras@gmail.com> (author: 100%)\n>   Jeff King <peff@peff.net> (signer: 83%)\n>   Max Horn <max@quendi.de> (signer: 16%)\n>   Junio C Hamano <gitster@pobox.com> (signer: 16%)\n>\n> Thus it can be used for 'git send-email' as a cc-cmd.\n\nHere's the interdiff with the previous one:\n\ndiff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\nindex c7ecf79..aa83a1a 100755\n--- a/contrib/cc-cmd/git-cc-cmd\n+++ b/contrib/cc-cmd/git-cc-cmd\n@@ -5,40 +5,35 @@ $min_percent = 5\n\n class Commit\n\n-  attr_reader :id\n-  attr_accessor :roles\n+  attr_reader :id, :roles\n\n   def initialize(id)\n     @id = id\n     @roles = []\n   end\n\n-  def self.parse(data)\n-    id = author = msg = nil\n+  def parse(data)\n+    author = msg = nil\n     roles = {}\n     data.each_line do |line|\n       if not msg\n         case line\n-        when /^commit (.+)$/\n-          id = $1\n         when /^author ([^<>]+) <(\\S+)>$/\n           author = $1, $2\n-          roles[author] = 'author'\n+          roles[author] = :author\n         when /^$/\n           msg = true\n         end\n       else\n         if line =~ /^(Signed-off|Reviewed|Acked)-by: ([^<>]+) <(\\S+?)>$/\n           person = $2, $3\n-          roles[person] = 'signer' if person != author\n+          roles[person] = :signer if person != author\n         end\n       end\n     end\n-    roles = roles.map do |person, role|\n-      address = \"%s <%s>\" % person\n+    @roles = roles.map do |person, role|\n       [person, role]\n     end\n-    [id, roles]\n   end\n\n end\n@@ -57,22 +52,15 @@ class Commits\n\n   def import\n     return if @items.empty?\n-    format = [ 'commit %H', 'author %an <%ae>', '', '%B' ].join('%n')\n-    File.popen(['git', 'show', '-z', '-s', '--format=format:' +\nformat] + @items.keys) do |p|\n-      p.each(\"\\0\") do |data|\n-        next if data == \"\\0\" # bug in git show?\n-        id, roles = Commit.parse(data)\n-        commit = @items[id]\n-        commit.roles = roles\n-      end\n-    end\n-  end\n-\n-  def each_person_role\n-    commit_roles = @items.values.map { |commit| commit.roles }.flatten(1)\n-    commit_roles.group_by { |person, role| person }.each do |person,\ncommit_roles|\n-      commit_roles.group_by { |person, role| role }.each do |role,\ncommit_roles|\n-        yield person, role, commit_roles.size\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 |l|\n+        if l =~ /^(\\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@@ -107,34 +95,37 @@ class Commits\n         end\n       end\n     end\n-    import\n   end\n\n end\n\n exit 1 if ARGV.size != 1\n\n-commits = Commits.new()\n+commits = Commits.new\n commits.from_patch(ARGV[0])\n+commits.import\n\n # hash of hashes\n persons = Hash.new { |hash, key| hash[key] = {} }\n\n-commits.each_person_role do |person, role, count|\n-  persons[person][role] = count\n+commits.items.values.each do |commit|\n+  commit.roles.each do |person, role|\n+    persons[person][role] ||= 0\n+    persons[person][role] += 1\n+  end\n end\n\n persons.each do |person, roles|\n   roles = roles.map do |role, count|\n     percent = count.to_f * 100 / commits.size\n     next if percent < $min_percent\n-    \"%s: %u%%\" % [role, percent]\n+    '%s: %u%%' % [role, percent]\n   end.compact\n   next if roles.empty?\n\n   name, email = person\n   # must quote chars?\n   name = '\"%s\"' % name if name =~ /[^\\w \\-]/i\n-  person = name ? \"%s <%s>\" % [name, email] : email\n-  puts \"%s (%s)\" % [person, roles.join(', ')]\n+  person = name ? '%s <%s>' % [name, email] : email\n+  puts '%s (%s)' % [person, roles.join(', ')]\n end\n\n-- \nFelipe Contreras\n"},{"id":"214884","messageId":"7vmwsuz3px.fsf@alter.siamese.dyndns.org","threadId":"33547","inReplyTo":"1366399831-5964-12-git-send-email-felipe.contreras@gmail.com","subject":"Re: [PATCH v3 11/11] contrib: cc-cmd: ignore chunks with no original lines","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-04-19T20:04:58Z","receivedAt":"2013-04-19T20:04:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Felipe Contreras <felipe.contreras@gmail.com> writes:\n\n> Suggested-by: Junio C Hamano <gitster@pobox.com>\n> Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n> ---\n>  contrib/cc-cmd/git-cc-cmd | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n>\n> diff --git a/contrib/cc-cmd/git-cc-cmd b/contrib/cc-cmd/git-cc-cmd\n> index 9a2d2fd..02841c4 100755\n> --- a/contrib/cc-cmd/git-cc-cmd\n> +++ b/contrib/cc-cmd/git-cc-cmd\n> @@ -125,7 +125,7 @@ class Commits\n>    end\n>  \n>    def get_blame(source, start, offset, from)\n> -    return unless source\n> +    return unless source and offset\n\nI do not think a separate patch at the end is a good idea for a\nfix-up like this, even if to give credit to others.\n\nJust squashing it in to the original would help reviewers; that way,\nthey do not have to wonder \"why is this broken\" and later notice\n\"ah, it is fixed at the end\".\n\nI do not know if that is an \"offset\", though.  It is the \"offset\"\nfor the end of the block relative to its beginning, but people\nusually call that the \"size\" of the block, I think.\n\n>      File.popen(['git', 'blame', '--incremental', '-C',\n>                 '-L', '%u,+%u' % [start, offset],\n>                 '--since', $since, from + '^',\n"},{"id":"214893","messageId":"7vy5cexj7y.fsf@alter.siamese.dyndns.org","threadId":"33547","inReplyTo":"1366399831-5964-2-git-send-email-felipe.contreras@gmail.com","subject":"Re: [PATCH v3 01/11] Add new git-cc-cmd helper to contrib","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-04-19T22:13:05Z","receivedAt":"2013-04-19T22:13:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Felipe Contreras <felipe.contreras@gmail.com> writes:\n\n> This script find people that might be interesting in a patch, by going\n> back through the history for each single hunk modified, and finding\n> people that reviewed, acknowledge, 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>   % git cc-cmd 0001-remote-hg-trivial-cleanups.patch\n>   Felipe Contreras <felipe.contreras@gmail.com> (author: 100%)\n>   Jeff King <peff@peff.net> (signer: 83%)\n>   Max Horn <max@quendi.de> (signer: 16%)\n>   Junio C Hamano <gitster@pobox.com> (signer: 16%)\n\nShould the contribution weight for each role add up to (close to)\n100% if there were no minimum cut-off?\n\nIn general, it is not all that clear what these numbers mean.  Does\na sign/review on a single commit, no matter what kind of commit it\nis or how big it is, count as contribution with the same weight?\n\nI am not saying that the counting criteria needs to be configurable.\nIt just needs to be explainable to the end users.\n\n> +commits = Commits.new\n> +commits.from_patch(ARGV[0])\n> +commits.import\n> +\n> +# hash of hashes\n> +persons = Hash.new { |hash, key| hash[key] = {} }\n> +\n> +commits.items.values.each do |commit|\n> +  commit.roles.each do |person, role|\n> +    persons[person][role] ||= 0\n> +    persons[person][role] += 1\n> +  end\n> +end\n> +\n> +persons.each do |person, roles|\n> +  roles = roles.map do |role, count|\n> +    percent = count.to_f * 100 / commits.size\n> +    next if percent < $min_percent\n> +    '%s: %u%%' % [role, percent]\n> +  end.compact\n> +  next if roles.empty?\n> +\n> +  name, email = person\n> +  # must quote chars?\n> +  name = '\"%s\"' % name if name =~ /[^\\w \\-]/i\n> +  person = name ? '%s <%s>' % [name, email] : email\n> +  puts '%s (%s)' % [person, roles.join(', ')]\n> +end\n"},{"id":"214895","messageId":"CACsJy8Dt2++tRUYcJk3jXuHUAv9jRmYYSYyVwHXQeo5O_3WQLw@mail.gmail.com","threadId":"33547","inReplyTo":"1366399831-5964-1-git-send-email-felipe.contreras@gmail.com","subject":"Re: [PATCH v3 00/11] Add new git-cc-cmd helper to contrib","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2013-04-20T01:16:31Z","receivedAt":"2013-04-20T01:16:31Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Sat, Apr 20, 2013 at 5:30 AM, Felipe Contreras\n<felipe.contreras@gmail.com> wrote:\n> This script allows you to get a list of relevant persons to Cc when sending a\n> patch series.\n>\n>   % git cc-cmd v1.8.1.6^^1..v1.8.1.6^^2\n>   \"Nguyễn Thái Ngọc Duy\" <pclouds@gmail.com> (author: 30%, signer: 7%)\n>   Duy Nguyen <pclouds@gmail.com> (author: 7%)\n\nYou see, I like to cause confusion. These two are the same person.\nMaybe you should check based on email only instead.\n\n>   \"Jean-Noël AVILA\" <avila.jn@gmail.com> (author: 7%)\n>   Jean-Noel Avila <jn.avila@free.fr> (signer: 7%)\n\nAnd these two are like the same. Perhaps mailmap support will help?\n--\nDuy\n"},{"id":"214897","messageId":"CAMP44s0rF+1_4hHUe_yEgP6rOPhQwVz=FrdiGm5defnYByqj1Q@mail.gmail.com","threadId":"33547","inReplyTo":"CACsJy8Dt2++tRUYcJk3jXuHUAv9jRmYYSYyVwHXQeo5O_3WQLw@mail.gmail.com","subject":"Re: [PATCH v3 00/11] Add new git-cc-cmd helper to contrib","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-04-20T01:34:14Z","receivedAt":"2013-04-20T01:34:14Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"On Fri, Apr 19, 2013 at 8:16 PM, Duy Nguyen <pclouds@gmail.com> wrote:\n> On Sat, Apr 20, 2013 at 5:30 AM, Felipe Contreras\n> <felipe.contreras@gmail.com> wrote:\n>> This script allows you to get a list of relevant persons to Cc when sending a\n>> patch series.\n>>\n>>   % git cc-cmd v1.8.1.6^^1..v1.8.1.6^^2\n>>   \"Nguyễn Thái Ngọc Duy\" <pclouds@gmail.com> (author: 30%, signer: 7%)\n>>   Duy Nguyen <pclouds@gmail.com> (author: 7%)\n>\n> You see, I like to cause confusion. These two are the same person.\n> Maybe you should check based on email only instead.\n\nShould be easy to implement.\n\n>>   \"Jean-Noël AVILA\" <avila.jn@gmail.com> (author: 7%)\n>>   Jean-Noel Avila <jn.avila@free.fr> (signer: 7%)\n>\n> And these two are like the same. Perhaps mailmap support will help?\n\nThere's already support for user-defined alias\n(sendemail.aliasesfile), it shouldn't be too difficult to parse a\n.mailmap file too.\n\nCheers.\n\n-- \nFelipe Contreras\n"}]}