File Coverage

blib/lib/App/Git/Workflow/Command/UpToDate.pm
Criterion Covered Total %
statement 148 155 95.4
branch 43 64 67.1
condition 17 30 56.6
subroutine 19 19 100.0
pod 11 11 100.0
total 238 279 85.3


line stmt bran cond sub pod time code
1             package App::Git::Workflow::Command::UpToDate;
2              
3             # Created on: 2014-01-16 04:14:31
4             # Create by: Ivan Wills
5             # $Id$
6             # $Revision$, $HeadURL$, $Date$
7             # $Revision$, $Source$, $Date$
8              
9 2     2   92731 use strict;
  2         15  
  2         60  
10 2     2   10 use warnings;
  2         4  
  2         49  
11 2     2   405 use version;
  2         1695  
  2         12  
12 2     2   608 use English qw/ -no_match_vars /;
  2         3374  
  2         11  
13 2     2   1249 use App::Git::Workflow;
  2         5  
  2         164  
14 2     2   505 use App::Git::Workflow::Command qw/get_options/;
  2         6  
  2         118  
15 2     2   14 use Carp qw/cluck/;
  2         4  
  2         3456  
16              
17             our $VERSION = version->new(1.1.16);
18             our $workflow = App::Git::Workflow->new;
19             our ($name) = $PROGRAM_NAME =~ m{^.*/(.*?)$}mxs;
20             our %option;
21             our %p2u_extra;
22              
23             sub run {
24 16     16 1 49 my $self = shift;
25 16   100     162 %option = (
26             format => 'test',
27             max_history => $workflow->config('workflow.max-history') || 1,
28             branches => 0,
29             );
30 16 100       126 get_options(
31             \%option,
32             'tag|t=s',
33             'branch|b=s',
34             'local|l!',
35             'remote|r!',
36             'format|f=s',
37             'quick|q!',
38             'include|i=s',
39             'exclude|e=s',
40             'all',
41             'max_history|max-history|m=i',
42             'fetch|F',
43             'fix|x',
44             ) or return;
45              
46 15   100     65 my $action = shift @ARGV || 'am_i';
47 15         55 my $format = 'format_' . $option{format};
48              
49 15         57 $action =~ s/-/_/g;
50 15         49 $action = "do_$action";
51              
52 15 100 100     223 if ( !$self->can($action) ) {
    100          
53 1         4 $action =~ s/^do_//;
54 1         3 $action =~ s/_/-/;
55 1         74 warn "Unknown action '$action'!\n";
56 1         12 Pod::Usage::pod2usage( %p2u_extra, -verbose => 1 );
57 1         9718 return 1;
58             }
59             elsif ( $action eq 'do_show' && !$self->can($format) ) {
60 1         68 warn "Unknown format '$option{format}'!\n";
61 1         13 Pod::Usage::pod2usage( %p2u_extra, -verbose => 1 );
62 1         9813 return 1;
63             }
64              
65 13         41 $workflow->{VERBOSE} = $option{verbose};
66 13         34 $workflow->{TEST } = $option{test};
67              
68 13 100       43 $workflow->git->fetch if $option{fetch};
69              
70             # do stuff here
71 13 50       40 if ($option{branch_age}) {
72 0         0 return branch_age();
73             }
74              
75 13 100       36 if ($action eq 'do_show') {
76 7         20 $option{branches} = 1;
77             }
78 13         84 my @releases = $workflow->releases(%option);
79              
80 13 50       46 if ($option{verbose}) {
81 0         0 my $local = localtime($releases[-1]{time});
82 0         0 my $now = localtime;
83 0         0 my $time = time;
84 0         0 warn <<"DETAILS";
85             Branch : $releases[-1]{name}
86             SHA : $releases[-1]{sha}
87             Time : $local ($releases[-1]{time})
88             Now : $now ($time)
89              
90             DETAILS
91             }
92              
93 13 100 100     63 $option{all} = 1 if $action eq 'do_show' && $option{format} eq 'test';
94              
95 13         64 $self->$action(@releases);
96              
97 13         76 return;
98             }
99              
100             sub do_show {
101 7     7 1 24 my ($self, @releases) = @_;
102 7         26 my $csv = branches_contain(@releases);
103 7 50       25 if ($option{verbose}) {
104 0         0 warn @$csv . " branches found\n";
105             }
106              
107 7         22 my $format = 'format_' . $option{format};
108 7         32 $self->$format($csv, @releases);
109              
110 7         23 return;
111             }
112              
113             sub do_am_i {
114 4     4 1 13 my (undef, @releases) = @_;
115              
116             # work out current branch, check that it contains a release branch
117 4         8 my $format = q/--format=format:%H %at <%an>%ae/;
118              
119 4         8 my $bad = 0;
120 4         11 for my $release (reverse @releases) {
121 4         13 my ($ans) = grep {/$release->{sha}/} $workflow->git->log($format);
  4         25  
122 4 100       13 chomp $ans if $ans;
123 4 100       36 next if $ans;
124 3         19 $bad++;
125 3         266 warn "Missing release $release->{name}!\n";
126             }
127              
128 4 100       25 if ($bad) {
129 3 100       13 if ( $option{fix} ) {
130 1         6 $workflow->git->merge($releases[-1]{name});
131 1         3 return;
132             }
133              
134 2         6 return $bad;
135             }
136             else {
137 1         80 print "Up to date\n";
138             }
139              
140 1         6 return;
141             }
142              
143             sub do_current {
144 1     1 1 3 my (undef, @releases) = @_;
145 1         73 print "Current prod \"$releases[0]{name}\"\n";
146              
147 1         5 return;
148             }
149              
150             sub do_update_me {
151 1     1 1 3 my (undef, @releases) = @_;
152 1         74 print "Merging \"$releases[0]{name}\"\n";
153 1         8 $workflow->git->merge($releases[0]{name});
154              
155 1         2 return;
156             }
157              
158             sub branches_contain {
159 7     7 1 102 my @releases = @_;
160 7 50       38 my @branches = $workflow->branches($option{remote} ? 'remote' : 'both');
161 7         22 my $format = q/--format=format:%at <%an>%ae/;
162 7         10 my @csv;
163              
164             BRANCH:
165 7         19 for my $branch (@branches) {
166 14 50 33     53 next BRANCH if $option{include} && $branch !~ /$option{include}/;
167 14 50 33     66 next BRANCH if $option{exclude} && $branch =~ /$option{exclude}/;
168              
169 14         29 my ($first, $author, $found, $release);
170              
171 14         47 my ($log) = $workflow->git->log($format, qw/-n 1/, $branch);
172 14 50       33 next if !$log;
173 14         61 my ($time, $user) = split /\s+/, $log, 2;
174              
175 14         29 $first = $time;
176 14         20 $author = $user;
177 14 100       55 if ( $time < $releases[-1]{time} ) {
178 4 50 33     12 warn "skipping $branch\n" if $option{verbose} && $option{verbose} > 1;
179 4         10 next BRANCH;
180             }
181              
182 10         25 my $age = time - $releases[0]{time} + 10 * 60 * 60 * 24;
183 10         17 my $ago = -1;
184             RELEASE:
185 10         22 for my $released (reverse @releases) {
186 10         18 $ago++;
187 10 50       27 next RELEASE if !$released->{branches}{$branch};
188              
189 10         16 $release = $released->{name};
190 10 50       28 $age = $released == $releases[-1] ? 0 : time - $released->{time};
191 10         26 last RELEASE;
192             }
193              
194 10 50 33     53 next BRANCH if !$option{all} && !$option{verbose} && $found;
      33        
195              
196 10   50     108 push @csv, [ $release || "Out of date", $branch, $author, int $age / 60 / 60 / 24, $ago];
197 10 0       36 warn +( $found ? 'up to date' : "missing $releases[-1]{name}" ) . "\t$branch\t$author\n" if $option{quick};
    50          
198             }
199              
200 7         17 return \@csv;
201             }
202              
203             sub format_text {
204 1     1 1 4 my (undef, $csv, @releases) = @_;
205              
206 1         2 my @max = (0,0,0);
207 1         3 for my $row (@$csv) {
208 2 100       5 $max[0] = length $row->[0] if $max[0] < length $row->[0];
209 2 50       6 $max[1] = length $row->[1] if $max[1] < length $row->[1];
210 2 100       7 $max[2] = length $row->[2] if $max[2] < length $row->[2];
211             }
212 1         3 for my $row (@$csv) {
213 2         146 printf "%$max[0]s %-$max[1]s %-$max[2]s (%2.0f days old)\n", @$row[0..3];
214             }
215              
216 1         4 return;
217             }
218              
219             sub format_csv {
220 2     2 1 6 my (undef, $csv, @releases) = @_;
221              
222 2 100       10 my $sepperator = $option{format} eq 'tab' ? "\t" : ',';
223 2         5 for my $row (@$csv) {
224 4         248 print +(join $sepperator, @$row), "\n";
225             }
226              
227 2         8 return;
228             }
229             {
230 2     2   21 no warnings qw/once/;
  2         5  
  2         1250  
231             *format_tab = *format_csv;
232             }
233              
234             sub format_json {
235 1     1 1 4 my (undef, $csv, @releases) = @_;
236              
237 1         9 require JSON;
238 1         5 my $repo = $workflow->config('remote.origin.url');
239 1         9 my ($name) = $repo =~ m{[/:](.*?)(?:[.]git)?$}xms;
240              
241             print JSON::encode_json({
242             repository => $repo,
243             name => $name,
244             release => $releases[-1]{name},
245             release_date => '' . localtime($releases[-1]{time}),
246             branches => [
247 1         62 map {{ status => $_->[0], name => $_->[1], last_author => $_->[2], release_age => $_->[3] }}
  2         102  
248             @$csv
249             ]
250             });
251              
252 1         9 return;
253             }
254              
255             sub format_html {
256 1     1 1 4 my (undef, $csv, @releases) = @_;
257              
258 1         2 my $sepperator = "</td><td>";
259 1         63 my $date = localtime;
260 1         8 my $repo = $workflow->config('remote.origin.url');
261 1         95 print <<"HTML";
262             <table>
263             <caption>Branch statuses for <i>$repo</i> ($date)</caption>
264             <thead>
265             <tr>
266             <th>Production Branch/Tag Status</th>
267             <th>Branch Name</th>
268             <th>Last commit owner</th>
269             <th>Included release age (days)</th>
270             </tr>
271             </thead>
272             HTML
273              
274 1         6 for my $row (@$csv) {
275 2 0 33     7 next if !$row && !$row->[2];
276 2         15 my ($name, $email) = $row->[2] =~ /^<([^>]+)>(.*)$/;
277 2 50       9 $row->[0] = $row->[0] eq $releases[-1]{name} ? $row->[0] : qq{<span class="old">$row->[0]</span>};
278 2 50       7 $row->[2] = $row->[0] eq $releases[-1]{name} ? $name : qq{<a href="mailto:$email?subject=$row->[1]%20is%20out%20of%20date">$name</a>};
279 2         32 print "<tr class=\"age_$row->[4]\"><td>" . (join $sepperator, @$row[0..3]), "</td></tr>\n";
280             }
281              
282 1         11 print "</table>\n";
283              
284 1         4 return;
285             }
286              
287             sub format_test {
288 2     2 1 7 my (undef, $csv, @releases) = @_;
289              
290 2         20 require Test::More;
291 2         28 Test::More->import();
292 2         546 for my $row (@$csv) {
293 0 0       0 is( $row->[0], $releases[-1]{name}, $row->[1] . ' is upto date')
294             or note("Release is $row->[3] days old");
295             }
296              
297 2         5 return;
298             }
299              
300             1;
301              
302             __DATA__
303              
304             =head1 NAME
305              
306             git-up-to-date - Check that git branches include latest production branch/tag
307              
308             =head1 VERSION
309              
310             This documentation refers to git-up-to-date version 1.1.16
311              
312             =head1 SYNOPSIS
313              
314             git-up-to-date [am-i] [option]
315             git-up-to-date show [option]
316             git-up-to-date current [option]
317             git-up-to-date update-me [option]
318              
319             SUB-COMMANDS
320             am-i (default) determine if the current branch is up-to-date
321             show Show's the status of all active branches (ie branches with
322             commits since last release)
323             current Show the current "production" branch or tag
324             update-me Merges in the latest release
325              
326             OPTIONS:
327             -t --tab[=]str Specify a tag that any branch with newer commits must contain
328             -b --branch[=]str Similarly a branch that other branches with newer commits must
329             contain (Default origin/master)
330             -l --local Shorthand for --branch '^master$'
331             -f --format[=](test|text|html|csv|tab|json)
332             Set the out put format
333             * test - TAP test formatted output (default)
334             * text - Simple formatted text
335             * html - A basic HTML page
336             * csv - Comma seperated values formatted output
337             * tab - Tab seperated values formatted output
338             -q --quick Print to STDERR the statuses as they are found (no formatting)
339             -i --include[=]regexp
340             Include only "neweer" branches that match this regexp
341             -e --exclude[=]regexp
342             Exclude any "neweer" branches that match this regexp
343             --all Show the status of all branches not just current ones.
344             -m --max-history[=]int
345             Set the maximum number of release branches/tags to go back
346             (if more than one) to find where a branch was created from.
347             (Default 1)
348              
349             -s --branch-status
350             Shows the status (name, last committer, last commit date) of
351             all branches.
352             -a --age-limit[=]date
353             With --branch-status limit to only branches created after
354             date (a YYYY-MM-DD formatted date)
355             -F --fetch Do a fetch before anything else.
356             -x --fix With am-i, merges in the current prod/release branch/tag
357              
358             -v --verbose Shows changed branches that are upto date.
359             --version Prints the version information
360             --help Prints this help information
361             --man Prints the full documentation for git-up-to-date
362              
363             =head1 DESCRIPTION
364              
365             The C<git up-to-date> command can tell you the status of "active" branches as
366             compared to a release tag or branch. It does this by finding all tags or
367             branches that match the regular expression passed to C<--tag> or C<--branch>,
368             sorts them alpha-numerically assuming that the largest is the most recent.
369              
370             eg release_1, release_1_1
371              
372             The branch release_1_1 would be considered the most recent. With the found
373             tag/branch the date/time it was created is used to find all branches that
374             have newer commits (unless C<--all> is used). These branches are then searched
375             to see if they contain the found release tag or branch (and if C<--max-history>
376             is specified and the branch doesn't contain the release branch or tag the older
377             releases are searched for).
378              
379             =head1 SUBROUTINES/METHODS
380              
381             =head2 C<run ()>
382              
383             Executes the git workflow command
384              
385             =head2 C<do_show ()>
386              
387             =head2 C<do_am_i ()>
388              
389             =head2 C<do_current ()>
390              
391             =head2 C<do_update_me ()>
392              
393             =head2 C<branches_contain ()>
394              
395             =head2 C<format_text ()>
396              
397             =head2 C<format_csv ()>
398              
399             =head2 C<format_json ()>
400              
401             =head2 C<format_html ()>
402              
403             =head2 C<format_test ()>
404              
405             =head2 C<format_tab ()>
406              
407             =head1 DIAGNOSTICS
408              
409             =head1 CONFIGURATION AND ENVIRONMENT
410              
411             Defaults for this script can be set through C<git config>
412              
413             workflow.prod Sets how a prod release is determined
414             eg the default equivalent is branch=^origin/master$
415             workflow.max-history
416             Sets the default C<--max-history> value
417              
418             You can set these values either by editing the repository local C<.git/config>
419             file or C<~/.gitconfig> or use the C<git config> command
420              
421             # eg Setting the global value
422             git config --global workflow.max-history 10
423              
424             # or set a repository's local value
425             git config workflow.prod 'tag=^release_\d{4}_\d{2}\d{2}$'
426              
427             =head1 DEPENDENCIES
428              
429             =head1 INCOMPATIBILITIES
430              
431             =head1 BUGS AND LIMITATIONS
432              
433             There are no known bugs in this module.
434              
435             Please report problems to Ivan Wills (ivan.wills@gmail.com).
436              
437             Patches are welcome.
438              
439             =head1 AUTHOR
440              
441             Ivan Wills - (ivan.wills@gmail.com)
442              
443             =head1 LICENSE AND COPYRIGHT
444              
445             Copyright (c) 2014 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW Australia 2077).
446             All rights reserved.
447              
448             This module is free software; you can redistribute it and/or modify it under
449             the same terms as Perl itself. See L<perlartistic>. This program is
450             distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
451             without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
452             PARTICULAR PURPOSE.
453              
454             =cut