#!/usr/bin/perl

=head1 NAME

    direct-svn-merge -- простые мержи в стандартно устроенном репозитории (как в Директе: trunk, branches)

=head1 DESCRIPTION

    Мерж транка в бранч:
    чекаутит бранч в новую рабочую копию, мержит в нее транк (самый свежий, или до указанной ревизии), если нет конфликтов -- коммитит. 
    Если есть конфликты -- оставляет рабочую копию, в которой можно конфликты разобрать. 
    По окончании работы пишет ямб-сообщение.

    смержить в my-branch свежий транк: 
    direct-svn-merge my-branch
    direct-svn-merge -b my-branch
    direct-svn-merge # без параметров - мержить в текущий бранч, при запуске из рабочей копии

    не спрашивать подтверждения: 
    direct-svn-merge my-branch -y

    смержить в my-branch транк вплоть до ревизии 12345:
    direct-svn-merge -r 12345 my-branch

    не коммитить, оставить каталог с смерженными файлами:
    direct-svn-merge -r 12345 my-branch --no-commit

    Мерж бранча в транк:
    чекаутит транк, мержит в него бранч, запускает юнит-тесты. 
    Ничего НЕ коммит: предполагается, что человек пойдет и посмотрит глазами, что собирается закоммитить.

    direct-svn-merge -b my-branch --reintegrate


    Опции

    -h, --help
        справка 

    -b, --branch <бранч>
        фиче-бранч

    --no-commit
        не коммитить изменения
        подразумевает и --no-cleanup

    --no-cleanup
        не удалять каталог, в котором делался мерж

    --no-unit-tests
        не запускать юнит-тесты
        для режима reintegrate: сработает только в сочетании с --count-on-me-i-ll-fix-everything
        пользоваться ответственно!

    --unit-tests
        для мержа транка в брач: запустить юнит-тесты

    --no-check-release-flag
        для режима reintegrate: не проверять релизный флаг

    -r, --rev <ревизия>
        при мерже транка в бранч: мержить транк только до указанной ревизии

    --reintegrate
        мержить бранч в транк

    -y, --yes
        не спрашивать подтверждения последовательности действий

=head1 TODO

 + проверить, правильно ли обрабатывает конфликты
 + при неудаче выводить на экран и в yamb состояние рабочей копии после мержа
 + по умолчанию -- ямб-сообщение (успех, неудача)

 + план действий, подтверждение от пользователя
 + переименовать в просто direct-svn-merge
 + если мержились до определенной ревизии транка -- указывать это в коммит-сообщении (up to r12345)
 + проверить, как ведет себя при повторном мерже (когда ничего не мержится)

 + таймаут на commit (на случай, если ждет ввода пароля -- чтобы не ждало бесконечно)
 + вместо --dry-run -- ключи --no-cleanup, no-commit
 - ключ --no-yamb
 - опция: не чекаутить свежий каталог, использовать существующий (насколько строго проверять неверсионированные файлы?)
 - --reintegrate -- мерж бранча в транк
   * надо тикет
   * проверять состояние и assignee тикета
   * возможно, проверять: в описании тикета упоминается обрабатываемый бранч

=cut

use strict;
use warnings;
use utf8;
use open ':std' => ':utf8';
$|++;

use feature qw/state/;

use Data::Dumper;
use Getopt::Long;
use File::Temp qw/tempdir/;
use File::Path qw/remove_tree/;
use IO::Prompt;
use POSIX qw/strftime/;
use Path::Tiny;
use JSON;

use Yandex::Shell;
use Yandex::Yamb;
use Yandex::Svn qw/svn_info/;

use ProjectSpecific qw/svn_url/;

our $state;
our $LOG_FILE = '/var/log/yandex/direct-svn-merge/direct-svn-merge.log.'.strftime("%Y%m", localtime);

