How I'm using Dist::Zilla to give credit to contributors

Recently, Gabor Szabo asked me how to list contributors in the META file of CPAN distributions. This seemed like a great idea to me -- I'd love to be able to credit people who contribute to my modules. I suggested using an "x_contributors" key in META.json with an array of names and email addresses, just like the "author" field has.

Unfortunately, I'm just lazy enough that I wouldn't want to maintain a contributors list by hand. At best, I note additions in Changes. If you're lucky. And catch me on a good day.

Fortunately, I love Dist::Zilla. I let it automate as much as I can of my distribution packaging, documentation and release process.

If you don't love Dist::Zilla, too, Gabor has written a tutorial on adding contributors to CPAN META files including other build systems.

As I was emailing back to Gabor, I realized that the easy thing to do is get contributors from the commit history. I looked on CPAN to see if anyone was doing something like that already.

Bingo! There was Dist::Zilla::Plugin::ContributorsFromGit waiting for me! That plugin, when combined with Pod::Weaver and Pod::Weaver::Section::Contributors, automatically grabs the commit authors list and creates a "CONTRIBUTORS" section in pod. All that was missing was dumping that same data into the META file. One pull request later, Chris Weyl shipped a new version that did just that.

Now, every distribution I release with my Dist::Zilla plugin bundle automatically credits my contributors both in the META files and the Pod.

You can see examples here and here.

If websites like metacpan.org start using the "x_contributors" key for some cool mashup, all my recent distributions will be ready and waiting.

The rest of this article will show you how you can do this, too.

An example distribution

I'm not going to give a full Dist::Zilla (dzil) tutorial here. If you're new to dzil, go see the Dist::Zilla tutorial site.

For those who know how I use dzil, I'm showing how to do this long-hand without my plugin bundle -- that would just confuse people.

You can follow along from this git repository: http://github.com/dagolden/zzz-givecreditwithdzil

First, I created a new distribution:

$ dzil new Acme::GiveCreditWithDzil

Then, I edited the dist.ini file for a pretty minimal dzil setup. It automatically sets a date-based version number. It creates a META.json. It has the PodWeaver plugin to automatically generate Pod sections like AUTHOR and LICENSE. It generates a README from the main module Pod. (Browse the dist.ini on github.)

Next, I wanted to add some committers. Normally, they'd send pull requests, but for this demonstration, I can add them with empty commits:

$ git commit --allow-empty --author="Ricardo Signes <rjbs@cpan.org>" -m "..."

This is really useful! If you have contributors on a project who didn't send you pull requests, you can still record the fact of their contribution in your git history and let dzil automatically include them in the contributors list later.

I added a few more:

$ git commit --allow-empty --author="Keedi Kim - 김도형 <keedi@cpan.org>" -m "..."
$ git commit --allow-empty --author="Chris Weyl <cweyl@alumni.drew.edu>" -m "..."
$ git commit --allow-empty --author="Rik Signes <www@rjbs.manxome.org>" -m "..."

Note that Keedi has Unicode in the author name. Also note that Ricardo is there twice, with different spellings and email addresses.

Next, I added the ContributorsFromGit plugin to the dist.ini:

--- a/dist.ini
+++ b/dist.ini
@@ -16,6 +16,9 @@ copyright_year   = 2013
 ; add $VERSION to module
 [PkgVersion]

+; gather contributors
+[ContributorsFromGit]
+
 ; generate pod sections
 [PodWeaver]

Then, after running dzil build, my META.json had a section that looked like this:

   "x_contributors" : [
      "Chris Weyl <cweyl@alumni.drew.edu>",
      "Keedi Kim - \u00ea\u00b9\u0080\u00eb\u008f\u0084\u00ed\u0098\u0095 <keedi@cpan.org>",
      "Ricardo Signes <rjbs@cpan.org>",
      "Rik Signes <www@rjbs.manxome.org>"
   ]

You can see the duplicate entries for Ricardo, but I'll show you how I fixed that later.

Next, I added the Pod::Weaver::Section::Contributors plugin to my weaver.ini file:

diff --git a/weaver.ini b/weaver.ini
index 7223daf..18494c2 100644
--- a/weaver.ini
+++ b/weaver.ini
@@ -2,3 +2,5 @@

 [-Transformer]
 transformer = List
+
+[Contributors]

Then, running dzil build created a new Pod section. Looking at the README generated from Pod, I saw this:

CONTRIBUTORS
    *   Chris Weyl <cweyl@alumni.drew.edu>

    *   Keedi Kim - 김도형 <keedi@cpan.org>

    *   Ricardo Signes <rjbs@cpan.org>

    *   Rik Signes <www@rjbs.manxome.org>

Unfortunately, Pod::Weaver isn't very smart about Unicode by default, so I got this when I ran perldoc on the generated file:

CONTRIBUTORS
       ·   Chris Weyl <cweyl@alumni.drew.edu>

       ·   Keedi Kim - ê¹<U+0080>ë<U+008F><U+0084>í<U+0098><U+0095> <keedi@cpan.org>

       ·   Ricardo Signes <rjbs@cpan.org>

       ·   Rik Signes <www@rjbs.manxome.org>

Oops.

I fixed that and Rik's name with a somewhat undocumented feature of git: the .mailmap file. Put simply, it remaps commit author name and email address.

Here's one I created to map Ricardo's commits together and strip the Unicode characters from Keedi (sorry!):

Keedi Kim <keedi@cpan.org>
Ricardo Signes <rjbs@cpan.org> <www@rjbs.manxome.org>

After that, here's what I got from perldoc:

CONTRIBUTORS
       ·   Chris Weyl <cweyl@alumni.drew.edu>

       ·   Keedi Kim <keedi@cpan.org>

       ·   Ricardo Signes <rjbs@cpan.org>

Excellent!

I've since learned that I can add "=encoding utf-8" to the top of my Pod and the Unicode bits will work, but I'm still leery of Pod::Weaver and UTF-8, so I tend not to rely on it.

Stepping back

I walked you through that step-by-step, but if I look at the diff between the original distribution and the one that reports contributors, the difference is two plugins and a .mailmap file:

diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..f229d28
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,2 @@
+Keedi Kim <keedi@cpan.org>
+Ricardo Signes <rjbs@cpan.org> <www@rjbs.manxome.org>
diff --git a/dist.ini b/dist.ini
index 4c0a7d8..a501a49 100644
--- a/dist.ini
+++ b/dist.ini
@@ -16,6 +16,9 @@ copyright_year   = 2013
 ; add $VERSION to module
 [PkgVersion]

+; gather contributors
+[ContributorsFromGit]
+
 ; generate pod sections
 [PodWeaver]

diff --git a/weaver.ini b/weaver.ini
index 7223daf..18494c2 100644
--- a/weaver.ini
+++ b/weaver.ini
@@ -2,3 +2,5 @@

 [-Transformer]
 transformer = List
+
+[Contributors]

That's four non-whitespace lines.

If you use Dist::Zilla, giving credit to contributors is that easy!

You've got no excuse. Even if you don't put Contributors in Pod, use ContributorsFromGit and put it into your metadata. Let's give credit where credit is due.

Posted in Uncategorized | 4 Responses

HTTP::Tiny now with cookies

In early March, the NY Perl Mongers hosted the first NY.pm hackathon in a space generously provided by the Rubenstein Technology Group.

One of the projects I was pleased to see completed was adding cookie jar support to HTTP::Tiny. A few weeks prior, I had released HTTP::CookieJar in preparation. At the event, Edward Zborowski from Rubenstein volunteered to bring the two together.

Thank you, Edward!

Now, persistent cookie support for HTTP::Tiny is as easy as this:

use HTTP::Tiny;
use HTTP::CookieJar;
use Path::Tiny;

my $jar_file = path("jar.txt");

$jar_file->touch;

my $jar = HTTP::CookieJar->new->load_cookies( $jar_file->lines );

my $ua = HTTP::Tiny->new( cookie_jar => $jar );

# ... do stuff that needs cookies ...

$jar_file->spew( join "\n", $jar->dump_cookies( {persistent => 1} ) );
Posted in perl programming | Tagged , , , , | 2 Responses

CPAN is for experimentation and I hope that never changes

This post is a response to Neil Bower's post Don't release experiments to CPAN.

Neil, respectfully, I disagree.

The huge success of CPAN is, I think, in large part because it encouraged experimentation and alternatives.

Suggesting people upload elsewhere (Github) and have module installers use that just creates barriers to participation and barriers to participation are the last thing that Perl needs.

At one time we had the "registered modules" list. That fell out of favor and usage. Why? I think, in part because it couldn't keep up with the pace of innovation on CPAN. (Possibly, because the approvers couldn't keep up or became a bottleneck themselves.)

We have a search problem and a problem finding "recommended" modules. But that's an output issue, not an input issue.

