public release
[git-grep-footer.git] / git-grep-footer
index b676ab8aef983e241ffb59921849d94075dd889d..97fbca7748b374a295e1758f43887ce6f14de8ef 100755 (executable)
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
 use 5.010;
 use strict;
 use warnings;
 use 5.010;
 use strict;
 use warnings;
@@ -7,40 +7,48 @@ use Encode 'decode';
 use Data::Dump 'pp';
 use Getopt::Long qw(:config bundling);
 
 use Data::Dump 'pp';
 use Getopt::Long qw(:config bundling);
 
+our $VERSION = '1.00';
+
 GetOptions(\my %opt,
        'debug!',
 GetOptions(\my %opt,
        'debug!',
+       '',  # stdin
        'count|c!',
        'simplify|s:s',
        'ignore-case|i!',
        'fuzzy!',
        'count|c!',
        'simplify|s:s',
        'ignore-case|i!',
        'fuzzy!',
+       'grep|S=s',
        'min|min-count|unique|u:i',
        'max|max-count|show|n:i',
        'min|min-count|unique|u:i',
        'max|max-count|show|n:i',
+       'hash|H!',
        'version|V'  => sub { Getopt::Long::VersionMessage() },
        'usage|h'    => sub { Getopt::Long::HelpMessage() },
        'help|man|?' => sub { Getopt::Long::HelpMessage(-verbose => 2) },
 ) or exit 129;
 
        'version|V'  => sub { Getopt::Long::VersionMessage() },
        'usage|h'    => sub { Getopt::Long::HelpMessage() },
        'help|man|?' => sub { Getopt::Long::HelpMessage(-verbose => 2) },
 ) or exit 129;
 
+my $inputstream = $opt{''} ? \*ARGV : eval {
+       require Git;
+       Git::command_output_pipe('log', '-z', '--pretty=format:%h%n%b', @ARGV);
+} || die "Automatic git log failed: $@";
+
 local $| = 1;
 local $/ = "\0";
 
 local $| = 1;
 local $/ = "\0";
 
-my $HEADERMATCH = qr/ [a-z]+ (?: (?:-\w+)+ | \ by ) | cc | reference /ix;
+my $HEADERMATCH = qr/ [a-z]+ (?: (?:-\w+)+ | \ by ) | cc | reference /imsx;
 
 my (%headercount, @headercache);
 
 
 my (%headercount, @headercache);
 