# сценарий работы
our @actions = (
    {
        # сразу проверяем, взведён ли релизный флаг, потому что между началом мерджа и коммитом проходит продолжительное время.
        name => 'check-release-flag',
        comment => sub { "check if release flag is set" },
        code => sub {
            if (system('direct-release-flag', '-q') != 0) {
                # сообщение скопировано из commit с поправкой на имя опции и то, что direct-svn-merge не сохраняет попытки коммитов с целью отправить письмо потом
                print "
Сейчас тестируется релиз, несрочные коммиты в trunk лучше сделать попозже.
Если твой коммит -- срочный, используй опцию --no-check-release-flag

Для просмотра состояния релизного флага есть скрипт direct-release-flag:

> direct-release-flag
".qx!direct-release-flag!;
                exit 1;
            }
        },
    },

    {
        #skip => 1,
        comment => sub { 
            return "create tmp dir /var/www/tmp/automerge.XXXXXXXXXX";
        },
        code => sub {
            $state->{tmp_dir} = tempdir("/var/www/tmp/automerge.XXXXXXXXXX", CLEANUP => 0);
            $state->{final_message} = "direct-svn-merge: merge to $state->{target_branch} done\nПожалуйста, посмотри глазами на получившийся дифф и закоммить правки: $state->{tmp_dir}\n\nПодходящее коммит-сообщение:\n$state->{commit_message}" if $state->{"no-commit"} && $state->{"no-cleanup"} && $state->{reintegrate};
        },
    },

    {
        code => sub {
            sleep 2;
            chdir $state->{tmp_dir};
            #yash_system('direct-svn-checkout', $state->{target_svn_url}, '.');
            yash_system('svn', 'co', $state->{target_svn_url}, '.', '--ignore-externals');
        },
        comment => sub { 
            return "checkout $state->{target_svn_url}"; 
        },
    },

    {
        code => sub {
            yash_system(svn => 'merge', $state->{source_svn_url}, "--non-interactive", ($state->{reintegrate} ? ( "--reintegrate" ) : () ));
        },
        comment => sub {
            return "svn merge $state->{source_svn_url}";
        },
    },

    {
        code => sub {
            $state->{svn_st_q} = yash_qx(qq/svn st $state->{tmp_dir}/);
            $state->{conflicts} = join "\n", grep {/^.{0,6}C/} split "\n", $state->{svn_st_q};
            die if $state->{conflicts};
        },
        comment => sub {
            return "check for conflicts".($state->{tmp_dir}?" in $state->{tmp_dir}":"");
        },
        comment_on_fail => sub {
            return "conflicts:\n\n$state->{conflicts}\n\ndirect-svn-merge: failed merge to $state->{target_branch}, go to $state->{hostname}:$state->{tmp_dir}, resolve conflicts and commit manually\n\nrecommended commit message:\n$state->{commit_message}";
        },
    },

    {
        code => sub {
            die "Коммитить нечего (возможно, все свежие изменения уже были смержены?)\n" unless $state->{svn_st_q};
        },
        comment => sub {
            return "check for changes to commit";
        },
    },

    {
        code => sub {
            my $svn_diff_depth_0 = yash_qx(qq!svn diff --depth=files!);
            $state->{svn_diff_mergeinfo} = $svn_diff_depth_0 =~ /Modified: svn:mergeinfo/;
            die "После мержа не изменилось svn:mergeinfo, это очень подозрительно!\n" unless $state->{svn_diff_mergeinfo};
        },
        comment => sub {
            return "check mergeinfo";
        },
    },

    {
        name => 'unit-tests', 
        code => sub {
            yash_system("direct-mk", "test-full");
            if ($ProjectSpecific::PROJECT eq "Direct") {
                yash_system("direct-mk", "test-frontend");
            }
        },
        comment => sub {
            return "unit-tests";
        },
        comment_on_fail => sub {
            return "working copy checked out to $state->{tmp_dir}";
        },
    },

    {
        name => 'commit',
        code => sub {
            yash_system(timeout => '300s', svn => 'commit', '-m' => $state->{commit_message});
        },
        comment => sub { 
            return "svn commit";
        },
    },

    {
        name => 'cleanup',
        code => sub {
            chdir '/';
            remove_tree($state->{tmp_dir});
        },
        comment => sub {
            return "remove tmp dir";
        },
    },
);


run() unless caller();

sub run
{
    # разбираем параметры
    $state = get_options();

    # проверяем сценарий: что делать, а что пропустить, показываем список пользователю
    print "\n\nПланируемые действия:\n";
    for my $act (@actions){
        $act->{skip} = !!($act->{skip} || $act->{name} && $state->{"no-$act->{name}"});
        my $skip_instruction = "";
        if ($act->{name} && $act->{name} eq 'unit-tests'){
            if      ( ! $state->{reintegrate} && ! $act->{skip} ) {
                $skip_instruction = " (use --no-$act->{name} to skip)";
            } elsif ( ! $state->{reintegrate} &&   $act->{skip} ){
                # nothing
            } elsif ( $state->{reintegrate} && ! $state->{responsible_person} ){
                $skip_instruction = !$act->{skip} ? " (use --no-$act->{name} --count-on-me-i-ll-fix-everything to skip. Will be written to log)" : "";
            } elsif ( $state->{reintegrate} &&   $state->{responsible_person} ){
                # nothing
            }
        } else {
            $skip_instruction = $act->{name} && !$act->{skip} ? " (use --no-$act->{name} to skip)" : "";
        }
        print "".( $act->{skip} ? "[SKIP] " : "       " ).$act->{comment}->()."$skip_instruction\n";
    }
    print "\nКоммит-сообщение:\n$state->{commit_message}\n\n";
    # подтвержение пользователя
    unless ($state->{yes}){
        prompt('Подтверждаешь?', -yn, -default => 'y') || exit(0);
    }

    write_log() if $state->{reintegrate} && $state->{"no-unit-tests"};

    # выполняем все действия по сценарию
    for my $act (@actions){
        next if $act->{skip};
        my $comment = $act->{comment}->();

        print "$comment ... \n";
        my $ok = eval {
            $act->{code}->();
            1;
        };
        if ( !$ok ){
            my $message = "direct-svn-merge: failed $comment".(exists $act->{comment_on_fail}?"\n".$act->{comment_on_fail}->():'')."\nerror: $@";
            print $message."\n";
            send_yamb_message(to => $state->{yamb_to}, message => $message);
            exit 1;
        }
        print "$comment done\n";
    }

    # готово, шлем финальное уведомление
    print "\n".$state->{final_message}."\n\n\n";
    send_yamb_message(to => $state->{yamb_to}, message => $state->{final_message});
    exit 0;
}



