#------------------------------------------------------------
# Copyright (C) 2006 RoyaltyShare, Inc.   All Rights Reserved
#------------------------------------------------------------

package Common::SVN::Branch;

#
# Common::SVN::Branch
#
# This class provides an interface to manage SVN branches.  It
# provides methods for checking out, creating branches, and
# merging code.
#
# Basic repository workflow is enforced using this class.
# The branching structure is as follows:
#
# /trunk
# /branches
#     /branch1
#     /branch2
# /testing
#     /trunk
#     /branches
#     /release
# /release
#     /20090519_001
#     /20090519_002
#     /20090529_001
#
# Notice that releases are branched with the date and a release number.  The only
# path to release is from the trunk via testing/trunk.  Therefore if you ware
# working on a branch then it must be merged into the trunk before it can be
# released.  However, a development branch can be pushed to testing/branches
# for testing.
#
# Usage:
#
#    $svn->checkout( $source );
#    $svn->checkout( $source, $destination );
#    $svn->toBranch( $branch );
#    $svn->toTesting( $source );
#    $svn->toRelease();
#    $svn->mergeBranch( $source, $local_wc );
#

use File::Temp;
use Data::Dumper;

use strict;

use lib '/app/tools/common/lib';
use base 'Common::SVN';

use Common::Assert;

sub source      { shift->{_source} }
sub destination { shift->{_destination} }

# The only way to a branch is from the trunk.  So all we need is the
# destination branch name.
sub toBranch {
    my $self = shift;
    my $destBranch = shift || die "destination branch required";

    if ( $self->exists("branches/$destBranch") ) {
        die "Branch '$destBranch' already exists";
    }

    $self->cp( "trunk", "branches/$destBranch", "Branch created from trunk" )
      || die "Failed to create branch '$destBranch'";

    $self->{_source}      = 'trunk';
    $self->{_destination} = "branches/$destBranch";

}

# We can go to testing from almost anywhere.  i.e. trunk, production, or
# a branch, but we can derive where it should go.  Therefore we only need the
# source.
sub toTesting {
    my $self   = shift;
    my $source = shift || die "source required";
    my $branch = $source;
    $branch =~ s/.*\///;

    die "source must be either 'trunk', 'branches/branch_name', 'release/release_name'"
      unless ( $source eq 'trunk' || $source =~ /^(branches|release)\/.+/ );

    ###
    #   Test trunk
    ###
    if ( $source eq 'trunk' ) {
        $self->rm( 'testing/trunk', "Removing testing trunk for new branch" )
          && $self->cp( 'trunk', 'testing/trunk', "Branch created from trunk" );

        $self->{_source}      = 'trunk';
        $self->{_destination} = 'testing/trunk';

        ###
        #   Test branch
        ###
    } elsif ( $source =~ /^branches/ ) {
        $self->rm( "testing/branches/$branch", "Removing testing branch" )
          if ( $self->exists($source) );

        if ( $self->exists($source) ) {
            $self->cp( $source, "testing/branches/$branch", "Testing created from $branch" );
        } else {
            print STDERR "Branch '$source' does not exists\n";
            return;
        }

        $self->{_source}      = $source;
        $self->{_destination} = "testing/branches/$branch";

        ###
        #   Test release
        ###
    } elsif ( $source =~ /^release/ ) {
        $self->rm( "testing/release/$branch", "Removing testing release" )
          if ( $self->exists($source) );

        if ( $self->exists($source) ) {
            $self->cp( $source, "testing/release/$branch", "Branch created from trunk" );
        } else {
            print STDERR "Release '$source' does not exists\n";
            return;
        }

        $self->{_source}      = $source;
        $self->{_destination} = "testing/branches/$branch";

    } else {
        die "Unknown source: $source";
    }

    return 1;
}

# We can only move to production from testing and the release number is
# derived.  So we don't need any parameters here.
sub toRelease {
    my $self  = shift;
    my $dest  = "release/" . $self->_getNextReleaseName();
    my $notes = $self->_getReleaseNotes();

    die "ERROR: release notes are required"
      unless ($notes);

    $self->cp( 'testing/trunk', $dest, $notes );

    $self->{_source}      = 'testing/trunk';
    $self->{_destination} = $dest;

    return 1;
}

