#!/usr/bin/perl
# $Id: mdvsys.in 270561 2010-08-14 13:53:56Z guillomovitch $

use strict;
use warnings;
use lib '/usr/share/mdvsys/lib';

use Cwd;
use Date::Format;
use Exception::Class;
use Getopt::Long;
use Pod::Usage;
use MDV::Repsys;
use File::Spec;
use File::Temp qw(tempdir);

=head1 NAME

mdvsys - Mandriva build system command line client

=head1 SYNOPSIS

mdvsys <action> [options]...

Actions:

    import        import packages into the repository
    create        create package directory into the repository
    checkout|co   checkout package into a working directory
    update        update package into repository
    mass-update   update all packages with available version
    info          get package information from repository
    log           get package changelog from repository
    tag           tag package into repository
    extract       extract source package from repository
    submit        submit package from repository
    sync          sync working directory content
    commit|ci     commit working directory content
    build         build package from working directory
    strip         strip package changelog from working directory
    help          display this help message

=head1 DESCRIPTION

This tool is an alternative to repsys and bm to manage packages in Mandriva
build system, allowing to perform different actions on packages.

Most actions implements a single part of the package release cycle. For
instance, the standard sequences of actions for updating a package already
imported into the subversion repository is:

=over

=item * B<mdvsys checkout>

=item * manual modifications

=item * B<mdvsys build>

=item * B<mdvsys sync>

=item * B<mdvsys commit>

=item * B<mdvsys submit>

=back

Other actions, tough, cover the whole update cycle of a single package, such as
B<mdvsys update>. And even the update cycle of several packages at once, such as
B<mdvsys mass-update>.

The default verbosity level for each action correspond to minimal feedback
output, and mask underlying details. Cumulative verbose flags can be used to
raise verbosity level.

=head1 CONFIGURATION

=head2 Setup

Configuration relies on the following configuration files:

=over

=item * the system configuration file is given through the B<--config> flag,
and defaults to /etc/mdvsys.conf

=item * the personal configuration file is ~/.mdvsys.conf

=back

The system configuration is read first, and the personal configuration
thereafter, overriding previous settings.

=head2 Content

Configuration uses .ini-style configuration files, consisting of a number of
sections, each preceded with the section name in square brackets, followed by
parameter names and their values.

    [section]
    parameter = value

The following parameters are available.


=head3 repositories

=over

=item * main

top-level url for read/write repository

=item * mirror

top-level url for read-only repository

=item * split_binaries

set to 'yes' to use distinct read/write binary repositories

=item * split_changelog

set to 'yes' to store old changelog outside of package path

=back

If splited repositories are in usage, an additional parameter is needed for each
distribution.

=head3 submit

=over

=item * default

default submission target

=item * host

submission host

=back

=head3 ldap

=over

=item * server

ldap server

=item * base

ldap base

=item * filter_format

ldap filter template

Example: (&(objectClass=inetOrgPerson)(uid=$username)))

=item * result_format

ldap result template

Example: $cn <$mail>

=back

=head3 helper

=over

=item * install_buildrequires

command used to install build dependencies

=item * create_srpm

command used to create source packages

=back

=head1 ACTIONS

More details about each individual actions follow.

=head2 IMPORT

mdvsys import [options] <file1> [[file2] ...]

Options:

    --verbose,-v         increase verbosity level (cumulative) (cumulative)
    --config,-c <file>   use given configuration file
    --revision <rev>     use given revision in repository
    --distribution <dis> use given distribution tree in repository
    --branch <branch>    use given package branch in repository
    --message <message>  use given message as commit message
    --nocommit           disable commit action
    --submit             submit the package just after importing
    --help,-h            display this help message

Import one or more source package into the subversion repository.

=head2 CREATE

mdvsys create [options] <package>

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --revision <rev>     use given revision in repository
    --distribution <dis> use given distribution tree in repository
    --branch <branch>    use given package branch in repository
    --message <message>  use given message as commit message
    --nocommit           disable commit action
    --help,-h            display this help message

Create a package directory in the subversion repository.

=head2 CHECKOUT

mdvsys checkout [options] <package> [dir]

mdvsys co [options] <package> [dir]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --revision <rev>     use given revision in repository
    --distribution <dis> use given distribution tree in repository
    --branch <branch>    use given package branch in repository
    --help,-h            display this help message

Checkout the current version of a package from the subversion repository. If no
directory name is provided, the package name is used instead.

=head2 UPDATE

mdvsys update [options] <package> [version]