sub get_options
{
    my %O;
    GetOptions(
        "h|help"     => sub { system("podselect -section NAME -section SYNOPSIS -section DESCRIPTION $0 | pod2text-utf8"); exit 0; },
        "rev|r=i"    => \$O{up_to_rev},
        "no-commit"  => \$O{"no-commit"},
        "no-cleanup" => \$O{"no-cleanup"},
        "no-unit-tests" => \$O{"no-unit-tests"},
        "no-check-release-flag" => \$O{"no-check-release-flag"},
        "unit-tests" => \$O{"unit-tests"},
        "y|yes"      => \$O{yes},
        "b|branch=s" => \$O{branch},
        "reintegrate" => \$O{reintegrate},
        "requester=s"  => \$O{requester},
        "count-on-me-i-ll-fix-everything"  => \$O{responsible_person},
    ) or die "cant't parse options, stop\n";
    
    if (!$O{branch} && !@ARGV) {
        my $svn_info = svn_info(".");
        my $branches_url = svn_url('branches');
        if ($svn_info->{url} =~ m!\Q$branches_url\E/([^/]+)!) {
            $O{branch} = $1;
        }
    }

    my $caller_login = [getpwuid($<)]->[0];
    my $robot_login = ProjectSpecific::get_project_data("autobeta_user");
    my $is_robot = $caller_login eq $robot_login;
    if (!$is_robot && $O{requester}) {
        die "option --requester should be used only by robots\n";
    }

    if ($is_robot) {
        die "-y is obligatory for robot user\n" unless $O{yes};
        die "--requester is obligatory for robot user\n" unless $O{requester};
    }

    if ( $O{reintegrate} ){
        die "-r <revision> shouldn't be used with --reintegrate" if $O{up_to_rev};
        die "robots shouldn't reintegrate\n" if $is_robot;
        $O{target_branch} = "trunk";
        $O{target_svn_url} = svn_url('trunk');
        $O{source_branch} = delete $O{branch} || shift @ARGV;
        die "no branch given, stop\n" unless $O{source_branch};
        $O{source_svn_url} = svn_url("branches").'/'.$O{source_branch};
        $O{commit_message} = "<ST issue>\nmerged branch $O{source_branch}";
        $O{"no-commit"} = 1;
        $O{"no-cleanup"} = 1;
        # если пользователь пообещал быстро все починить -- разрешаем игнорировать тесты даже при мерже в транк
        $O{"no-unit-tests"} = 0 unless $O{responsible_person};
    } else {
        $O{target_branch} = delete $O{branch} || shift @ARGV;
        die "no branch given, stop\n" unless $O{target_branch};
        $O{target_svn_url} = svn_url("branches").'/'.$O{target_branch};
        $O{peg_revision} = $O{up_to_rev} ? "\@$O{up_to_rev}" : '';
        $O{source_svn_url} = svn_url('trunk').$O{peg_revision};
        $O{commit_message} = "$O{target_branch}: merged trunk changes".($O{up_to_rev} ? " up to r$O{up_to_rev}" : "")."\n(via direct-svn-merge" . ($O{requester} ? " on behalf of $O{requester}\@" : "") . ")";
        $O{final_message} = "direct-svn-merge: merge to $O{target_branch} done";
        $O{"no-unit-tests"} = 1 unless $O{"unit-tests"};
        $O{"no-cleanup"} = 1 if $O{"no-commit"};
        $O{"no-check-release-flag"} = 1;
    }

    $O{yamb_to} = $O{requester} || $caller_login;

    $O{hostname} = `hostname -f`;
    chomp $O{hostname};

    #$O{tmp_dir} = "/tmp/automerge.dpFBUr9qmc";
    return \%O;
}

sub write_log
{
    my $details = {
        login => [getpwuid($<)]->[0],
        logtime => strftime("%Y-%m-%d %H:%M:%S", localtime),
        reintegrate_no_unit_tests => 1,
    };

    path($LOG_FILE)->append( JSON::to_json($details, {utf8 => 1, canonical =>1}), "\n" );
    return;
}
