1
+#!/usr/bin/perl
2
+
3
+# List people who might be interested in a patch. Useful as the argument to
4
+# git-send-email --cc-cmd option, and in other situations.
5
+#
6
+# Usage: git contacts <file> ...
7
+
8
+use strict;
9
+use warnings;
10
+use IPC::Open2;
11
+
12
+my $since = '5-years-ago';
13
+my $min_percent = 10;
14
+my $labels_rx = qr/Signed-off-by|Reviewed-by|Acked-by|Cc/i;
15
+my %seen;
16
+
17
+sub format_contact {
18
+ my ($name, $email) = @_;
19
+ return "$name <$email>";
20
+}
21
+
22
+sub parse_commit {
23
+ my ($commit, $data) = @_;
24
+ my $contacts = $commit->{contacts};
25
+ my $inbody = 0;
26
+ for (split(/^/m, $data)) {
27
+ if (not $inbody) {
28
+ if (/^author ([^<>]+) <(\S+)> .+$/) {
29
+ $contacts->{format_contact($1, $2)} = 1;
30
+ } elsif (/^$/) {
31
+ $inbody = 1;
32
+ }
33
+ } elsif (/^$labels_rx:\s+([^<>]+)\s+<(\S+?)>$/o) {
34
+ $contacts->{format_contact($1, $2)} = 1;
35
+ }
36
+ }
37
+}
38
+
39
+sub import_commits {
40
+ my ($commits) = @_;
41
+ return unless %$commits;
42
+ my $pid = open2 my $reader, my $writer, qw(git cat-file --batch);
43
+ for my $id (keys(%$commits)) {
44
+ print $writer "$id\n";
45
+ my $line = <$reader>;
46
+ if ($line =~ /^([0-9a-f]{40}) commit (\d+)/) {
47
+ my ($cid, $len) = ($1, $2);
48
+ die "expected $id but got $cid\n" unless $id eq $cid;
49
+ my $data;
50
+ # cat-file emits newline after data, so read len+1
51
+ read $reader, $data, $len + 1;
52
+ parse_commit($commits->{$id}, $data);
53
+ }
54
+ }
55
+ close $reader;
56
+ close $writer;
57
+ waitpid($pid, 0);
58
+ die "git-cat-file error: $?\n" if $?;
59
+}
60
+
61
+sub get_blame {
62
+ my ($commits, $source, $start, $len, $from) = @_;
63
+ $len = 1 unless defined($len);
64
+ return if $len == 0;
65
+ open my $f, '-|',
66
+ qw(git blame --porcelain -C), '-L', "$start,+$len",
67
+ '--since', $since, "$from^", '--', $source or die;
68
+ while (<$f>) {
69
+ if (/^([0-9a-f]{40}) \d+ \d+ \d+$/) {
70
+ my $id = $1;
71
+ $commits->{$id} = { id => $id, contacts => {} }
72
+ unless $seen{$id};
73
+ $seen{$id} = 1;
74
+ }
75
+ }
76
+ close $f;
77
+}
78
+
79
+sub scan_patches {
80
+ my ($commits, $f) = @_;
81
+ my ($id, $source);
82
+ while (<$f>) {
83
+ if (/^From ([0-9a-f]{40}) Mon Sep 17 00:00:00 2001$/) {
84
+ $id = $1;
85
+ $seen{$id} = 1;
86
+ }
87
+ next unless $id;
88
+ if (m{^--- (?:a/(.+)|/dev/null)$}) {
89
+ $source = $1;
90
+ } elsif (/^--- /) {
91
+ die "Cannot parse hunk source: $_\n";
92
+ } elsif (/^@@ -(\d+)(?:,(\d+))?/ && $source) {
93
+ get_blame($commits, $source, $1, $2, $id);
94
+ }
95
+ }
96
+}
97
+
98
+sub scan_patch_file {
99
+ my ($commits, $file) = @_;
100
+ open my $f, '<', $file or die "read failure: $file: $!\n";
101
+ scan_patches($commits, $f);
102
+ close $f;
103
+}
104
+
105
+if (!@ARGV) {
106
+ die "No input patch files\n";
107
+}
108
+
109
+my %commits;
110
+for (@ARGV) {
111
+ scan_patch_file(\%commits, $_);
112
+}
113
+import_commits(\%commits);
114
+
115
+my $contacts = {};
116
+for my $commit (values %commits) {
117
+ for my $contact (keys %{$commit->{contacts}}) {
118
+ $contacts->{$contact}++;
119
+ }
120
+}
121
+
122
+my $ncommits = scalar(keys %commits);
123
+for my $contact (keys %$contacts) {
124
+ my $percent = $contacts->{$contact} * 100 / $ncommits;
125
+ next if $percent < $min_percent;
126
+ print "$contact\n";
127
+}