Options:

    --verbose,-v                        increase verbosity level (cumulative)
    --config,-c <file>                  use given configuration file
    --revision <rev>                    use given revision in repository
    --distribution <dis>                use given distribution tree in repository
    --branch <branch>                   use given package branch in repository
    --release <release>                 pass to updater object
    --spec-line-expression <expression> pass to updater object
    --keep-on-failure,-k                keep temporary directory on failure
    --message <message>                 use given message as commit message
    --target <target>                   use given submission target
    --nocommit                          disable commit action
    --nosubmit                          disable submit action
    --nobuild                           disable local build
    --help,-h                           display this help message

Checkout a package to a temporary directory, update it to either a new version
if a version number is given or to a new release, then submit it. See
Youri::Package::RPM::Updater for specific options description.

=head2 MASS-UPDATE

mdvsys mass-update [options] <url>

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --include <regexp>   only process packages matching given regexp
    --exclude <regexp>   don't process packages matching given regexp
    --keep-on-failure,-k keep temporary directories on failure
    --nocommit           disable commit action
    --nosubmit           disable submit action
    --help,-h            display this help message

Fetch a youri-check available update report at given URL, and try to update
each package for which a new version is available.

=head2 INFO

mdvsys info [options] [package]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --revision <rev>     use given revision in repository
    --distribution <dis> use given distribution tree in repository
    --branch <branch>    use given package branch in repository
    --help,-h            display this help message

Give informations about a package from subversion repository. If no package name
is provided, the one from the current directory is used.

=head2 LOG

mdvsys log [options] [package]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --revision <rev>     use given revision in repository
    --distribution <dis> use given distribution tree in repository
    --branch <branch>    use given package branch in repository
    --help,-h            display this help message

Generate the changelog of a package from subversion repository. If no package
name is provided, the one from the current directory is used.

=head2 TAG

mdvsys tag [options] [package]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --revision <rev>     use given revision in repository
    --help,-h            display this help message

Tag package in subversion repository. If no package name is provided, the one
from the current directory is used.

=head2 SUBMIT

mdvsys submit [options] [package]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --revision <rev>     use given revision in repository
    --distribution <dis> use given distribution tree in repository
    --branch <branch>    use given package branch in repository
    --target <target>    use given submission target
    --define <value>     pass --define <value> to underlying command
    --help,-h            display this help message

Submit a package from subversion repository to the build system. If no
package name is provided, the one from the current directory is used.

=head2 EXTRACT

mdvsys extract [options] [package]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --revision <rev>     use given revision in repository
    --distribution <dis> use given distribution tree in repository
    --branch <branch>    use given package branch in repository
    --keep-on-failure,-k keep temporary directory on failure
    --destdir            use given directory as destination
    --prefix             add prefix for mandriva submission tool
    --help,-h            display this help message

Extract a source package from the subversion repository into the current
directory. The --prefix options make it compatible with mandriva submission
tool. If no package name is provided, the one from the current directory is
used.

=head2 SYNC

mdvsys sync [options] [dir]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --help,-h            display this help message

Ensure content from a working directory content is synchronized with subversion
repository, by adding new sources and patches referenced in the spec file, and
removing old ones. If no directory name is provided, the current one is used.

=head2 COMMIT

mdvsys commit [options] [dir]

mdvsys ci [options] [dir]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --sync               perform sync action before commiting
    --message <message>  use given message as commit message
    --help,-h            display this help message

Commit content from a working directory content. If no directory name is
provided, the current one is used. 

=head2 BUILD

mdvsys build [options] [file]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --stage <stage>      run rpmbuild -b<stage>
    --short-circuit>     run rpmbuild --short-circuit
    --define <value>     run rpmbuild --define <value>
    --noinstall          don't install build dependencies
    --help,-h            display this help message

Build package with given spec file. If no spec file is provided, the one
from the current directory is used.

=head2 STRIP

mdvsys strip [options] [file]

Options:

    --verbose,-v         increase verbosity level (cumulative)
    --config,-c <file>   use given configuration file
    --help,-h            display this help message

Remove the changelog of the spec file. If no spec file is provided, the one
from the current directory is used.

=head1 CHANGES

The 2.0 branches introduces many changes, renaming some actions for
consistency, and changing behaviour of others for sake of isolation. Here is a
list of changes:

=over

=item * action B<getsrpm> has been renamed to B<extract>

=item * action B<extract> doesn't prefix generated package by default, you have
to use B<--prefix> option

=item * action B<commit> doesn't check for unsync content now, it just commit
changes

=back