Think about Amazon. They offer the "long tail" of books, but make it easy to find things. Bestseller lists. Recommendations. Reviews.

We should be focusing on helping people find recommended modules. Task::Kensho. Your own excellent review articles.

Suggesting people not upload code because it's not "worthy" is a recipe for disaster.

I'm the acting PAUSE admin for ID approvals (by which I mean I'm the Turing test to make sure applicants are people and not bots or spammers). Every week I see a regular trickle of brand new CPAN authors. I see their ideas when they apply for an account.

Are all ideas brilliant? Are they going to replace some existing module or revolutionize something on their first try? Probably not. But they are contributing. They are motivated by the idea of giving back to the community.

Some of these authors, someday, will start contributing things that do matter.

Telling them not to upload to CPAN until they have something good enough is a terrible idea. Telling them to keep their ideas on other forums until they're good enough is a terrible idea.

If we do that, we might as well just tell them to go away.

Posted in cpan, perl programming | Tagged , , , | 12 Responses

How to move CPAN RT tickets to Github

Most of my new CPAN modules use Github for issue tracking because of the nice integration with pull requests. I recently wanted to migrate an older distribution to using Github, but didn't want to track tickets in two places.

A while ago, Yanick Champoux wrote Bandying tickets from RT to Github, which looked like exactly what I wanted. Unfortunately, it used the old Github API, which is no longer supported.

I've fixed it up and am sharing it here. If you have your Github user and an OAuth token in your git config file as github.user and github.token, it will use those as defaults. Ditto if you have your PAUSE credentials in a .pause file already for uploading.

If you don't have a github OAuth token, get one with this script, which is right out of the Net::Github SYNOPSIS:

#!/usr/bin/env perl
use v5.10;
use strict;
use warnings;
use autodie;

use Net::GitHub::V3;
use IO::Prompt::Tiny qw/prompt/;

my $user = prompt( "Github username:" );
my $pass = prompt( "Github password:" );
my $gh = Net::GitHub::V3->new( login => $user, pass => $pass );
my $oauth = $gh->oauth;
my $o = $oauth->create_authorization( {
    scopes => ['user', 'public_repo', 'repo', 'gist'], # just ['public_repo']
    note   => 'Net::GitHub',
} );
say $o->{token};

Here is the script that does the work. There are a bunch of heuristics that are very specific to my way of working (like getting the distribution name out of a dist.ini file), but those only set prompt defaults so you should be able to use this as is or tweak it to your needs.

#!/usr/bin/env perl
use v5.10;
use strict;
use warnings;
use Carp;
use IO::Prompt::Tiny qw/prompt/;
use Net::GitHub;
use Path::Tiny;
use RT::Client::REST::Ticket;
use RT::Client::REST;
use Syntax::Keyword::Junction qw/any/;

sub _git_config {
    my $key = shift;
    chomp( my $value = `git config --get $key` );
    croak "Unknown $key" unless $value;
    return $value;
}

my $pause_rc = path( $ENV{HOME}, ".pause" );
my %pause;

sub _pause_rc {
    my $key = shift;
    if ( $pause_rc->exists && !%pause ) {
        %pause = split " ", $pause_rc->slurp;
    }
    return $pause{$key} // '';
}

sub _dist_name {
    # dzil only for now
    my $dist = path("dist.ini");
    if ( $dist->exists ) {
        my ($first) = $dist->lines( { count => 1 } );
        my ($name) = $first =~ m/name\s*=\s*(\S+)/;
        return $name if defined $name;
    }
    return '';
}

my $github_user       = prompt( "github user: ",  _git_config("github.user") );
my $github_token      = prompt( "github token: ", _git_config("github.token") );
my $github_repo_owner = prompt( "repo owner: ",   $github_user );
my $github_repo       = prompt( "repo name: ",    path(".")->absolute->basename );

my $rt_user = prompt( "PAUSE ID: ", _pause_rc("user") );
my $rt_password =
  _pause_rc("password") ? _pause_rc("password") : prompt("PAUSE password: ");
my $rt_dist = prompt( "RT dist name: ", _dist_name() );

my $gh = Net::GitHub->new( access_token => $github_token );
$gh->set_default_user_repo( $github_repo_owner, $github_repo );
my $gh_issue = $gh->issue;

my $rt = RT::Client::REST->new( server => 'https://rt.cpan.org/' );
$rt->login(
    username => $rt_user,
    password => $rt_password
);