-while (readline) {
-       s/^([0-9a-f]{4,40})\n//m and
-       my $hash = $1;
+while (readline $inputstream) {
+       s/^ ([0-9a-f]{4,40}) \n//msx;
+       my $hash = $opt{hash} ? $1 : undef;
 
        # strip commit seperator
        chomp;
        # skip expensive checks without potential identifier
        m/:/ or next;
        # try to parse as UTF-8
 
        # strip commit seperator
        chomp;
        # skip expensive checks without potential identifier
        m/:/ or next;
        # try to parse as UTF-8
-       eval { $_ = decode(utf8   => $_, Encode::FB_CROAK()) };
+       eval { $_ = decode(utf8   => $_, Encode::FB_CROAK()); return 1 }
        # if invalid, assume it's latin1
        # if invalid, assume it's latin1
-              $_ = decode(cp1252 => $_) if $@;
-
-       my %attr;
+           or $_ = decode(cp1252 => $_);
 
        BLOCK:
        for (reverse split /\n\n/) {
 
        BLOCK:
        for (reverse split /\n\n/) {
@@ -49,14 +57,14 @@ while (readline) {
 
                LINE:
                for (split /\n/) {
 
                LINE:
                for (split /\n/) {
-                       next if not /\S/;
+                       next if not m/\S/;
                        my @header = m{
                                ^
                                (?<key> $HEADERMATCH)
                                : \s*
                        my @header = m{
                                ^
                                (?<key> $HEADERMATCH)
                                : \s*
-                               (?<val> \S .+)
+                               (?<val> \S [^\n]+)
                                $
                                $
-                       }imx or do {
+                       }imsx or do {
                                $prefix++;
                                next LINE;
                        };
                                $prefix++;
                                next LINE;
                        };
@@ -67,14 +75,18 @@ while (readline) {
                                for ($header[0]) {
                                        tr/ _/-/;
 
                                for ($header[0]) {
                                        tr/ _/-/;
 
-                                       state $BY = qr{ (?: -? b[yu] )? \Z }ix;
-                                       s{^ si (?:ge?n|n?g) (?:e?[dt])? -? (?:of+)? $BY}{Signed-off-by}ix;
-                                       s{^ ack (?:ed|de)?  $BY}{Acked-by}ix;
-                                       s{^ review (?:e?d)? $BY}{Reviewed-by}ix;
-                                       s{^ teste[dt]       $BY}{Tested-by}ix;
+                                       state $BY = qr{ (?: -? b[yu] )? \Z }imsx;
+                                       s{\A si (?:ge?n|n?g) (?:e?[dt])? -? (?:of+)? $BY}{Signed-off-by}imsx;
+                                       s{\A ack (?:ed|de)?  $BY}{Acked-by}imsx;
+                                       s{\A review (?:e?d)? $BY}{Reviewed-by}imsx;
+                                       s{\A teste[dt]       $BY}{Tested-by}imsx;
                                }
                        }
 
                                }
                        }
 
+                       if (defined $opt{grep}) {
+                               $_ ~~ qr/$opt{grep}/im or next LINE;
+                       }
+
                        given ($opt{simplify} // 'none') {
                                when (['email', 'authors']) {
                                        $header[1] =~ s{
                        given ($opt{simplify} // 'none') {
                                when (['email', 'authors']) {
                                        $header[1] =~ s{
@@ -85,7 +97,7 @@ while (readline) {
                                        }{<...>}imsx;
                                }
                                when (['var', 'vars', '']) {
                                        }{<...>}imsx;
                                }
                                when (['var', 'vars', '']) {
-                                       when ($header[0] =~ /[ _-] (?: by | to ) $ | ^cc$/imsx) {
+                                       when ($header[0] =~ m/[ _-] (?: by | to ) $ | ^cc$/imsx) {
                                                $header[1] = undef;
                                        }
                                        for ($header[1]) {
                                                $header[1] = undef;
                                        }
                                        for ($header[1]) {
@@ -123,11 +135,11 @@ while (readline) {
 
                for (@headers) {
                        my $line = $_->[2] // join(': ', @$_);
 
                for (@headers) {
                        my $line = $_->[2] // join(': ', @$_);
-                       $line =~ s/^/$hash / if defined $hash;
+                       $line =~ s/\A/$hash /msx if defined $hash;
 
                        if (defined $opt{min} or $opt{max} or $opt{count}) {
                                my $counter = \$headercount{ $_->[0] }->{ $_->[1] // '' };
 
                        if (defined $opt{min} or $opt{max} or $opt{count}) {
                                my $counter = \$headercount{ $_->[0] }->{ $_->[1] // '' };
-                               my $excess = $$counter++ - ($opt{min} // 0);
+                               my $excess = ${$counter}++ - ($opt{min} // 0);
                                next if $excess >= ($opt{max} || 1);
                                next if $excess <  0;
                                if ($opt{count}) {
                                next if $excess >= ($opt{max} || 1);
                                next if $excess <  0;
                                if ($opt{count}) {
@@ -154,7 +166,9 @@ git-grep-footer - Find custom header lines in commit messages
 
 =head1 SYNOPSIS
 
 
 =head1 SYNOPSIS
 
-F<git> log --pretty=%b%x00 | F<git-grep-footer> [OPTIONS]
+F<git-grep-footer> [OPTIONS] [-- <git log options>]
+
+F<git> log -z --pretty=format:%b | F<git-grep-footer> [OPTIONS] -
 
 =head1 DESCRIPTION
 
 
 =head1 DESCRIPTION
 
@@ -200,6 +214,11 @@ Values will be hidden entirely, so only attribute names remain.
 
 =back
 
 
 =back
 
+=item --grep=<pattern>
+
+Only include lines matching the specified regular expression.
+Case insensitivity can be disabled by prepending C<(?-i)>.
+
 =item -u, --unique[=<threshold>]
 
 Each match is only shown once,
 =item -u, --unique[=<threshold>]
 
 Each match is only shown once,
@@ -216,6 +235,37 @@ Additional samples are optionally given upto the given maximum.
 Prefixes (unique) lines by the number of occurrences.
 Causes output to be buffered until all input has been read (obviously).
 
 Prefixes (unique) lines by the number of occurrences.
 Causes output to be buffered until all input has been read (obviously).
 
+=item -H, --hash
+
+Prefixes the SHA1 hash of the (or a) matching commit.
+
+=back
+
+=head1 EXAMPLES
+
+=over
+
+=item git-grep-footer --grep=^ack v2.6.32..v2.6.33
+
+Search for I<Acked-by> lines for version I<v2.6.33>.
+Append C<-uin> to skip reoccurrences.
+
+=item git-grep-footer -u --grep=junio
+
+Show distinct lines mentioning a specific author.
+
+=item git-grep-footer -c --simplify --grep=^si
+
+Compare various capitalisations and (mis)spellings of signoffs.
+
+=item git-grep-footer -c --simplify=all -i | sort -n -r | head -n10
+
+List the ten most frequently used attribute names.
+
+=item git-grep-footer -n2 -i -s --hash -- --reverse
+
+The earliest two usages of each distinct identifier.
+
 =back
 
 =head1 AUTHOR
 =back
 
 =head1 AUTHOR
@@ -224,5 +274,7 @@ Mischa POSLAWSKY <perl@shiar.org>
 
 =head1 LICENSE
 
 
 =head1 LICENSE
 
-Copyright. All rights reserved.
+This software is free software;
+you can redistribute and/or modify it under the terms of the GNU GPL
+version 2 or later.