# Merge a svn repository branch to a local working copy.  The method will blow
# up if the local working copy is not up to date.
sub mergeBranch {
    my $self        = shift;
    my $source      = shift || die "source required";
    my $destination = shift || die "destination required";
    my $sourceURL   = $self->SVNREPO . "/$source";

    unless ( $self->exists($sourceURL) ) {
        die "$sourceURL doesn't exists";
    }

    unless ( -d $destination ) {
        die "$destination doesn't exists";
    }

    my $info = $self->info($destination);

    unless ($info) {
        die "'$destination' doesn't appear to be a local repository";
    }

    my $localBranch = $info->{entry}->{url};
    my $root        = $info->{entry}->{repository}->{root};

    unless ( $localBranch && $root ) {
        die "Failed to read local repository information\n";
    }

    $localBranch =~ s/$root\///;

    unless ( $localBranch && $localBranch ne $source ) {
        die "Destination and source branches are the same\n";
    }

    die "source must be either 'trunk', 'branches/branch_name', 'release/release_name'"
      unless ( $source eq 'trunk' || $source =~ /^(branches|release)\/.+/ );

    die "destination must be either a local branch checkout to 'trunk', 'branches/branch_name', 'release/release_name'"
      unless ( $localBranch eq 'trunk' || $localBranch =~ /^(branches|release)\/.+/ );

    if ( $localBranch eq 'trunk' ) {
        print STDERR "When merging back to the trunk the source branch can no longer be merged.\n";
        print STDERR "Sorry, this is a subversion issue.\n";
        print STDERR "\nType 'merge' to continue with the merge: ";
        my $response = <STDIN>;
        chomp $response;

        unless ( $response && $response eq 'merge' ) {
            print STDERR "Aborting ... \n";
            return;
        }
    }

    return $self->merge( $source, $destination );
}

sub _getNextReleaseName {
    my $self = shift;
    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime(time);
    my $date = sprintf( "%4d%02d%02d", $year + 1900, $mon + 1, $mday );
    my $max = 0;

    my @line = grep $date, $self->ls('release');

    if (@line) {
        for my $file (@line) {
            $file =~ s/\/$//g;
            my ( undef, $number ) = split( /_/, $file );
            $max = $number if ( $number && $number > $max );
        }
    }

    return sprintf( "%s_%03d", $date, $max + 1 );
}

sub _getLastReleaseName {
    my $self = shift;
    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime(time);
    my $date = sprintf( "%4d%02d%02d", $year + 1900, $mon + 1, $mday );
    my $max = 0;

    my @line = grep $date, $self->ls('release');

    if (@line) {
        for my $file (@line) {
            $file =~ s/\/$//g;
            my ( undef, $number ) = split( /_/, $file );
            $max = $number if ( $number && $number > $max );
        }
    }

    return sprintf( "%s_%03d", $date, $max );
}

sub _getReleaseNotes {
    my $self   = shift;
    my $editor = $self->_getEditor();
    my $result;

    my $file = File::Temp->new( UNLINK => 1 ) || die "Could not create tmp file";
    $self->_populateReleaseTemplate($file);

    my $res = system("$editor $file");
    return if ($res);

    open( INFILE, "$file" );
    while ( my $line = <INFILE> ) {
        $result .= "$line" unless ( $line =~ /^#/ || $line =~ /^$/ );
    }

    return "\n$result" if ($result);
}

sub _getEditor {
    if    ( $ENV{EDITOR} )                  { return $ENV{EDITOR} }
    elsif ( -e '/etc/alternatives/editor' ) { return '/etc/alternatives/editor' }
    else                                    { return '/bin/vi' }
}

sub _populateReleaseTemplate {
    my $self = shift;
    my $file = shift || die "file required";

    open( OUT, ">$file" ) || die "ouch";

    print OUT "\n";
    print OUT "######################################################################\n";
    print OUT "# All lines beginning with # are ignored.\n";
    print OUT "# \n";
    print OUT "# Please describe what is being released\n";
    print OUT "######################################################################\n";

    close(OUT);

}

###
1;    # Play nicely.
###
