threads / patch / 34338

v2, 3 partscontrib: add git-contacts helper

Subject: [PATCH v2 1/3] contrib: add git-contacts helper

## tl;dr

4 messages between Jul 3, 2013 and Jul 3, 2013. Diffs are folded; open one to read it.

replies: 3people: 1as markdown or json

Eric Sunshine· Jul 3, 2013, 20:59 UTC · lore

[PATCH v2 0/3] Perl rewrite of Ruby git-related

This is v2 of the Perl rewrite of Felipe Contreras' git-related v9 patch series[1] which was written in Ruby. v2 addresses (hopefully all of) Junio's v1[2] review points.

[1]: http://thread.gmane.org/gmane.comp.version-control.git/226065/ [2]: http://thread.gmane.org/gmane.comp.version-control.git/229266/

Changes since v1:
* Match Signed-off-by:, etc. labels case-insensitively.
* Recognize Cc: in addition to Signed-off-by: and other typical labels.
* Match SHA-1's and surrounding context more strictly in output of
  git-format-patch, git-blame, git-cat-file.
* Mention -C in commit message of [1/3].
* Pass just a single -C to git-blame and git-diff. Previously, git-blame
  was invoked inconsistently with '-C -C' and git-diff with '-C'.
* Employ git-blame --porcelain rather than --incremental.
* Fix bug where a "new file" ('--- /dev/null') hunk following a "changed
  file" ('--- a/path/to/file') hunk would incorrectly invoke git-blame
  on "changed file" with line numbers from "new file".
* Fix bug where filename containing whitespace was not recognized
  correctly when scanning hunk source lines ('--- /a/file name').
* Abort when unable to parse hunk source ('--- path') from diff with
  custom --src-prefix=.
* Emit diagnostic, rather than exiting silently, when user neglects to
  provide input arguments.
