Grandma's Fridge

A TitanOfOld dev blog

A Betterer Ebuild workflow with Pure Git and pkgcheck and repoman...oh my!

Inspired by Michał Górny's post of a similar name, I've taken things a bit further by using hooks.

Never look at the command line with Git hooks and Magit!

I've been a long time user of Emacs. Recently, there's been some changes that have made it difficult to use repoman commit as a part of my ebuild workflow. (repoman wasn't waiting for Emacs, then thew a hissy fit that I hadn't written a message.) I set out to solve it when I stumbled upon Michał's post suggesting I just use pkgcheck. However, despite Michał's claim, repoman isn't quite obsolete.

A common error developers, the mere mortal ones anyway, encounter is committing an ebuild with stable keywords instead of the appropriate testing. Usually this comes about when doing version bumps as developers will just copy a previous version and move along. It's easy to change the keywords, but just as easy to miss. repoman will warn you that you're about to make the mistake, whereas pkgcheck keeps its mouth shut.

Further, it would be better if these checks happened in the correct place and automatically. I shouldn't have to remember to make sure the automatic systems will take it. The systems should tell me in advance that I have errors.

Git has hooks of which we can take advantage. Primarily the pre-commit, prepare-commit-msg, and post-commit. You'll find samples of these in the ~/top-level-of-repo/.git/hooks/ directory, but we'll be crafting our own.

Git hooks are never shared, and they're not version controlled. No one will know that you put them there. They'll only be on your machine. So, if you make a mistake, you won't hurt anyone but yourself. So, let's set some up and see what kind of trouble we can get into.

I'll be using Perl with Git::Repository as the only non-core module. It has a dependency on System::Command that we'll also use to ease a couple interactions/inspections with the system, of all things.

Git::Repository is, effectively, a thin wrapper around the git command.

1. $repo_root/.git/hooks/pre-commit

First up is the pre-commit script. Git will call this script as part of its git commit process. If the script returns nonzero, Git will halt the presses. You can provide the --no-verify option

#!/usr/bin/env perl
use v5.30;
use utf8;
use warnings;
use open qw(:std :utf8);
use feature qw(signatures);
no warnings qw(experimental::signatures);

# The above are pretty standard boilerplate. Waiting for Perl 7 to make it go
# away...mostly

# The next three are what we'll be using.
use File::Spec;
use Git::Repository;
use System::Command;

my $r = Git::Repository->new() or die;
my $ever_nonzero = 0;
say 'pre-commit running in ' . $r->work_tree;

# We don't want to run repoman in the same directory multiple times. So, we keep
# track of where we've been.
my %visited;

# Get a list of files names added/deleted/modified
for my $touched ($r->run(diff => "--staged", "--name-only")) {
  my @path = File::Spec->splitdir( $touched );
  # We only want repoman to work with cat/pkg. So, we look for that.
  if ($#path == 2 and $path[0] =~ m/^\w+-\w+$/) {
    my $rel_path = File::Spec->catdir(@path[0,1]);
    next if exists $visited{$rel_path};
    say "Running repoman for: " . $rel_path;
    my $cmd = System::Command->new('repoman', 'full', '-x', '-d', '-q',
                                   {cwd => File::Spec->rel2abs($rel_path)});

    my ( @output, @errput );
    $cmd->loop_on(
        input_record_separator => "\n",
        stdout => sub { chomp( my $o = shift ); push @output, "\t$o"; },
        stderr => sub { chomp( my $e = shift ); push @errput, "\t$e"; },
    );
    say for @output;
    say for @errput;

    $ever_nonzero = 1 if $cmd->close->exit != 0;

    for (@output, @errput) {
      if (m/digest\.unused/) {
        say "spotted unused digest";
        $ever_nonzero = 1;
      }
    }

    $visited{$rel_path}++;
  }
}

say 'pre-commit done in ' . $r->work_tree;
exit $ever_nonzero;

2. $repo_root/.git/hooks/prepare-commit-msg

We'll use this to guess a proper first line. This can save us from having to copy and paste. If we need a different first line, well that's just two keystrokes to trash.

#!/usr/bin/env perl
use v5.30;
use utf8;
use warnings;
use open qw(:std :utf8);
use feature qw(signatures);
no warnings qw(experimental::signatures);

use File::Basename;
use File::Spec;
use Git::Repository;
use System::Command;
use Tie::File;

my $r = Git::Repository->new() or die;
say 'prepare-commit-msg running in ' . $r->work_tree;

tie my @msg, 'Tie::File', $ARGV[0] or die;

for my $touched ($r->run(diff => "--staged", "--name-only")) {
  my @path = File::Spec->splitdir( $touched );
  if ($#path == 2 and $path[0] =~ m/^\w+-\w+$/) {
    $msg[0] = File::Spec->catdir(@path[0,1]) . ": ";
    last; # Stop when we've got a cat/pkg
  } else {
    $msg[0] = dirname( $touched ) . ": ";
  }
}

say 'prepare-commit-msg finished in ' . $r->work_tree;

3. $repo_root/.git/hooks/post-commit

This is where we actually call pkgcheck. While post-commit can't keep us from moving things along, it can let us know if there are other issues in the directories we've touched that should be addressed.

#!/usr/bin/env perl
use v5.30;
use utf8;
use warnings;
use open qw(:std :utf8);
use feature qw(signatures);
no warnings qw(experimental::signatures);

use File::Basename;
use Git::Repository;
use System::Command;
use Data::Dumper;

my $r = Git::Repository->new() or die;
my $ever_nonzero = 0;
say 'post-commit running in ' . $r->work_tree;

# Start pkgcheck
say "Running pkgcheck for...";

my %visited;
for my $touched ($r->run(diff => '--name-only', 'HEAD^..HEAD')) {
  my $rel_path = dirname( $touched );
  next if exists $visited{$rel_path};

  my $cmd = System::Command->new('pkgcheck', 'scan', '--net', '.',
                               {cwd => File::Spec->rel2abs($rel_path)});

  my ( @output, @errput );
  $cmd->loop_on(
                input_record_separator => "\n",
                stdout => sub { chomp( my $o = shift ); push @output, "\t$o"; },
                stderr => sub { chomp( my $e = shift ); push @errput, "\t$e"; },
               );
  say for @output;
  say for @errput;

  $ever_nonzero = 1 if $cmd->close->exit != 0;

  $visited{$rel_path}++;
}

say 'post-commit checks done in ' . $r->work_tree;
exit $ever_nonzero;

4. $repo_root/.git/hooks/pre-push

Forthcoming. We'll use this to prevent pushing when pkgcheck has a fatal error.

5. Conclusion

Some long bits in there.

While I'm not terribly fond of moving away from the command line, not having to leave Emacs has been a boon.

Especially after watching some of System Crafters videos.