From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

# ABSTRACT: Compute difference between two revisions
use Moose;
use MooseX::Types::Moose qw(ArrayRef Bool);
use MooseX::MarkAsMethods ( autoclean => 1 );
use Pinto::Constants qw(:diff);
use Pinto::Types qw(DiffStyle);
use Pinto::Util qw(itis default_diff_style);
use overload ( q{""} => 'to_string' );
#------------------------------------------------------------------------------
our $VERSION = '0.14'; # VERSION
#------------------------------------------------------------------------------
has left => (
is => 'ro',
isa => 'Pinto::Schema::Result::Revision',
required => 1,
);
has right => (
is => 'ro',
isa => 'Pinto::Schema::Result::Revision',
required => 1,
);
has entries => (
traits => [qw(Array)],
handles => { entries => 'elements' },
isa => ArrayRef ['Pinto::DifferenceEntry'],
builder => '_build_diffs',
init_arg => undef,
lazy => 1,
);
has additions => (
traits => [qw(Array)],
handles => { additions => 'elements' },
isa => ArrayRef ['Pinto::DifferenceEntry'],
default => sub { [ grep { $_->op eq '+' } shift->entries ] },
init_arg => undef,
lazy => 1,
);
has deletions => (
traits => [qw(Array)],
handles => { deletions => 'elements' },
isa => ArrayRef ['Pinto::DifferenceEntry'],
default => sub { [ grep { $_->op eq '-' } shift->entries ] },
init_arg => undef,
lazy => 1,
);
has is_different => (
is => 'ro',
isa => Bool,
init_arg => undef,
default => sub { shift->entries > 0 },
lazy => 1,
);
has style => (
is => 'ro',
isa => DiffStyle,
default => \&default_diff_style,
);
#------------------------------------------------------------------------------
around BUILDARGS => sub {
my $orig = shift;
my $class = shift;
my $args = $class->$orig(@_);
# The left and right attributes can also be Stack objects.
# In those cases, we just use the head revision of the Stack
for my $side (qw(left right)) {
if ( $args->{$side}->isa('Pinto::Schema::Result::Stack') ) {
$args->{$side} = $args->{$side}->head;
}
}
return $args;
};
#------------------------------------------------------------------------------
sub _build_diffs {
my ($self) = @_;
# We want to find the registrations that are "different" in either
# side. Two registrations are the same if they have the same values in
# the package, distribution, and is_pinned columns. So we use these
# columns to construct the keys of a hash. The value is the id of
# the registration. For a concise diff, we just use the distribution
# and is_pinned columns, which effectively groups the records so there
# is only one diff entry per distribution. In that case, the package
# referenced by the registration won't be meaningful.
my @fields = $self->style eq $PINTO_DIFF_STYLE_DETAILED
? qw(distribution package is_pinned)
: qw(distribution is_pinned);
my $cb = sub {
my $value = $_[0]->id;
my $key = join '|', map { $_[0]->get_column($_) } @fields;
return ( $key => $value );
};
my $attrs = { select => [ 'id', @fields ] };
my %left = $self->left->registrations( {}, $attrs )->as_hash($cb);
my %right = $self->right->registrations( {}, $attrs )->as_hash($cb);
# Now that we have hashes representing the left and right, we use
# the keys as "sets" and compute the difference between them. Keys
# present on the right but not on the left have been added. And
# those present on left but not on the right have been deleted.
my @add_ids = @right{ grep { not exists $left{$_} } keys %right };
my @del_ids = @left{ grep { not exists $right{$_} } keys %left };
# Now we have the ids of all the registrations that were added or
# deleted between the left and right revisions. We use those ids to
# requery the database and construct full objects for each of them.
my @adds = $self->_create_entries( '+', $self->right, \@add_ids );
my @dels = $self->_create_entries( '-', $self->left, \@del_ids );
# Strictly speaking, the registrations are an unordered list. But
# the diff is more readable if we group registrations together by
# distribution name.
my @diffs = sort @dels, @adds;
return \@diffs;
}
#------------------------------------------------------------------------------
sub _create_entries {
my ( $self, $type, $side, $ids ) = @_;
# The number of ids is potentially pretty big (1000's) and we
# can't use that many values in an IN clause. So we insert all
# those ids into a temporary table.
my $tmp_tbl = "__diff_${$}__";
my $dbh = $self->right->result_source->schema->storage->dbh;
$dbh->do("CREATE TEMP TABLE $tmp_tbl (reg INTEGER NOT NULL)");
my $sth = $dbh->prepare("INSERT INTO $tmp_tbl VALUES( ? )");
$sth->execute($_) for @{$ids};
# Now fetch the actual Registration objects (with all their
# related objects) for each id in the temp table. Finally,
# map all the Registrations into DifferenceEntry objects.
my $where = { 'me.id' => { in => \"SELECT reg from $tmp_tbl" } };
my $reg_rs = $side->registrations($where)->with_distribution->with_package;
my @entries = map { Pinto::DifferenceEntry->new( op => $type,
registration => $_ ) } $reg_rs->all;
$dbh->do("DROP TABLE $tmp_tbl");
return @entries;
}
#------------------------------------------------------------------------------
sub foreach {
my ( $self, $cb ) = @_;
$cb->($_) for $self->entries;
return $self;
}
#------------------------------------------------------------------------------
sub to_string {
my ($self) = @_;
my $format = $self->style eq $PINTO_DIFF_STYLE_CONCISE
? '%o[%F] %a/%f'
: '';
return join("\n", map {$_->to_string($format) } $self->entries) . "\n";
}
#------------------------------------------------------------------------------
__PACKAGE__->meta->make_immutable;
#------------------------------------------------------------------------------
1;
__END__
=pod
=encoding UTF-8
=for :stopwords Jeffrey Ryan Thalhammer
=head1 NAME
Pinto::Difference - Compute difference between two revisions
=head1 VERSION
version 0.14
=head1 AUTHOR
Jeffrey Ryan Thalhammer <jeff@stratopan.com>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2015 by Jeffrey Ryan Thalhammer.
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