Verbosity has also been altered in order to be able to use actions with large
scope, such as B<mass-update> without getting drowned under individual package
build details. The default verbosity is minimal feedback for each action, and
can be gradually increased using cumulative B<--verbose> flags.

=head1 AUTHORS

Olivier Thauvin <nanardon@mandriva.org>

Guillaume Rousse <guillomovitch@mandriva.org>

Michaël Scherer <misc@mandriva.org>

=head1 SEE ALSO

L<repsys>

=cut

my %options_lists = (
    'create'      => [ qw/revision|r=s distribution=s branch=s message|m=s commit!/ ],
    'import'      => [ qw/revision|r=s distribution=s branch=s message|m=s commit! submit!/ ],
    'checkout'    => [ qw/revision|r=s distribution=s branch=s/ ],
    'update'      => [ qw/revision|r=s distribution=s branch=s spec-line-expression=s keep-on-failure|k! release=s message|m=s target=s commit! submit! build!/ ],
    'mass-update' => [ qw/include=s exclude=s keep-on-failure|k! commit! submit! build!/ ],
    'info'        => [ qw/revision|r=s distribution=s branch=s/ ],
    'log'         => [ qw/revision|r=s distribution=s branch=s/ ],
    'tag'         => [ qw/revision|r=s/ ],
    'submit'      => [ qw/revision|r=s distribution=s branch=s target=s define=s@/ ],
    'commit'      => [ qw/message|m=s sync!/ ],
    'build'       => [ qw/stage=s define=s@ short-circuit! install!/ ],
    'extract'     => [ qw/revision|r=s distribution=s branch=s destdir=s prefix! keep-on-failure|k!/ ],
);

my %aliases = (
    co => 'checkout',
    ci => 'commit',
);

my $action = shift @ARGV;
pod2usage("no action given, aborting\n") if !$action || $action =~ /^-/;

if ($action eq 'help') {
    my $subject = $ARGV[0];
    $subject = $aliases{$subject} if $subject && $aliases{$subject};
    _display_help($subject);
}

$action = $aliases{$action} if $aliases{$action};

my $options_list = $options_lists{$action} || [];
my $verbose = 0;
my $config  = '/etc/mdvsys.conf';
my $help    = undef;
my %options;
GetOptions(
    \%options,
    'verbose|v+' => \$verbose,
    'config|c=s' => \$config,
    'help|h!'    => \$help,
    @{$options_list}
);

_display_help($action) if $help;

my $repsys = MDV::Repsys->new(
    configfile => $config,
    defined $options{build}  ? (build      => $options{build})  : (),
    defined $options{commit} ? (commit     => $options{commit}) : (),
    defined $options{submit} ? (submit     => $options{submit}) : ()
);

my $function = _get_function($action);
pod2usage("unknown action, aborting\n") unless $function;

eval {
    $function->();
};
my $exception = Exception::Class->caught();
if ($exception) {
    if (ref $exception) {
        die "Error performing action $action: ", $exception->error(), "\n";
    } else {
        die "Error performing action $action: ", $exception, "\n";
    }
}

exit(0);

sub import {
    my @files = @ARGV;
    pod2usage({
        -msg => "no package file given, aborting\n",
        -verbose => 99,
        -sections => 'ACTIONS/IMPORT'
    }) unless @files;

    $repsys->set_verbosity(2 + $verbose);
    foreach my $file (@files) {
        $repsys->import_package($file, %options);
    }
}

sub create {
    my $package = $ARGV[0] ||
                  pod2usage({
                      -msg => "no package name given, aborting\n",
                      -verbose => 99,
                      -sections => 'ACTIONS/CREATE'
                  });

    $repsys->set_verbosity(2 + $verbose);
    $repsys->create_package_directory($package, %options);
}

sub checkout {
    my $package = $ARGV[0] ||
                  pod2usage({
                      -msg => "no package name given, aborting\n",
                      -verbose => 99,
                      -sections => 'ACTIONS/CHECKOUT'
                  });
    my $dir     = $ARGV[1];

    $repsys->set_verbosity(3 + $verbose);
    $repsys->checkout_package($package, $dir, %options);
}

sub update {
    my $package = $ARGV[0] ||
                  pod2usage({
                      -msg => "no package name given, aborting\n",
                      -verbose => 99,
                      -sections => 'ACTIONS/UPDATE'
                  });
    my $version = $ARGV[1];

    $repsys->set_verbosity(2 + $verbose);
    $repsys->update_package($package, $version, %options);
}

