The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl

package Git::Hooks::GerritChangeId;
# ABSTRACT: Git::Hooks plugin to insert a Change-Id in a commit message
$Git::Hooks::GerritChangeId::VERSION = '2.5.0';
use 5.010;
use utf8;
use strict;
use warnings;
use Git::Hooks;
use Git::Message;
use Path::Tiny;
use Carp;
use Try::Tiny;

(my $CFG = __PACKAGE__) =~ s/.*::/githooks./;

##########

sub gen_change_id {
    my ($git, $msg) = @_;

    my $filename = Path::Tiny->tempfile(UNLINK => 1);
    open my $fh, '>', $filename ## no critic (RequireBriefOpen)
        or $git->fault("Internal error: can't open '$filename' for writing:", {details => $!})
        and die;

    foreach my $info (
        [ tree      => [qw/write-tree/] ],
        [ parent    => [qw/rev-parse HEAD^0/] ],
        [ author    => [qw/var GIT_AUTHOR_IDENT/] ],
        [ committer => [qw/var GIT_COMMITTER_IDENT/] ],
    ) {
        # It's OK if we can't find info.
        try {
            $fh->print($info->[0], ' ', scalar($git->run(@{$info->[1]})));
        };
    }

    $fh->print("\n", $msg);
    $fh->close();

    return 'I' . $git->run(qw/hash-object -t blob/, $filename);
}

sub insert_change_id {
    my ($git, $msg) = @_;

    # Does Change-Id: already exist? if so, exit (no change).
    return if $msg =~ /^Change-Id:/im;

    my $cmsg = Git::Message->new($msg);

    # Don't mess with the message if it's empty.
    if (   (! defined $cmsg->title || $cmsg->title !~ /\S/)
        && (! defined $cmsg->body  || $cmsg->body  !~ /\S/)
       ) {
        # (Signed-off-by footers don't count.)
        my @footer = $cmsg->get_footer_keys;
        return if @footer == 0 || @footer == 1 && $footer[0] eq 'signed-off-by';
    }

    # Insert the Change-Id footer
    $cmsg->add_footer_values('Change-Id' => gen_change_id($git, $cmsg->as_string));

    return $cmsg->as_string;
}

sub rewrite_message {
    my ($git, $commit_msg_file) = @_;

    my $msg = eval { $git->read_commit_msg_file($commit_msg_file) };
    unless (defined $msg) {
        $git->fault("Internal error: cannot read commit message file '$commit_msg_file'",
                    {details => $@});
        return 0;
    }

    # Rewrite the message file
    if (my $new_msg = insert_change_id($git, $msg)) {
        $git->write_commit_msg_file($commit_msg_file, $new_msg);
    }

    return 1;
}

# Install hooks
COMMIT_MSG \&rewrite_message;

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Git::Hooks::GerritChangeId - Git::Hooks plugin to insert a Change-Id in a commit message

=head1 VERSION

version 2.5.0

=head1 SYNOPSIS

As a C<Git::Hooks> plugin you don't use this Perl module directly. Instead, you
may configure it in a Git configuration file like this:

  [githooks]
    plugin = CheckGerritChangeId
    admin = joe molly

The first section enables the plugin and defines the users C<joe> and C<molly>
as administrators, effectivelly exempting them from any restrictions the plugin
may impose.

=head1 DESCRIPTION

This L<Git::Hooks> plugin hooks itself to the C<commit-msg> hook. It is a
reimplementation of Gerrit's official commit-msg hook for inserting
change-ids in git commit messages.  It's does not produce the same
C<Change-Id> for the same message, but this is not really necessary, since
it keeps existing Change-Id footers unmodified.

(What follows is a partial copy of that document's DESCRIPTION
section.)

This plugin automatically inserts a globally unique Change-Id tag in
the footer of a commit message. When present, Gerrit uses this tag to
track commits across cherry-picks and rebases.

After the hook has been installed in the user's local Git repository
for a project, the hook will modify a commit message such as:

    Improve foo widget by attaching a bar.
    
    We want a bar, because it improves the foo by providing more
    wizbangery to the dowhatimeanery.
    
    Signed-off-by: A. U. Thor <author@example.com>

by inserting a new C<Change-Id: > line in the footer:

    Improve foo widget by attaching a bar.
    
    We want a bar, because it improves the foo by providing more
    wizbangery to the dowhatimeanery.
    
    Change-Id: Ic8aaa0728a43936cd4c6e1ed590e01ba8f0fbf5b
    Signed-off-by: A. U. Thor <author@example.com>

The hook implementation is reasonably intelligent at inserting the
Change-Id line before any Signed-off-by or Acked-by lines placed at
the end of the commit message by the author, but if no such lines are
present then it will just insert a blank line, and add the Change-Id
at the bottom of the message.

If a Change-Id line is already present in the message footer, the
script will do nothing, leaving the existing Change-Id
unmodified. This permits amending an existing commit, or allows the
user to insert the Change-Id manually after copying it from an
existing change viewed on the web.

To enable the plugin you should add it to the githooks.plugin
configuration option:

    git config --add githooks.plugin GerritChangeId

=for Pod::Coverage gen_change_id insert_change_id rewrite_message

=head1 NAME

Git::Hooks::GerritChangeId - Git::Hooks plugin to insert Change-Ids in
commit messages

=head1 CONFIGURATION

There's no configuration needed or provided.

=head1 REFERENCES

=over

=item * L<Gerrit's Home Page|http://gerrit.googlecode.com/>

=item * L<Gerrit's official commit-msg
hook|https://gerrit.googlesource.com/gerrit/+/master/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg>

=item * L<Gerrit's official hook
documentation|https://gerrit.googlesource.com/gerrit/+/master/Documentation/cmd-hook-commit-msg.txt>

=back

=head1 AUTHOR

Gustavo L. de M. Chaves <gnustavo@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2018 by CPqD <www.cpqd.com.br>.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut