Group
Extension

App-Git-Workflow/lib/App/Git/Workflow/Command/Changes.pm

package App::Git::Workflow::Command::Changes;

# Created on: 2014-06-11 10:00:36
# Create by:  Ivan Wills
# $Id$
# $Revision$, $HeadURL$, $Date$
# $Revision$, $Source$, $Date$

use strict;
use warnings;
use version;
use English qw/ -no_match_vars /;
use Time::Piece;
use App::Git::Workflow;
use App::Git::Workflow::Command qw/get_options/;
use utf8;

our $VERSION  = version->new(1.1.13);
our $workflow = App::Git::Workflow->new;
our ($name)   = $PROGRAM_NAME =~ m{^.*/(.*?)$}mxs;
our %option;

sub run {
    my ($self) = @_;
    %option = (
        period => 'day',
    );
    get_options(
        \%option,
        'remote|r',
        'all|a',
        'fmt|format|f=s',
        'changes|c',
        'commits|C',
        'multi_user|multi-user|m',
        'files|paths|f=s@',
        'min|min-commits|M=i',
        'since|s=s',
        'until|u=s',
        'period|p=s',
        'periods|P=i',
        'merges|m!',
    );

    my @stats;
    my $total_commits = 0;
    my $since = $option{since};

    if (!$since) {
        my $now = localtime;
        my $period
            = $option{period} eq 'day'   ? 1
            : $option{period} eq 'week'  ? 7
            : $option{period} eq 'month' ? 30
            : $option{period} eq 'year'  ? 365
            :                              die "Unknown period '$option{period}' please choose one of day, week, month or year\n";
        $since
            = $now->wday == 1 ? localtime(time - 3 * $period * 24 * 60 * 60)->ymd
            : $now->wday == 7 ? localtime(time - 2 * $period * 24 * 60 * 60)->ymd
            :                   localtime(time - 1 * $period * 24 * 60 * 60)->ymd;
    }

    my @options;
    push @options, '-r' if $option{remote};
    push @options, '-a' if $option{all};
    my @log = (
        '--format=format:/// %h %an',
        '--name-only',
        ($option{merges} ? () : '--no-merges'),
    );

    if (@ARGV) {
        push @{ $option{files} }, @ARGV;
    }

    my @paths;
    for my $file (@{ $option{files} }) {
        my $path = $file;
        $path =~ s/[*]/[^\/]*/g;
        push @paths, $path;
    }

    my $periods = $option{periods} || 1;
    while ($periods--) {
        my $commits = 0;
        my %paths;
        my %users;
        my @dates;
        if ($option{periods}) {
            @dates = $self->dates($option{period}, $option{periods}--);
        }
        else {
            @dates = (
                "--since=$since",
                ($option{until} ? "--until=$option{until}" : ()),
            );
        }

        for my $branch ($workflow->git->branch(@options)) {
            next if $branch =~ / -> /;
            $branch =~ s/^[*]?\s*//;
            my ($last_hash, $last_name);
            for my $log ( $workflow->git->log( @log, @dates, $branch, '--', @{$option{files}} ) ) {
                my (undef, $hash, $name) = split /\s/, $log, 3;
                if ($hash) {
                    $last_hash = $hash;
                    $last_name = $name;
                    next;
                }
                next if !$log;
                my $file = $log;
                for my $path (@paths) {
                    $file =~ s/($path).*$/$1/;
                }
                $paths{$file}{$last_name}{$last_hash} = 1;
                $users{$last_name} = 1;
                $commits++;
            }
            #use Data::Dumper qw/Dumper/;
            #$Data::Dumper::Sortkeys = 1;
            #$Data::Dumper::Indent = 1;
            #die Dumper \%paths if $commits > 10;
        }

        for my $path (keys %paths) {
            for my $user (keys %{ $paths{$path} }) {
                my $commits = $paths{$path}{$user};
                $paths{$path}{$user} = {
                    commit_count => scalar keys %{ $paths{$path}{$user} },
                    $option{commits} ? (commits => [keys %{ $paths{$path}{$user} }]) : (),
                    $option{changes} ? (changes => $self->changes($commits)) : (),
                };
            }
        }
        my $dates = join ' - ',
            map {/=(.*)$/; $1}
            @dates;
        push @stats, {
            period  => $dates,
            ( %paths ? (commits => $commits     ) : () ),
            ( %paths ? (paths   => \%paths      ) : () ),
            ( %paths ? (users   => [keys %users]) : () ),
        };
        $total_commits += $commits;
    }

    my $fmt = 'fmt_' . ($option{fmt} || 'table');
    if ($self->can($fmt)) {
        $self->$fmt(\@stats, $total_commits);
    }

    return;
}

sub dates {
    my ($self, $period, $count) = @_;

    my $now = localtime;
    $period
        = $period eq 'day'   ? 1
        : $period eq 'week'  ? 7  - $now->wdaygg
        : $period eq 'month' ? 30
        : $period eq 'year'  ? 365
        :                      die "Unknown period '$option{period}' please choose one of day, week, month or year\n";

    my $until = localtime(time - ($count - 1) * $period * 24 * 60 * 60);
    my $since
        = $until->wday == 1 ? localtime(time - 3 * $count * $period * 24 * 60 * 60)
        : $until->wday == 7 ? localtime(time - 2 * $count * $period * 24 * 60 * 60)
        :                     localtime(time - 1 * $count * $period * 24 * 60 * 60);

    return (
        "--since=" . $since->ymd,
        "--until=" . $until->ymd,
    );
}

