File Coverage

blib/lib/App/Git/Workflow/Command/Changes.pm
Criterion Covered Total %
statement 24 141 17.0
branch 0 64 0.0
condition 0 15 0.0
subroutine 8 14 57.1
pod 6 6 100.0
total 38 240 15.8


line stmt bran cond sub pod time code
1             package App::Git::Workflow::Command::Changes;
2              
3             # Created on: 2014-06-11 10:00:36
4             # Create by: Ivan Wills
5             # $Id$
6             # $Revision$, $HeadURL$, $Date$
7             # $Revision$, $Source$, $Date$
8              
9 1     1   1097 use strict;
  1         2  
  1         43  
10 1     1   7 use warnings;
  1         2  
  1         26  
11 1     1   6 use version;
  1         2  
  1         6  
12 1     1   92 use English qw/ -no_match_vars /;
  1         2  
  1         6  
13 1     1   923 use Time::Piece;
  1         9813  
  1         5  
14 1     1   89 use App::Git::Workflow;
  1         2  
  1         46  
15 1     1   7 use App::Git::Workflow::Command qw/get_options/;
  1         2  
  1         44  
16 1     1   6 use utf8;
  1         3  
  1         9  
17              
18             our $VERSION = version->new(1.1.13);
19             our $workflow = App::Git::Workflow->new;
20             our ($name) = $PROGRAM_NAME =~ m{^.*/(.*?)$}mxs;
21             our %option;
22              
23             sub run {
24 0     0 1   my ($self) = @_;
25 0           %option = (
26             period => 'day',
27             );
28 0           get_options(
29             \%option,
30             'remote|r',
31             'all|a',
32             'fmt|format|f=s',
33             'changes|c',
34             'commits|C',
35             'multi_user|multi-user|m',
36             'files|paths|f=s@',
37             'min|min-commits|M=i',
38             'since|s=s',
39             'until|u=s',
40             'period|p=s',
41             'periods|P=i',
42             'merges|m!',
43             );
44              
45 0           my @stats;
46 0           my $total_commits = 0;
47 0           my $since = $option{since};
48              
49 0 0         if (!$since) {
50 0           my $now = localtime;
51             my $period
52             = $option{period} eq 'day' ? 1
53             : $option{period} eq 'week' ? 7
54             : $option{period} eq 'month' ? 30
55 0 0         : $option{period} eq 'year' ? 365
    0          
    0          
    0          
56             : die "Unknown period '$option{period}' please choose one of day, week, month or year\n";
57 0 0         $since
    0          
58             = $now->wday == 1 ? localtime(time - 3 * $period * 24 * 60 * 60)->ymd
59             : $now->wday == 7 ? localtime(time - 2 * $period * 24 * 60 * 60)->ymd
60             : localtime(time - 1 * $period * 24 * 60 * 60)->ymd;
61             }
62              
63 0           my @options;
64 0 0         push @options, '-r' if $option{remote};
65 0 0         push @options, '-a' if $option{all};
66             my @log = (
67             '--format=format:/// %h %an',
68             '--name-only',
69 0 0         ($option{merges} ? () : '--no-merges'),
70             );
71              
72 0 0         if (@ARGV) {
73 0           push @{ $option{files} }, @ARGV;
  0            
74             }
75              
76 0           my @paths;
77 0           for my $file (@{ $option{files} }) {
  0            
78 0           my $path = $file;
79 0           $path =~ s/[*]/[^\/]*/g;
80 0           push @paths, $path;
81             }
82              
83 0   0       my $periods = $option{periods} || 1;
84 0           while ($periods--) {
85 0           my $commits = 0;
86 0           my %paths;
87             my %users;
88 0           my @dates;
89 0 0         if ($option{periods}) {
90 0           @dates = $self->dates($option{period}, $option{periods}--);
91             }
92             else {
93             @dates = (
94             "--since=$since",
95 0 0         ($option{until} ? "--until=$option{until}" : ()),
96             );
97             }
98              
99 0           for my $branch ($workflow->git->branch(@options)) {
100 0 0         next if $branch =~ / -> /;
101 0           $branch =~ s/^[*]?\s*//;
102 0           my ($last_hash, $last_name);
103 0           for my $log ( $workflow->git->log( @log, @dates, $branch, '--', @{$option{files}} ) ) {
  0            
104 0           my (undef, $hash, $name) = split /\s/, $log, 3;
105 0 0         if ($hash) {
106 0           $last_hash = $hash;
107 0           $last_name = $name;
108 0           next;
109             }
110 0 0         next if !$log;
111 0           my $file = $log;
112 0           for my $path (@paths) {
113 0           $file =~ s/($path).*$/$1/;
114             }
115 0           $paths{$file}{$last_name}{$last_hash} = 1;
116 0           $users{$last_name} = 1;
117 0           $commits++;
118             }
119             #use Data::Dumper qw/Dumper/;
120             #$Data::Dumper::Sortkeys = 1;
121             #$Data::Dumper::Indent = 1;
122             #die Dumper \%paths if $commits > 10;
123             }
124              
125 0           for my $path (keys %paths) {
126 0           for my $user (keys %{ $paths{$path} }) {
  0            
127 0           my $commits = $paths{$path}{$user};
128             $paths{$path}{$user} = {
129 0           commit_count => scalar keys %{ $paths{$path}{$user} },
130 0           $option{commits} ? (commits => [keys %{ $paths{$path}{$user} }]) : (),
131 0 0         $option{changes} ? (changes => $self->changes($commits)) : (),
    0          
132             };
133             }
134             }
135             my $dates = join ' - ',
136 0           map {/=(.*)$/; $1}
  0            
  0            
137             @dates;
138 0 0         push @stats, {
    0          
    0          
139             period => $dates,
140             ( %paths ? (commits => $commits ) : () ),
141             ( %paths ? (paths => \%paths ) : () ),
142             ( %paths ? (users => [keys %users]) : () ),
143             };
144 0           $total_commits += $commits;
145             }
146              
147 0   0       my $fmt = 'fmt_' . ($option{fmt} || 'table');
148 0 0         if ($self->can($fmt)) {
149 0           $self->$fmt(\@stats, $total_commits);
150             }
151              
152 0           return;
153             }
154              
155             sub dates {
156 0     0 1   my ($self, $period, $count) = @_;
157              
158 0           my $now = localtime;
159 0 0         $period
    0          
    0          
    0          
160             = $period eq 'day' ? 1
161             : $period eq 'week' ? 7 - $now->wdaygg
162             : $period eq 'month' ? 30
163             : $period eq 'year' ? 365
164             : die "Unknown period '$option{period}' please choose one of day, week, month or year\n";
165              
166 0           my $until = localtime(time - ($count - 1) * $period * 24 * 60 * 60);
167 0 0         my $since
    0          
168             = $until->wday == 1 ? localtime(time - 3 * $count * $period * 24 * 60 * 60)
169             : $until->wday == 7 ? localtime(time - 2 * $count * $period * 24 * 60 * 60)
170             : localtime(time - 1 * $count * $period * 24 * 60 * 60);
171              
172             return (
173 0           "--since=" . $since->ymd,
174             "--until=" . $until->ymd,
175             );
176             }
177              
178             sub changes {
179 0     0 1   my ($self, $commits) = @_;
180 0           my %changes = (
181             lines_added => 0,
182             lines_removed => 0,
183             files => {},
184             files_added => 0,
185             files_removed => 0,
186             );
187              
188 0           for my $commit (keys %$commits) {
189             # get the stats from each commit
190 0           my @show = $workflow->git->show($commit);
191 0           $changes{lines_added} += grep {/^[+](?:[^+]|[+][^+]|[+][+]\s|$)/} @show;
  0            
192 0           $changes{lines_removed} += grep {/^[-](?:[^-]|[-][^-]|[-][-]\s|$)/} @show;
  0            
193             $changes{files} = {
194 0 0         %{ $changes{files} || {} },
195 0   0       map {/^[+]{3}\s+b\/(.*)$/; ($1 || "" => 1) }
  0            
196 0           grep {/^[+]{3}\s/}
  0            
197             @show
198             };
199 0           $changes{total}++;
200             }
201 0 0         $changes{files} = keys %{ $changes{files} || {} };
  0            
202              
203 0           return \%changes;
204             }
205              
206             sub fmt_table {
207 0     0 1   my ($self, $stats) = @_;
208 0           my $fmt = " %-25s % 7d";
209 0           my $max = 1;
210 0   0       my $paths = $stats->[0]{paths} || {};
211 0   0       my $users = $stats->[0]{users} || [];
212 0   0       my $total = $stats->[0]{commits} || 0;
213              
214 0 0         if ($option{changes}) {
215 0           $fmt .= " % 9d % 9d % 5d";
216 0           my $fmt2 = $fmt;
217 0           $fmt2 =~ s/d/s/g;
218 0           printf "$fmt2\n", qw/Name Commits Added Removed Files/;
219 0           $max = 4;
220             }
221              
222             #my @users =
223             # reverse sort {$users->{$a}{commit_count} <=> $users->{$b}{commit_count}}
224             # grep { $users->{$_}{commit_count} >= ($option{min} || 0) }
225             # keys %$users;
226             #my @paths =
227              
228 0           for my $path (sort keys %$paths) {
229             # if --multi-user is specified skip path if there are less than 2 users making changes
230 0 0 0       next if $option{multi_user} && ((keys %{ $paths->{$path} }) < 2);
  0            
231              
232 0           print "$path\n";
233 0           for my $user (sort keys %{ $paths->{$path} }) {
  0            
234             my @out = (
235             $user,
236             $paths->{$path}{$user}{commit_count},
237             $paths->{$path}{$user}{changes}{lines_added},
238             $paths->{$path}{$user}{changes}{lines_removed},
239             $paths->{$path}{$user}{changes}{files},
240 0           );
241 0           printf "$fmt\n", @out[0..$max];
242             }
243             }
244 0           print "Total commits = $total\n";
245              
246 0           return;
247             }
248              
249             sub fmt_json {
250 0     0 1   my ($self, $users, $total) = @_;
251 0           require JSON;
252              
253 0           print JSON::encode_json({ total => $total, users => $users });
254             }
255              
256             sub fmt_perl {
257 0     0 1   my ($self, $users, $total) = @_;
258 0           require Data::Dumper;
259              
260 0           local $Data::Dumper::Indent = 1;
261 0           print Data::Dumper::Dumper({ total => $total, users => $users });
262             }
263              
264             1;
265              
266             __DATA__
267              
268             =head1 NAME
269              
270             git-changes - Stats on the number of commits by committer
271              
272             =head1 VERSION
273              
274             This documentation refers to git-changes version 1.1.13
275              
276             =head1 SYNOPSIS
277              
278             git-changes [option]
279              
280             OPTIONS:
281             -r --remote Changes to remote branches
282             -a --all Changes to any branch (remote or local)
283             -c --changes Add stats for lines added/removed
284             -C --commits Output the individual commits (with --format json)
285             -s --since[=]YYYY-MM-DD
286             Only commits since this date
287             -u --until[=]YYYY-MM-DD
288             Only commits up until this date
289             -f --format[=](table|json|csv)
290             Change how the data is presented
291             - table : shows the data in a simple table
292             - json : returns the raw data as a json object
293             - perl : Dump the data structure
294             -p --period=[day|week|month|year]
295             If --since is not specified this works out the date for the
296             last day/week/month/year
297             -P --periods[=]int
298             Generate stats for more than one period.
299             -M --min-commit[=]int
300             Only show stats for users with at least this number of commits
301             -m --merges Count merge commits
302             --no-merges
303             Don't count merge commits
304              
305             -v --verbose Show more detailed option
306             --version Prints the version information
307             --help Prints this help information
308             --man Prints the full documentation for git-changes
309              
310             =head1 DESCRIPTION
311              
312             The C<git-changes> command allows to get statistics on who is committing
313             to the git repository.
314              
315             =head1 SUBROUTINES/METHODS
316              
317             =head2 C<run ()>
318              
319             Executes the git workflow command
320              
321             =head2 C<dates ($period, $count)>
322              
323             Returns the C<--since> and C<--until> dates for the C<$period> specified
324              
325             =head2 C<changes ($commits)>
326              
327             Calculates the changes for C<$commits>.
328              
329             =head2 C<fmt_table ()>
330              
331             Output a table
332              
333             =head2 C<fmt_json ()>
334              
335             Output JSON
336              
337             =head2 C<fmt_perl ()>
338              
339             Output a Perl object
340              
341             =head1 DIAGNOSTICS
342              
343             =head1 CONFIGURATION AND ENVIRONMENT
344              
345             =head1 DEPENDENCIES
346              
347             =head1 INCOMPATIBILITIES
348              
349             =head1 BUGS AND LIMITATIONS
350              
351             There are no known bugs in this module.
352              
353             Please report problems to Ivan Wills (ivan.wills@gmail.com).
354              
355             Patches are welcome.
356              
357             =head1 AUTHOR
358              
359             Ivan Wills - (ivan.wills@gmail.com)
360              
361             =head1 LICENSE AND COPYRIGHT
362              
363             Copyright (c) 2014 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW Australia 2077).
364             All rights reserved.
365              
366             This module is free software; you can redistribute it and/or modify it under
367             the same terms as Perl itself. See L<perlartistic>. This program is
368             distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
369             without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
370             PARTICULAR PURPOSE.
371              
372             =cut