The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package MyCPAN::App::DPAN::SVNPostFlight;
use strict;
use warnings;
use utf8;

use vars qw( $logger );

use Data::Dumper;
use IPC::Cmd;
use IPC::Run qw();
use IPC::System::Simple qw(capturex systemx);
use Log::Log4perl;
use XML::Simple;

=encoding utf8

=head1 NAME

MyCPAN::App::DPAN::SVNPostFlight - A No-op reports processor

=head1 SYNOPSIS

Use this from C<dpan> by specifying it as the C<postflight_class> class:

	# in dpan.conf
	postflight_class  MyCPAN::App::DPAN::SVNPostFlight
	postflight_dry_run 1

=head1 DESCRIPTION

This class is an example for a user-defined class to run at the end of
C<dpan>'s normal processing. The class only needs to provide a C<run>
method, which is automatically called by C<dpan>. Be careful that you
don't import anything called C<run> (looking at you, C<IPC::Run>)!

This example checks that the DPAN directory is under source control,
invokes an svn update, checks the svn status to see what's changes,
and creates a list of svn commands to run. It adds any new files it
finds and removes any missing miles. If it detects a conflict, it
stops the process before anything happens.

If you've set the C<postflight_dry_run> configuration variable, this
class merely prints the svn adds and removes that it would run, but it
doesn't actually run them. That gives you a chance to see what it
would do without doing it.

At the end of the run, this prints the URL you need to use to access the
repository.

=head2 Logging

This module uses the C<PostFlight> logging category in the C<Log::Log4perl>
setup.

=head2 Writing your own

If you want to maek your own class, check out the source for C<run>. The
code comments explain what you should be doing. However, most of the
code in this example isn't specific to the post flight processing.

=head2 Methods

=over 4

=cut

# Log::Log4perl should already be set up. The
BEGIN {
$logger = Log::Log4perl->get_logger( 'PostFlight' );
}

BEGIN {
my $svn = IPC::Cmd::can_run( 'svn' );
$logger->debug( "svn commmand is [$svn]" );

=item svn

Returns the path to the svn binary.

=cut

sub svn { $svn }

=item dry_run

Returns the value of the postflight_dry_run configuration directive.

=cut

sub dry_run { $_[0]->{postflight_dry_run} }

=item run_svn

Runs an svn command. During a dry run it merely prints the command to
standard output. Otherwise, it actually runs the svn command.

=cut

sub run_svn
	{
	my( $self, @commands ) = @_;

	if( $self->dry_run )
		{
		print "dry run: $svn @commands\n";
		}
	else
		{
		$logger->debug( "$svn @commands" );
		print capturex( $self->svn, @commands );
		}
	}

}

=item run( $application )

Makes the hamsters go. This is called automatically from dpan. It gets
the application object as its argument.

=cut

sub run
	{
	# dpan calls this as a class method after it runs
	# $application->cleanup. All of dpan's work is done and it's removed
	# most of its mess. You're picking up control just before it would
	# normally exit.
	#
	# The only argument is the $application object.
	my( $class, $application ) = @_;

	# The coordinator object has references to all of the other components
	# and the application notes. See MyCPAN::Indexer::Tutorial and
	# MyCPAN::Indexer::Coordinator for more information
	my $coordinator = $application->get_coordinator;

	# The Coordinator knows how to get the configuration object
	my $config      = $coordinator->get_config;

	# You are probably already in this directory, but it's nice to be
	# sure.
	my $dpan_dir = $config->get( 'dpan_dir' );
	chdir $dpan_dir;

	# If there isn't a .svn directory, there's not much that we can do
	unless( -e '.svn' )
		{
		$logger->logdie( "There isn't an .svn directory in [$dpan_dir]! I can't continue!" );
		return;
		}

	# Construct an object, although it's not necessary. We're going to
	# use it to adjust some configuration, etc, that we can pass around.
	# In this case, we just transfer the postflight_dry_run value.
	#
	# You can add any configuration directive that you like. You might
	# want to give it a prefix that won't conflict with the standard
	# dpan directives.
	#
	# In case we need the application object for something else, we'll
	# store a a reference to that too.
	my $self = bless
		{
		postflight_dry_run => $config->get( 'postflight_dry_run' ),
		application        => $application,
		}, $class;

	# Now we're past all of the special parts. It's whatever you want to
	# do now.
	$logger->info( "Checking the svn status" );
	my $commands = $self->_get_commands;

	$logger->info( "Handling svn additions and deletions" );
	$self->_handle_commands( $commands );

	$logger->info( "All done. Have a nice day!" );
	$self->_report_repo_url;
	}

BEGIN {
my %Commands = (
	'unversioned' => 'add',
	'deleted'     => 'rm',
	);

sub _get_commands
	{
	my( $self ) = @_;
	my $xml = $self->_get_svn_status_xml;

	my $ref = XMLin( $xml );

	my( @commands, @conflicts );
	foreach my $entry ( @{ $ref->{target}{entry} } )
		{
		my $status = $entry->{'wc-status'}{item};
		my $path   = $entry->{path};
		$logger->debug( "svn status for $path: $status" );

		if( exists $Commands{ $status } )
			{
			push @commands, [ $Commands{ $status }, $path ];
			}
		if( $status eq 'conflicted' )
			{
			push @conflicts, $path;
			}
		}

	if( @conflicts )
		{
		my $list = join "\n\t", @conflicts;

		$logger->logdie( "I can't continue. There are conflicts in svn:\n\t$list\n" );
		return;
		}

	\@commands;
	}
}

sub _svn_update
	{
	my( $self ) = @_;
	# don't use run_svn because we have to still run for dry run
	my $output = capturex( $self->svn, 'update' );
	$logger->debug( "svn status output: $output" );
	$output;
	}

sub _get_svn_status_xml
	{
	my( $self ) = @_;
	# don't use run_svn because we have to still run for dry run
	my $status = capturex( $self->svn, 'status', '--xml' );
	$logger->debug( "svn status output: $status" );
	$status;
	}

sub _handle_commands
	{
	my( $self, $commands ) = @_;

	my $svn = $self->svn;

	$self->_svn_update;

	foreach my $command ( @$commands )
		{
		$self->run_svn( @$command );
		}

	$logger->info( "Committing work to svn" );
	my @commit_command = (  $svn, 'commit', '-m', 'DPAN PostFlight commit' );

	my( $in, $output ) = ( '' );

	IPC::Run::run( \@commit_command, \$in, \$output, \$output )
		or do {
			$logger->debug( "svn commit output: $output" );
			$logger->logdie( "Could not commit to svn!" );
			return;
			};
	$logger->debug( "Output from commit: $output" );

	return 1;
	}

sub _report_repo_url
	{
	my( $self ) = @_;

	# don't use run_svn because we have to still run for dry run
	my $xml = capturex( $self->svn, 'info', '--xml' );
	$logger->debug( "svn info output: $xml" );

	my $ref  = XMLin( $xml );
	my $repo = $ref->{entry}{url};
	print "To use this DPAN, point your CPAN tool to:\n\n\t$repo\n\n";
	}

=back

=head1 SEE ALSO

MyCPAN::App::DPAN, dpan

=head1 SOURCE AVAILABILITY

This code is in Github:

	git://github.com/briandfoy/mycpan-app-dpan.git

=head1 AUTHOR

brian d foy, C<< <bdfoy@cpan.org> >>

=head1 COPYRIGHT AND LICENSE

Copyright © 2010-2018, brian d foy <bdfoy@cpan.org>. All rights reserved.

You may redistribute this under the terms of the Artistic License 2.0.

=cut

1;