git-mailmap

October 21st, 2017
gitdist-zilla

git-mailmap

One of the many Dist-Zilla plugins that I use is Dist::Zilla::Plugin::Git:Contributors. It peruses the project’s commits and automatically draw the list of contributors, which is then added to the META.* files. And turned into the file CONTRIBUTORS if you use Dist::Zilla::Plugin::ContributorsFile as well.

But of course, sometimes a contributor will push commits from different machines, and they can thus appear under several names/email combos. For example, if we look at the contributors of the PPIx::EditorTools repo, we have:

$ git shortlog -sne
    51  Gabor Szabo <gabor@szabgab.com>
    20  Ahmad M. Zawawi <ahmad.zawawi@gmail.com>
    16  Yanick Champoux <yanick@cpan.org>
     5  Kevin Dawson <kevin@dawson10.plus.com>
     4  Adam Kennedy <adamk@cpan.org>
     4  Mark Grimes <mgrimes@cpan.org>
     4  Ryan Niebur <rsn@cpan.org>
     3  Steffen Mueller <smueller@cpan.org>
     3  kevindawson <bowtie@cpan.org>
     2  Sebastian Willing <sewi@cpan.org>
     2  Zeno Gantner <zeno.gantner@gmail.com>
     1  Florian Schlichting <fschlich@zedat.fu-berlin.de>
     1  bowtie <bowtie@cpan.org>

As we can see, Kevin Dawson is there a few times. It’d be nice to aggregate his different incarnations under a single entity.

Fortunately, we can. Easily too! Git has this concept of a .mailmap file that can maps author names/emails to canonical values (see the git shortlog documentation for all the details).

It’s easy. But… it’s also work. Wouldn’t it be better if there was a tool to help us populate the .mailmap?

… as you might surmise, as of a few hours ago there is one:

The video showcases pretty much all the action the script does. It’ll list the contributors of the current branch (with purty colors), and allow for a few actions:

  • /regex filters the authors with the given regular expression.
  • a Somebody <blah@blah.com> adds a new author entry (for when the canonical entry is not present)
  • A list of indexes take the first index to be the canonical address, and the other ones as the aliases. An asterisk, like in the video, can be used to say “all the selected authors”.
  • q means quit, natch.

If you want me to shut up and just take your money, the script can be found on GitHub.

If you want to know how it looks, soldier on.

How It’s Done

As usual, I leverage the heck of CPAN to do the minimal amount of work.

First, I use MooseX::App::Simple to take care of the clitudicity of the thing:

use 5.20.0;
use warnings;

use MooseX::App::Simple;

parameter regex => (
    is      => 'rw',
    default => '',
);

# ... more stuff will go here ...


__PACKAGE__->new_with_options->run;

Then to walk the logs, I reach out for Git::Wrapper.

use Git::Wrapper;
use List::AllUtils qw/ uniq /;

has authors => (
    is => 'rw',
    traits => [ 'Array' ],
    default => sub { [
        uniq( Git::Wrapper->new('.')
            ->RUN( 'log', { pretty => '%aN <%aE>' } ) )
    ] },
    handles => {
        all_authors  => 'sort',
        grep_authors => 'grep',
        add_author   => 'push',
    },
);

sub selected_authors {
    my $self = shift;

    my $regex = $self->regex or return $self->all_authors;

    return $self->grep_authors(sub { /$regex/i });
}

We’ll only append to .mailmap, so we could just use open. But I like Path::Tiny, and it looks nicer:

use Path::Tiny;

sub set_aliases {
    my ( $self, $line ) = @_;

    my @authors = $self->selected_authors;

    my( $real, @clones ) = 
        uniq map { $_ eq '*' ? @authors : $authors[$_] }
                split /,|s+/;

    path( '.mailmap' )->append( map { "$real $_\n" } @clones );

    $self->authors(
        [ $self->grep_authors(sub{ not $_ ~~ @clones } ) ]
    );
}

Saw how we have pretty colors? That’s all Term::ANSIColor’s fault:

use Term::ANSIColor qw/ colored /;

sub print_authors {
    my $self = shift;
    my @authors = $self->selected_authors;

    my $length = length scalar @authors;
    my $i = 0;

    printf colored( ['Blue'], '%'.$length."d" ) 
            . ". %-20s "
            . colored( [ 'Green' ], '%s' )."\n", 
            $i++, split /(?=<)/, $_ 
        for @authors;
}

Finally, for the interaction, I went for IO::Prompt::Simple:

use IO::Prompt::Simple;
use experimental 'smartmatch';

sub run {
    my $self = shift;

    while() {

        $self->print_authors;

        given ( prompt "[@{[ $self->regex ]}]" ) {

            $self->regex($1)       when m#^/(.*)#; 

            $self->add_author($1)  when /^a +(.*)/;

            $self->set_aliases($_) when /^d/;

            return                 when /^q/;
        }
    }
}

And there we go. A tool with help menu, colors, interaction, git perusing and file munging in 116 lines or so.

Not bad, heh?