# see which tickets we already have on the github side
my @gh_issues =
  map { /\[rt\.cpan\.org #(\d+)\]/ }
  map { $_->{title} }
  $gh_issue->repos_issues( $github_repo_owner, $github_repo, { state => 'open' } );

my @rt_tickets = $rt->search(
    type  => 'ticket',
    query => qq{
        Queue = '$rt_dist' 
        and
        ( Status = 'new' or Status = 'open' )
    },
);

for my $id (@rt_tickets) {

    if ( any(@gh_issues) eq $id ) {
        say "ticket #$id already on github";
        next;
    }

    # get the information from RT
    my $ticket = RT::Client::REST::Ticket->new(
        rt => $rt,
        id => $id,
    );
    $ticket->retrieve;

    # we just want the first transaction, which
    # has the original ticket description
    my $desc = $ticket->transactions->get_iterator->()->content;

    $desc =~ s/^/    /gms;

    my $subject = $ticket->subject;

    my $isu = $gh_issue->create_issue(
        {
            "title" => "$subject [rt.cpan.org #$id]",
            "body"  => "https://rt.cpan.org/Ticket/Display.html?id=$id\n\n$desc",
        }
    );

    say "ticket #$id ($subject) copied to github";
}

A more sophisticated version would import each of the RT comments as github comments, but this was enough for me to use Github's issue tracker and not lose track of things that were previously in RT.

Posted in git, perl programming | Tagged , , , , | 6 Responses

My Perl 6 post was really about Perl 5

As I see the comments about my Is Perl 6 pointless, hopeless or just not done? article, I realize that I've somehow misled people into thinking I was posing a conundrum about Perl 6.

I'm not.

In the various debates about the evolution of Perl 5 -- how fast it should evolve, whether it should break back compatibility, whether it should be renamed, whether it should be renumbered, etc. -- I find that I can cluster people based on their opinions of Perl 6.

Because Perl 6 promised so much and (so far) has failed to deliver, I find that Perl 6 opinions are a decent predictor of Perl 5 evolution opinions.

If you think Perl 6 is pointless -- or, more politely, just irrelevant to you -- then you probably strongly favor stability of the Perl 5 we have today.

If you find the potential of a backwards-incompatible step-change from Perl 5 appealing (as Perl 6 promises) but you don't think Perl 6 is going to deliver, then you probably favor either more radical evolution of Perl 5 or the possibility of a name change, version bump or fork to break back compatibility for the sake of progress.

If you find the potential of a backwards-incompatible step-change from Perl 5 appealing (as Perl 6 promises) and think that Perl 6 is relatively close, then you probably think Perl 5 evolution debates are tempests in a teapot and wonder why people aren't more involved in helping Perl 6. Or more practically, you may think that Perl 5 can continue to be a stable rock and people's energy is better spent on Perl 6.

[Update: raiph's comment reminded that me that among those who think Perl 6 is close I do find those who are interested in Perl 5-like forks as a stepping stone for Perl 5 to Perl 6 migration. But I don't generally see Perl 6 proponents arguing for major changes in the existing interpreter.]

How do those characterizations help me?

I've spent enough time in the guts of the Perl source and listed to the opinions of those with even great experience and expertise on it that I'm convinced that significant evolution to the Perl 5 interpreter is nearly impossible without a bigger break in compatibility than people are used to seeing in Perl 5.

If the wonders of Perl 6 aren't enough to convince you that you might someday want to switch to something incompatible with what you have today, then none of the far-less-ambitious ideas for Perl 5 are going to justify breaking anything and there's no point in having a debate with you about it.

The likelihood of Perl 5 evolving significantly will hinge on the balance between these groups.

If the pro-evolution but Perl-6-disillusioned group is larger or louder than the other two groups combined, then Perl 5 may yet make an evolutionary leap or there could even be a chance for a critical mass to build around a sufficiently awesome fork.

Otherwise, Perl 5 is more or less done evolving except in minor compatible ways and renaming it or doing anything else ambitious isn't going to succeed in changing the language, the community or external perceptions. We might get Perl 6 or we might not, but the Perl 5 we have now will be substantially the same a decade from now.

If the latter case is what it looks like we have, then I'm pretty much done advocating or participating in further changes in the core, because I think the barriers are too great. I'd rather spend my volunteer time in other areas where I can have more impact.

Posted in perl programming | Tagged , , , | 15 Responses

© 2009-2013 David Golden All Rights Reserved