#!/usr/bin/perl

# PODNAME: is_git_synced


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

# '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,
    '--ignore_missing' => $FALSE,
    '--version'        => $FALSE,
    '--show_ok'        => $FALSE,
    '--help'           => $FALSE,
);
lock_keys(%OPTIONS); # To make sure you haven't misspelled key

my %STATUSES = (
    'success' => 1,
    'fail'    => 2,
    'skip'    => 3,
);
lock_keys(%STATUSES); # To make sure you haven't misspelled key

# subs
sub get_paths_and_set_options {

    my @paths;

    foreach my $argv (@ARGV) {
        if (grep {$argv eq $_} 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_dir {
    my ($path) = @_;

    if (-d $path) {
        return $STATUSES{success};
    } elsif ( (not -d $path) and $OPTIONS{'--ignore_missing'}) {
        return $STATUSES{skip};
    } else {
        return (
            $STATUSES{fail},
            "path '$path' is not a directory"
        );
    }
}

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

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

    my $has_untracked = $FALSE;
    foreach my $line (@remotes) {
        $has_untracked = $TRUE if $line =~ /^\?\?/;
    }

    if (not $has_untracked) {
        return $STATUSES{success};
    } else {
        return (
            $STATUSES{fail},
            "path '$path' has untracked files",
        );
    }
}

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

    `cd $path; git status 2>&1`;

    if (not ${^CHILD_ERROR_NATIVE}) {
        return $STATUSES{success};
    } else {
        return (
            $STATUSES{fail},
            "path '$path' is not a git repository"
        );
    }
}

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

    `cd $path; git diff --exit-code 2>&1`;

    if (not ${^CHILD_ERROR_NATIVE}) {
        return $STATUSES{success};
    } else {
        return (
            $STATUSES{fail},
            "path '$path' has unstaged changes"
        );
    }
}

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

    `cd $path; git diff --cached --exit-code 2>&1`;

    if (not ${^CHILD_ERROR_NATIVE}) {
        return $STATUSES{success};
    } else {
        return (
            $STATUSES{fail},
            "path '$path' has staged changes",
        );
    }
}

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

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

    my $has_origin;
    foreach my $remote (@remotes) {
        $has_origin = $TRUE if $remote eq 'origin';
    }

    if ($has_origin) {
        return $STATUSES{success};
    } else {
        return (
            $STATUSES{fail},
            "path '$path' has no remote 'origin'",
        );
    }
}

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

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

    my $has_divergences_with_origin;
    foreach my $branch (@branches) {
        next if $branch eq '(no branch)';
        next if $branch =~ /(detached .*)/;

        my $local = `cd $path; git rev-parse --verify $branch 2>&1`;
        my $origin = `cd $path; git rev-parse --verify origin/$branch 2>&1`;

        $has_divergences_with_origin = $TRUE if $local ne $origin;
    }

    if (not $has_divergences_with_origin) {
        return $STATUSES{success};
    } else {
        return (
            $STATUSES{fail},
            "path '$path' has some divergences with remote 'origin'",
        );
    }
}

# main
my @paths = get_paths_and_set_options();

if ($OPTIONS{'--help'}) {
    pod2usage({
        -exitval => $SUCCESS_EXIT_STATUS,
    });
} elsif ($OPTIONS{'--version'}) {
    print "is_git_synced $App::IsGitSynced::VERSION\n";
    exit $SUCCESS_EXIT_STATUS;
}

my $was_error;

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

foreach my $path (@paths) {

    my @checks = (
        \&is_dir,
        \&is_git_repo,
        \&has_no_untracked,
        \&has_no_unstaged_changes,
        \&has_no_staged_changes,
        \&has_origin,
        \&has_no_divergences_with_origin,
    );

    my $local_error;
    my $skipped;

    CHECKS:
    foreach my $check (@checks) {
        my ($check_result, $fail_text) = $check->($path);

        if ($check_result == $STATUSES{success}) {
            next CHECKS;
        } elsif ($check_result == $STATUSES{skip}) {
            $skipped = $TRUE;
            last CHECKS;
        } elsif ($check_result == $STATUSES{fail}) {
            error($fail_text);
            $local_error = 1;
            last CHECKS;
        }
    };

    if ($local_error) {
        $was_error++;
    } elsif ($skipped) {
        print "Skipping path '$path'\n" if (!$OPTIONS{'--quiet'} && !$OPTIONS{'--only_errors'});
    } 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;
}

__END__

=pod

=encoding UTF-8

=head1 NAME

is_git_synced

=head1 VERSION

version 1.0.3

=head1 SYNOPSIS

is_git_synced [options] dir1 [dir2 ...]

 Options:

      --quiet           Script will not output anything
      --only_errors     Script will write only dirs with errors
      --ignore_missing  Will ignore missing dirs
      --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 no 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 SOURCE CODE

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

=begin comment

Subs that check git repo

They return 2 values: 1) $check_status (from %STATUSES) 2) $fail_text that
should be printed

=end comment

=head1 AUTHOR

Ivan Bessarabov <ivan@bessarabov.ru>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Ivan Bessarabov.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut