{"thread":{"id":"33998","subject":"[PATCH v8 0/6] New git-related helper","startedAt":"2013-05-31T07:37:38Z","lastAt":"2013-05-31T07:48:43Z","messageCount":8,"participants":["Felipe Contreras"],"isPatch":true,"patchVersion":8,"patchTotal":6},"messages":[{"id":"219067","messageId":"1369985864-31922-1-git-send-email-felipe.contreras@gmail.com","threadId":"33998","inReplyTo":null,"subject":"[PATCH v8 0/6] New git-related helper","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-05-31T07:37:38Z","receivedAt":"2013-05-31T07:37:38Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Hi,\n\nSince there hasn't been any comments of importance this is basically the same\nas v7, plus a few other patches to make it actually usable (for me).\n\nUnfortunately it turns out Ruby's optparse is not suitable for our needs, so I\nimplemented a very small parser that is.\n\nFelipe Contreras (6):\n  Add new git-related helper to contrib\n  contrib: related: add option parsing\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: implement custom option parser\n\n contrib/related/git-related | 247 ++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 247 insertions(+)\n create mode 100755 contrib/related/git-related\n\n-- \n1.8.3.358.g5a91d05\n"},{"id":"219068","messageId":"1369985864-31922-2-git-send-email-felipe.contreras@gmail.com","threadId":"33998","inReplyTo":"1369985864-31922-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v8 1/6] Add new git-related helper to contrib","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-05-31T07:37:39Z","receivedAt":"2013-05-31T07:37:39Z","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..1b9b1e7\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+    msg = nil\n+    data.each_line do |line|\n+      if not msg\n+        case line\n+        when /^author ([^<>]+) <(\\S+)> (.+)$/\n+          @persons << '%s <%s>' % [$1, $2]\n+        when /^$/\n+          msg = true\n+        end\n+      else\n+        if line =~ /^(Signed-off|Reviewed|Acked)-by: ([^<>]+) <(\\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.3.358.g5a91d05\n"},{"id":"219069","messageId":"1369985864-31922-3-git-send-email-felipe.contreras@gmail.com","threadId":"33998","inReplyTo":"1369985864-31922-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v8 2/6] contrib: related: add option parsing","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-05-31T07:37:40Z","receivedAt":"2013-05-31T07:37:40Z","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 | 16 ++++++++++++++++\n 1 file changed, 16 insertions(+)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 1b9b1e7..bde5b99 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -3,9 +3,25 @@\n # This script finds people that might be interested in a patch\n # usage: git related <file>\n \n+require 'optparse'\n+\n $since = '5-years-ago'\n $min_percent = 10\n \n+OptionParser.new do |opts|\n+  opts.program_name = 'git related'\n+  opts.banner = 'usage: git related [options] <file>'\n+\n+  opts.on('-p', '--min-percent N', Integer,\n+          'Minium percentage of role participation') do |v|\n+    $min_percent = v\n+  end\n+  opts.on('-d', '--since DATE',\n+          'How far back to search for relevant commits') do |v|\n+    $since = v\n+  end\n+end.parse!\n+\n class Commit\n \n   attr_reader :persons\n-- \n1.8.3.358.g5a91d05\n"},{"id":"219073","messageId":"1369985864-31922-4-git-send-email-felipe.contreras@gmail.com","threadId":"33998","inReplyTo":"1369985864-31922-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v8 3/6] contrib: related: add support for multiple patches","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-05-31T07:37:41Z","receivedAt":"2013-05-31T07:37:41Z","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 1 file changed, 19 insertions(+), 16 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex bde5b99..0de1c6c 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -10,7 +10,7 @@ $min_percent = 10\n \n OptionParser.new do |opts|\n   opts.program_name = 'git related'\n-  opts.banner = 'usage: git related [options] <file>'\n+  opts.banner = 'usage: git related [options] <files>'\n \n   opts.on('-p', '--min-percent N', Integer,\n           'Minium percentage of role participation') do |v|\n@@ -56,6 +56,7 @@ class Commits\n \n   def initialize\n     @items = {}\n+    @main_commits = {}\n   end\n \n   def size\n@@ -91,23 +92,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@@ -115,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 count_per_person = Hash.new(0)\n-- \n1.8.3.358.g5a91d05\n"},{"id":"219071","messageId":"1369985864-31922-5-git-send-email-felipe.contreras@gmail.com","threadId":"33998","inReplyTo":"1369985864-31922-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v8 4/6] contrib: related: add option to parse from committish","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-05-31T07:37:42Z","receivedAt":"2013-05-31T07:37:42Z","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 | 38 ++++++++++++++++++++++++++++++++++++--\n 1 file changed, 36 insertions(+), 2 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 0de1c6c..3573237 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -7,10 +7,12 @@ require 'optparse'\n \n $since = '5-years-ago'\n $min_percent = 10\n+$files = []\n+$rev_args = []\n \n OptionParser.new do |opts|\n   opts.program_name = 'git related'\n-  opts.banner = 'usage: git related [options] <files>'\n+  opts.banner = 'usage: git related [options] <files | rev-list options>'\n \n   opts.on('-p', '--min-percent N', Integer,\n           'Minium percentage of role participation') do |v|\n@@ -118,10 +120,42 @@ class Commits\n     end\n   end\n \n+  def from_rev_args(args)\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+        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+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)\n-- \n1.8.3.358.g5a91d05\n"},{"id":"219070","messageId":"1369985864-31922-6-git-send-email-felipe.contreras@gmail.com","threadId":"33998","inReplyTo":"1369985864-31922-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v8 5/6] contrib: related: parse committish like format-patch","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-05-31T07:37:43Z","receivedAt":"2013-05-31T07:37:43Z","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 1 file changed, 15 insertions(+)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 3573237..585572b 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -121,6 +121,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     source = nil\n     File.popen(%w[git rev-list --reverse] + args) do |p|\n       p.each do |e|\n-- \n1.8.3.358.g5a91d05\n"},{"id":"219072","messageId":"1369985864-31922-7-git-send-email-felipe.contreras@gmail.com","threadId":"33998","inReplyTo":"1369985864-31922-1-git-send-email-felipe.contreras@gmail.com","subject":"[PATCH v8 6/6] contrib: related: implement custom option parser","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-05-31T07:37:44Z","receivedAt":"2013-05-31T07:37:44Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ruby's option parser is not extensible enough to keep unknown options.\n\nSigned-off-by: Felipe Contreras <felipe.contreras@gmail.com>\n---\n contrib/related/git-related | 83 ++++++++++++++++++++++++++++++++++++++-------\n 1 file changed, 71 insertions(+), 12 deletions(-)\n\ndiff --git a/contrib/related/git-related b/contrib/related/git-related\nindex 585572b..c933898 100755\n--- a/contrib/related/git-related\n+++ b/contrib/related/git-related\n@@ -3,26 +3,85 @@\n # This script finds people that might be interested in a patch\n # usage: git related <file>\n \n-require 'optparse'\n-\n $since = '5-years-ago'\n $min_percent = 10\n $files = []\n $rev_args = []\n \n-OptionParser.new do |opts|\n-  opts.program_name = 'git related'\n-  opts.banner = 'usage: git related [options] <files | rev-list options>'\n+class SimpleParser\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-  opts.on('-p', '--min-percent N', Integer,\n-          'Minium percentage of role participation') do |v|\n-    $min_percent = v\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+    i = 0\n+    if ARGV.member?('-h') or ARGV.member?('--help')\n+      usage\n+      exit 1\n+    end\n+    while cur = ARGV[i] do\n+      if cur =~ /^(-.+?)(?:=(.*))?$/\n+        opt = @list[$1]\n+        if opt\n+          v = $2 || ARGV.delete_at(i + 1)\n+          opt.call(v)\n+          ARGV.delete_at(i)\n+          next\n+        end\n+      end\n+      i += 1\n+    end\n   end\n-  opts.on('-d', '--since DATE',\n-          'How far back to search for relevant commits') do |v|\n-    $since = v\n+\n+  def usage\n+    puts 'usage: %s' % @usage\n+    @list.values.uniq.each do |opt|\n+      s = '    '\n+      s << [opt.short, opt.long].compact.join(', ')\n+      s << '%*s%s' % [26 - s.size, '', opt.help] if opt.help\n+      puts s\n+    end\n   end\n-end.parse!\n+\n+end\n+\n+opts = SimpleParser.new\n+opts.usage = 'usage: git related [options] <files | rev-list options>'\n+\n+opts.on('-p', '--min-percent',\n+        '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+  $since = v\n+end\n+\n+opts.parse\n \n class Commit\n \n-- \n1.8.3.358.g5a91d05\n"},{"id":"219079","messageId":"CAMP44s3+QnXiSs3pPsMFdphMe-MDw5iD5+Sb1F=B0s+kTDipHw@mail.gmail.com","threadId":"33998","inReplyTo":"1369985864-31922-1-git-send-email-felipe.contreras@gmail.com","subject":"Re: [PATCH v8 0/6] New git-related helper","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2013-05-31T07:48:43Z","receivedAt":"2013-05-31T07:48:43Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"On Fri, May 31, 2013 at 2:37 AM, Felipe Contreras\n<felipe.contreras@gmail.com> wrote:\n\n> Since there hasn't been any comments of importance this is basically the same\n> as v7, plus a few other patches to make it actually usable (for me).\n>\n> Unfortunately it turns out Ruby's optparse is not suitable for our needs, so I\n> implemented a very small parser that is.\n\nNevermind, we can drop the option parsing changes for later, it's not\nimportant at the moment.\n\n-- \nFelipe Contreras\n"}]}