sub changes {
    my ($self, $commits) = @_;
    my %changes = (
        lines_added   => 0,
        lines_removed => 0,
        files         => {},
        files_added   => 0,
        files_removed => 0,
    );

    for my $commit (keys %$commits) {
        # get the stats from each commit
        my @show = $workflow->git->show($commit);
        $changes{lines_added}   += grep {/^[+](?:[^+]|[+][^+]|[+][+]\s|$)/} @show;
        $changes{lines_removed} += grep {/^[-](?:[^-]|[-][^-]|[-][-]\s|$)/} @show;
        $changes{files} = {
            %{ $changes{files} || {} },
            map {/^[+]{3}\s+b\/(.*)$/; ($1 || "" => 1) }
            grep {/^[+]{3}\s/}
            @show
        };
        $changes{total}++;
    }
    $changes{files} = keys %{ $changes{files} || {} };

    return \%changes;
}

sub fmt_table {
    my ($self, $stats) = @_;
    my $fmt = "  %-25s % 7d";
    my $max = 1;
    my $paths = $stats->[0]{paths}   || {};
    my $users = $stats->[0]{users}   || [];
    my $total = $stats->[0]{commits} || 0;

    if ($option{changes}) {
        $fmt .= " % 9d % 9d % 5d";
        my $fmt2 = $fmt;
        $fmt2 =~ s/d/s/g;
        printf "$fmt2\n", qw/Name Commits Added Removed Files/;
        $max = 4;
    }

    #my @users =
    #    reverse sort {$users->{$a}{commit_count} <=> $users->{$b}{commit_count}}
    #    grep { $users->{$_}{commit_count} >= ($option{min} || 0) }
    #    keys %$users;
    #my @paths =

    for my $path (sort keys %$paths) {
        # if --multi-user is specified skip path if there are less than 2 users making changes
        next if $option{multi_user} && ((keys %{ $paths->{$path} }) < 2);

        print "$path\n";
        for my $user (sort keys %{ $paths->{$path} }) {
            my @out = (
                $user,
                $paths->{$path}{$user}{commit_count},
                $paths->{$path}{$user}{changes}{lines_added},
                $paths->{$path}{$user}{changes}{lines_removed},
                $paths->{$path}{$user}{changes}{files},
            );
            printf "$fmt\n", @out[0..$max];
        }
    }
    print "Total commits = $total\n";

    return;
}

sub fmt_json {
    my ($self, $users, $total) = @_;
    require JSON;

    print JSON::encode_json({ total => $total, users => $users });
}

sub fmt_perl {
    my ($self, $users, $total) = @_;
    require Data::Dumper;

    local $Data::Dumper::Indent = 1;
    print Data::Dumper::Dumper({ total => $total, users => $users });
}

1;

__DATA__

=head1 NAME

git-changes - Stats on the number of commits by committer

=head1 VERSION

This documentation refers to git-changes version 1.1.13

=head1 SYNOPSIS

   git-changes [option]

 OPTIONS:
  -r --remote   Changes to remote branches
  -a --all      Changes to any branch (remote or local)
  -c --changes  Add stats for lines added/removed
  -C --commits  Output the individual commits (with --format json)
  -s --since[=]YYYY-MM-DD
                Only commits since this date
  -u --until[=]YYYY-MM-DD
                Only commits up until this date
  -f --format[=](table|json|csv)
                Change how the data is presented
                   - table : shows the data in a simple table
                   - json  : returns the raw data as a json object
                   - perl  : Dump the data structure
  -p --period=[day|week|month|year]
                If --since is not specified this works out the date for the
                last day/week/month/year
  -P --periods[=]int
                Generate stats for more than one period.
  -M --min-commit[=]int
                Only show stats for users with at least this number of commits
  -m --merges   Count merge commits
     --no-merges
                Don't count merge commits

  -v --verbose  Show more detailed option
     --version  Prints the version information
     --help     Prints this help information
     --man      Prints the full documentation for git-changes

=head1 DESCRIPTION

The C<git-changes> command allows to get statistics on who is committing
to the git repository.

=head1 SUBROUTINES/METHODS

=head2 C<run ()>

Executes the git workflow command

=head2 C<dates ($period, $count)>

Returns the C<--since> and C<--until> dates for the C<$period> specified

=head2 C<changes ($commits)>

Calculates the changes for C<$commits>.

=head2 C<fmt_table ()>

Output a table

=head2 C<fmt_json ()>

Output JSON

=head2 C<fmt_perl ()>

Output a Perl object

=head1 DIAGNOSTICS

=head1 CONFIGURATION AND ENVIRONMENT

=head1 DEPENDENCIES

=head1 INCOMPATIBILITIES

=head1 BUGS AND LIMITATIONS

There are no known bugs in this module.

Please report problems to Ivan Wills (ivan.wills@gmail.com).

Patches are welcome.

=head1 AUTHOR

Ivan Wills - (ivan.wills@gmail.com)

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2014 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW Australia 2077).
All rights reserved.

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself. See L<perlartistic>.  This program is
distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.