* Combine v1 patches [1/4] and [2/4]. As Junio pointed, a single patch
  file may contain multiple patches (for example "git format-patch
  --stdout -4 >P.mbox"), so the machinery for dealing with multiple
  patches must already be present in [1/4] whether the patches come from
  one file or many. Consequently, adding "multiple file" support
  involved only a very minor change to @ARGV handling, not deserving of
  a separate [2/4] patch.
* Drop Perl source file and line number information from user-facing
  error messages.
Possible future directions and considerations:
* Add mailmap support.
* Make participation parameters (minimum-percent, blame since-date, -C
  level) configurable.
* Optimize by coalescing ranges into a single git-blame -L invocation;
  or enhance git-blame to accept multiple -L's.
* Improve Windows support if needed, possibly via grabbing
  run_cmd_pipe() from git-add--interactive.perl or similar.
Eric Sunshine (3):
  contrib: add git-contacts helper
  contrib: contacts: add ability to parse from committish
  contrib: contacts: interpret committish akin to format-patch
 contrib/contacts/git-contacts | 170 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 170 insertions(+)
 create mode 100755 contrib/contacts/git-contacts
-- 
1.8.3.2
Eric Sunshine· Jul 3, 2013, 20:59 UTC · re: Eric Sunshine · lore

This script lists people that might be interested in a patch by going back through the history for each patch hunk, and finding people that reviewed, acknowledge, signed, authored, or were Cc:'d on the code the patch is modifying.

It does this by running git-blame incrementally on each hunk and then parsing the commit message. After gathering all participants, it determines each person's relevance by considering how many commits mentioned that person compared with the total number of commits under consideration. The final output consists only of participants who pass a minimum threshold of participation.

Several conditions controlling a person's significance are currently hard-coded, such as minimum participation level, blame date-limiting, and -C level for detecting moved and copied lines. In the future, these conditions may become configurable.

For example:
  % git contacts 0001-remote-hg-trivial-cleanups.patch
  Felipe Contreras <felipe.contreras@gmail.com>
  Jeff King <peff@peff.net>
  Max Horn <max@quendi.de>
  Junio C Hamano <gitster@pobox.com>

Thus, it can be invoked as git-send-email's --cc-cmd option, among other possible uses.

This is a Perl rewrite of Felipe Contreras' git-related patch series[1] written in Ruby.

[1]: http://thread.gmane.org/gmane.comp.version-control.git/226065/
Signed-off-by: Eric Sunshine <sunshine@sunshineco.com>
---
 contrib/contacts/git-contacts | 127 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 127 insertions(+)
 create mode 100755 contrib/contacts/git-contacts
Show changes to contrib/contacts/git-contacts +127 −0
diff --git a/contrib/contacts/git-contacts b/contrib/contacts/git-contacts
new file mode 100755
index 0000000..6e9ab45
--- /dev/null
+++ b/contrib/contacts/git-contacts
@@ -0,0 +1,127 @@
+#!/usr/bin/perl
+
+# List people who might be interested in a patch.  Useful as the argument to
+# git-send-email --cc-cmd option, and in other situations.
+#
+# Usage: git contacts <file> ...
+
+use strict;
+use warnings;
+use IPC::Open2;
+
+my $since = '5-years-ago';
+my $min_percent = 10;
+my $labels_rx = qr/Signed-off-by|Reviewed-by|Acked-by|Cc/i;
+my %seen;
+
+sub format_contact {
+	my ($name, $email) = @_;
+	return "$name <$email>";
+}
+
+sub parse_commit {
+	my ($commit, $data) = @_;
+	my $contacts = $commit->{contacts};
+	my $inbody = 0;
+	for (split(/^/m, $data)) {
+		if (not $inbody) {
+			if (/^author ([^<>]+) <(\S+)> .+$/) {
+				$contacts->{format_contact($1, $2)} = 1;
+			} elsif (/^$/) {
+				$inbody = 1;
+			}
+		} elsif (/^$labels_rx:\s+([^<>]+)\s+<(\S+?)>$/o) {
+			$contacts->{format_contact($1, $2)} = 1;
+		}
+	}
+}
+
+sub import_commits {
+	my ($commits) = @_;
+	return unless %$commits;
+	my $pid = open2 my $reader, my $writer, qw(git cat-file --batch);
+	for my $id (keys(%$commits)) {
+		print $writer "$id\n";
+		my $line = <$reader>;
+		if ($line =~ /^([0-9a-f]{40}) commit (\d+)/) {
+			my ($cid, $len) = ($1, $2);
+			die "expected $id but got $cid\n" unless $id eq $cid;
+			my $data;
+			# cat-file emits newline after data, so read len+1
+			read $reader, $data, $len + 1;
+			parse_commit($commits->{$id}, $data);
+		}
+	}
+	close $reader;
+	close $writer;
+	waitpid($pid, 0);
+	die "git-cat-file error: $?\n" if $?;
+}
+
+sub get_blame {
+	my ($commits, $source, $start, $len, $from) = @_;
+	$len = 1 unless defined($len);
+	return if $len == 0;
+	open my $f, '-|',
+		qw(git blame --porcelain -C), '-L', "$start,+$len",
+		'--since', $since, "$from^", '--', $source or die;
+	while (<$f>) {
+		if (/^([0-9a-f]{40}) \d+ \d+ \d+$/) {
+			my $id = $1;
+			$commits->{$id} = { id => $id, contacts => {} }
+				unless $seen{$id};
+			$seen{$id} = 1;
+		}
+	}
+	close $f;
+}
+
+sub scan_patches {
+	my ($commits, $f) = @_;
+	my ($id, $source);
+	while (<$f>) {
+		if (/^From ([0-9a-f]{40}) Mon Sep 17 00:00:00 2001$/) {
+			$id = $1;
+			$seen{$id} = 1;
+		}
+		next unless $id;
+		if (m{^--- (?:a/(.+)|/dev/null)$}) {
+			$source = $1;
+		} elsif (/^--- /) {
+			die "Cannot parse hunk source: $_\n";
+		} elsif (/^@@ -(\d+)(?:,(\d+))?/ && $source) {
+			get_blame($commits, $source, $1, $2, $id);
+		}
+	}
+}
+
+sub scan_patch_file {
+	my ($commits, $file) = @_;
+	open my $f, '<', $file or die "read failure: $file: $!\n";
+	scan_patches($commits, $f);
+	close $f;
+}
+
+if (!@ARGV) {
+	die "No input patch files\n";
+}
+
+my %commits;
+for (@ARGV) {
+	scan_patch_file(\%commits, $_);
+}
+import_commits(\%commits);
+
+my %count_per_person;
+for my $commit (values %commits) {
+	for my $contact (keys %{$commit->{contacts}}) {
+		$count_per_person{$contact}++;
+	}
+}
+
+my $ncommits = scalar(keys %commits);
+for my $contact (keys %count_per_person) {
+	my $percent = $count_per_person{$contact} * 100 / $ncommits;
+	next if $percent < $min_percent;
+	print "$contact\n";
+}
-- 
1.8.3.2
Eric Sunshine· Jul 3, 2013, 20:59 UTC · re: Eric Sunshine · lore

[PATCH v2 2/3] contrib: contacts: add ability to parse from committish

For example:
  % git contacts master..feature

Committishes and patch files can be mentioned in the same invocation:

  % git contacts master..feature extra/*.patch
Signed-off-by: Eric Sunshine <sunshine@sunshineco.com>
---
 contrib/contacts/git-contacts | 38 ++++++++++++++++++++++++++++++++------
 1 file changed, 32 insertions(+), 6 deletions(-)
Show changes to contrib/contacts/git-contacts +32 −6
diff --git a/contrib/contacts/git-contacts b/contrib/contacts/git-contacts
index 6e9ab45..42e6059 100755
--- a/contrib/contacts/git-contacts
+++ b/contrib/contacts/git-contacts
@@ -3,7 +3,7 @@
 # List people who might be interested in a patch.  Useful as the argument to
 # git-send-email --cc-cmd option, and in other situations.
 #
-# Usage: git contacts <file> ...
+# Usage: git contacts <file | rev-list option> ...
 
 use strict;
 use warnings;
@@ -77,8 +77,8 @@ sub get_blame {
 }
 
 sub scan_patches {
-	my ($commits, $f) = @_;
-	my ($id, $source);
+	my ($commits, $id, $f) = @_;
+	my $source;
 	while (<$f>) {
 		if (/^From ([0-9a-f]{40}) Mon Sep 17 00:00:00 2001$/) {
 			$id = $1;
@@ -98,18 +98,44 @@ sub scan_patches {
 sub scan_patch_file {
 	my ($commits, $file) = @_;
 	open my $f, '<', $file or die "read failure: $file: $!\n";
-	scan_patches($commits, $f);
+	scan_patches($commits, undef, $f);
+	close $f;
+}
+
+sub scan_rev_args {
+	my ($commits, $args) = @_;
+	open my $f, '-|', qw(git rev-list --reverse), @$args or die;
+	while (<$f>) {
+		chomp;
+		my $id = $_;
+		$seen{$id} = 1;
+		open my $g, '-|', qw(git show -C --oneline), $id or die;
+		scan_patches($commits, $id, $g);
+		close $g;
+	}
 	close $f;
 }
 
 if (!@ARGV) {
-	die "No input patch files\n";
+	die "No input revisions or patch files\n";
 }
 
-my %commits;
+my (@files, @rev_args);
 for (@ARGV) {
+	if (-e) {
+		push @files, $_;
+	} else {
+		push @rev_args, $_;
+	}
+}
+
+my %commits;
+for (@files) {
 	scan_patch_file(\%commits, $_);
 }
+if (@rev_args) {
+	scan_rev_args(\%commits, \@rev_args)
+}
 import_commits(\%commits);
 
 my %count_per_person;
-- 
1.8.3.2
Eric Sunshine· Jul 3, 2013, 20:59 UTC · re: Eric Sunshine · lore

[PATCH v2 3/3] contrib: contacts: interpret committish akin to format-patch

As a convenience, accept the same style <since> committish as accepted by git-format-patch. For example:

  % git contacts master

will consider commits in the current branch built atop 'master', just as "git format-patch master" will format commits built atop 'master'.

Signed-off-by: Eric Sunshine <sunshine@sunshineco.com>
---
 contrib/contacts/git-contacts | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)
Show changes to contrib/contacts/git-contacts +18 −1
diff --git a/contrib/contacts/git-contacts b/contrib/contacts/git-contacts
index 42e6059..3cd0ea2 100755
--- a/contrib/contacts/git-contacts
+++ b/contrib/contacts/git-contacts
@@ -102,9 +102,26 @@ sub scan_patch_file {
 	close $f;
 }
 
+sub parse_rev_args {
+	my @args = @_;
+	open my $f, '-|',
+		qw(git rev-parse --revs-only --default HEAD --symbolic), @args
+		or die;
+	my @revs;
+	while (<$f>) {
+		chomp;
+		push @revs, $_;
+	}
+	close $f;
+	return @revs if scalar(@revs) != 1;
+	return "^$revs[0]", 'HEAD' unless $revs[0] =~ /^-/;
+	return $revs[0], 'HEAD';
+}
+
 sub scan_rev_args {
 	my ($commits, $args) = @_;
-	open my $f, '-|', qw(git rev-list --reverse), @$args or die;
+	my @revs = parse_rev_args(@$args);
+	open my $f, '-|', qw(git rev-list --reverse), @revs or die;
 	while (<$f>) {
 		chomp;
 		my $id = $_;
-- 
1.8.3.2

← back to recent threads