#!/usr/bin/perl

=encoding UTF-8

=head1 NAME

is_git_synced - script to find out if the local git repo is fully synced

=head1 VERSION

Version 0.03

=head1 SYNOPSIS

is_git_synced [options] dir1 [dir2 ...]

 Options:

      --quiet           Script will not output anything
      --only_errors     Script will write only dirs with errors
      --show_ok         Show 'ok' message if everthing is synced
      --help            Show this message
      --version         Show version number

Script checks every specified dir if it is a git repo and it has ho local
changes that are not in remote repository origin. Script by default will
output information about every checking dir in the separate line. The exit
status will be 0 if everything is synced and 1 otherwise.

Project url: https://github.com/bessarabov/App-IsGitSynced

=head1 AUTHOR

Ivan Bessarabov, C<< <ivan@bessarabov.ru> >>

=head1 SOURCE CODE

The source code for this module is hosted on GitHub
L<https://github.com/bessarabov/App-IsGitSynced>

=head1 BUGS

Please report any bugs or feature requests in GitHub Issues
L<https://github.com/bessarabov/App-IsGitSynced/issues>

=head1 LICENSE AND COPYRIGHT

Copyright 2012 Ivan Bessarabov.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut

use strict;
use warnings FATAL => 'all';
use Pod::Usage;
use Term::ANSIColor qw(:constants);
use Hash::Util qw(lock_keys);

our $VERSION = 0.03;

# 'constants'
my $true  = 1;
my $false = '';

my $success_exit_status = 0;
my $error_exit_status   = 1;

# global vars
my %options = (
    '--quiet'       => $false,
    '--only_errors' => $false,
    '--show_ok'     => $false,
    '--help'        => $false,
    '--version'     => $false,
);
lock_keys(%options); # To make sure you haven't misspelled key

# subs
sub get_paths_and_set_options {

    my @paths;

    foreach my $argv (@ARGV) {
        if ($argv ~~ [keys %options]) {
            $options{$argv} = $true;
        } else {
            push @paths, $argv;
        }
    }

    return @paths;
}

sub error {
    my ($message) = @_;

    if (!$options{'--quiet'}) {
        if (-t STDOUT) {
            print RED();
            print "Error: $message\n";
            print RESET();
        } else {
            print "Error: $message\n";
        }
    }
}

sub is_git_repo {
    my ($path) = @_;

    `cd $path; git status 2>&1`;
    return ${^CHILD_ERROR_NATIVE} ? $false : $true;
}

sub has_unstaged_changes {
    my ($path) = @_;

    `cd $path; git diff --exit-code 2>&1`;
    return ${^CHILD_ERROR_NATIVE} ? $false : $true;
}

sub has_staged_changes {
    my ($path) = @_;

    `cd $path; git diff --cached --exit-code 2>&1`;
    return ${^CHILD_ERROR_NATIVE} ? $false : $true;
}

sub has_untracked {
    my ($path) = @_;

    my $output = `cd $path; git status --porcelain`;
    my @remotes = split(/\n/, $output);

    foreach my $line (@remotes) {
        return $true if $line =~ /^\?\?/;
    }

    return $false;
}

sub has_origin {
    my ($path) = @_;

    my $output = `cd $path; git remote`;
    my @remotes = split(/\n/, $output);

    foreach my $remote (@remotes) {
        return $true if $remote eq 'origin';
    }

    return $false;
}

# http://stackoverflow.com/questions/8830833/check-that-the-local-git-repo-has-everything-commited-and-pushed-to-master
sub has_divergences_with_origin {
    my ($path) = @_;

    my $output = `cd $path; git branch`;
    my @branches = map { s/..(.*)/$1/; $_; } split(/\n/, $output);

    foreach my $branch (@branches) {
        next if $branch eq '(no branch)';
        my $local = `cd $path; git rev-parse --verify $branch 2>&1`;
        my $origin = `cd $path; git rev-parse --verify origin/$branch 2>&1`;
        return $true if $local ne $origin;
    }

    return $false;
}

my $checks = [
    {
        text => sub { error("path '$_[0]' is a file") },
        func => sub { return -f $_[0] },
    },
    {
        text => sub { error("path '$_[0]' does not exist") },
        func => sub { return !-d $_[0] },
    },
    {
        text => sub { error("path '$_[0]' is not a git repository") },
        func => sub { return !is_git_repo($_[0]) },
    },
    {
        text => sub { error("path '$_[0]' has unstaged changes") },
        func => sub { return !has_unstaged_changes($_[0]) },
    },
    {
        text => sub { error("path '$_[0]' has staged changes") },
        func => sub { return !has_staged_changes($_[0]) },
    },
    {
        text => sub { error("path '$_[0]' has untracked files") },
        func => sub { return has_untracked($_[0]) },
    },
    {
        text => sub { error("path '$_[0]' has no remote 'origin'") },
        func => sub { return !has_origin($_[0]) },
    },
    {
        text => sub { error("path '$_[0]' has some divergences with remote 'origin'") },
        func => sub { return has_divergences_with_origin($_[0]) },
    },
];

# main
my @paths = get_paths_and_set_options();

if ($options{'--help'}) {
    pod2usage({
        -exitval => $success_exit_status,
    });
} elsif ($options{'--version'}) {
    print "is_git_synced $VERSION\n";
    exit $success_exit_status;
}

my $was_error;

if (!@paths) {
    error("no required path specified");
    $was_error++;
}

foreach my $path (@paths) {
    my $local_error;

    CHECKS:
    foreach my $check (@{$checks}) {
        if ($check->{func}->($path)) {
            $check->{text}->($path);
            $local_error = 1;
            last CHECKS;
        }
    };

    if ($local_error) {
        $was_error++;
    } else {
        print "Success: path '$path' has no local changes and fully synced to remote\n" if (!$options{'--quiet'} && !$options{'--only_errors'});
    }
}

if (!$was_error) {
    if ($options{'--show_ok'}) {
        print GREEN();
        print "ok\n";
        print RESET();
    }
    exit $success_exit_status;
} else {
    exit $error_exit_status;
}