sub info {
    my $package = $ARGV[0] ||
                  $repsys->get_package_name_from_current_directory() ||
                  pod2usage({
                      -msg => "no package name given, aborting\n",
                      -verbose => 99,
                      -sections => 'ACTIONS/INFO'
                  });

    $repsys->set_verbosity(2 + $verbose);
    my $info = $repsys->get_package_info($package, %options);
    printf "Information for package %s\n", $package;
    printf "Size:                 %s\n", $info->{size};
    printf "Last change author:   %s\n", $info->{last_author};
    printf "Last change revision: %s\n", $info->{last_rev};
    printf "Last change time:     %s\n", ctime($info->{last_time} / 1000000);
}

sub log {
    my $package = $ARGV[0] ||
                  $repsys->get_package_name_from_current_directory() ||
                  pod2usage({
                      -msg => "no package name given, aborting\n",
                      -verbose => 99,
                      -sections => 'ACTIONS/LOG'
                  });

    $repsys->set_verbosity(2 + $verbose);
    $repsys->print_changelog($package, %options);
}

sub tag {
    my $package = $ARGV[0] ||
                  $repsys->get_package_name_from_current_directory() ||
                  pod2usage({
                      -msg => "no package name given, aborting\n",
                      -verbose => 99,
                      -sections => 'ACTIONS/TAG'
                  });

    $repsys->set_verbosity(2 + $verbose);
    $repsys->tag_package($package, %options);
}

sub submit {
    my $package = $ARGV[0] ||
                  $repsys->get_package_name_from_current_directory() ||
                  pod2usage({
                      -msg => "no package name given, aborting\n",
                      -verbose => 99,
                      -sections => 'ACTIONS/SUBMIT'
                  });

    $repsys->set_verbosity(3 + $verbose);
    $repsys->submit_package($package, %options);
}

sub extract {
    my $package = $ARGV[0] || 
                  $repsys->get_package_name_from_current_directory() ||
                  pod2usage({
                      -msg => "no package name given, aborting\n",
                      -verbose => 99,
                      -sections => 'ACTIONS/EXTRACT'
                  });

    $repsys->set_verbosity(2 + $verbose);
    $repsys->extract_package($package, %options);
}

sub sync {
    my $dir = $ARGV[0] ? File::Spec->rel2abs($ARGV[0]) : cwd();

    $repsys->set_verbosity(3 + $verbose);
    $repsys->split_sources("$dir/SOURCES", "$dir/SOURCES-bin", link => 1);
    $repsys->sync_sources(
        $repsys->find_unsync_sources(
            $dir, $repsys->find_current_sources($dir)
        )
    );
}

sub commit {
    my $dir = $ARGV[0] ? File::Spec->rel2abs($ARGV[0]) : cwd();

    $repsys->set_verbosity(3 + $verbose);
    $repsys->commit_directory($dir, %options);
}

sub build {
    my $file = $ARGV[0] ||
               (glob("SPECS/*.spec"))[0] ||
               pod2usage({
                   -msg => "no spec file given, aborting\n",
                   -verbose => 99,
                   -sections => 'ACTIONS/BUILD'
               });

    $repsys->set_verbosity(3 + $verbose);
    $repsys->build_package($file, %options);
}

sub strip {
    my $file = $ARGV[0] ||
               (glob("SPECS/*.spec"))[0] ||
               pod2usage({
                   -msg => "no spec file given, aborting\n",
                   -verbose => 99,
                   -sections => 'ACTIONS/STRIP'
               });

    $repsys->set_verbosity(2 + $verbose);
    $repsys->strip_changelog($file);
}

sub mass_update {
    my $url = $ARGV[0] ||
              pod2usage({
                   -msg => "no url given, aborting\n",
                   -verbose => 99,
                   -sections => 'ACTIONS/MASS-UPDATE'
               });

    $repsys->set_verbosity(1 + $verbose);
    $repsys->mass_update($url, %options);
}

sub _display_help {
    my ($subject) = @_;
    if ($subject) {
        if (defined _get_function($subject)) {
            pod2usage({
                -verbose => 99,
                -sections => "ACTIONS/" . uc($subject),
                -exitval => 0
            });
        } else {
            pod2usage("unknown action, aborting\n");
        }
    } else {
        pod2usage(0);
    }
}

sub _get_function {
    my ($action) = @_;
    $action =~ s/-/_/;
    ## no critic ProhibitNoStrict
    no strict 'refs';
    ## use critic
    return unless defined &{"main::$action"};
    return \&{"main::$action"};
}
