File Coverage

blib/lib/App/Git/Workflow.pm
Criterion Covered Total %
statement 163 164 99.3
branch 53 62 85.4
condition 16 20 80.0
subroutine 28 28 100.0
pod 15 15 100.0
total 275 289 95.1


line stmt bran cond sub pod time code
1             package App::Git::Workflow;
2              
3             # Created on: 2014-03-11 22:09:32
4             # Create by: Ivan Wills
5             # $Id$
6             # $Revision$, $HeadURL$, $Date$
7             # $Revision$, $Source$, $Date$
8              
9 23     23   230185 use strict;
  23         64  
  23         683  
10 23     23   108 use warnings;
  23         42  
  23         577  
11 23     23   1384 use version;
  23         5483  
  23         135  
12 23     23   11933 use autodie;
  23         310951  
  23         115  
13 23     23   145895 use Carp qw/carp croak cluck confess longmess/;
  23         56  
  23         1991  
14 23     23   10451 use Data::Dumper qw/Dumper/;
  23         92427  
  23         1513  
15 23     23   2494 use English qw/ -no_match_vars /;
  23         6777  
  23         180  
16 23     23   16771 use App::Git::Workflow::Repository qw//;
  23         71  
  23         608  
17 23     23   197 use base qw/Exporter/;
  23         47  
  23         2833  
18              
19             our $VERSION = version->new(1.1.16);
20              
21             sub _alphanum_sort {
22 23     23   153 no warnings qw/once/;
  23         45  
  23         54519  
23 177     177   339 my $A = $a;
24 177         404 $A =~ s/(\d+)/sprintf "%014i", $1/egxms;
  95         333  
25 177         271 my $B = $b;
26 177         370 $B =~ s/(\d+)/sprintf "%014i", $1/egxms;
  125         451  
27              
28 177         642 return $A cmp $B;
29             }
30              
31             sub new {
32 140     140 1 469 my $class = shift;
33 140         447 my %param = @_;
34 140         309 my $self = \%param;
35              
36 140         282 bless $self, $class;
37              
38 140         561 $self->{TEST} = 0;
39 140         290 $self->{VERBOSE} = 0;
40 140         372 $self->{GIT_DIR} = '.git';
41 140         308 $self->{branches} = undef;
42 140         263 $self->{tags} = undef;
43 140   100     782 $self->{settings_dir} = ($ENV{HOME} || "/tmp/") . '/.git-workflow';
44 140 100       3492 mkdir $self->{settings_dir} if !-d $self->{settings_dir};
45              
46 140         4779 return $self;
47             }
48              
49 493 50   493 1 3563 sub git { $_[0]->{git} || ($_[0]->{git} = App::Git::Workflow::Repository->git); }
50              
51             sub branches {
52 121     121 1 341 my ($self, $type, $contains) = @_;
53 121   100     298 $type ||= 'local';
54             my @options
55 121 100       718 = $type eq 'local' ? ()
    100          
    100          
56             : $type eq 'remote' ? ('-r')
57             : $type eq 'both' ? ('-a')
58             : confess "Unknown type '$type'!\n";
59              
60 120 100       252 if ($contains) {
61 46         92 push @options, "--contains", $contains;
62 46         90 $type .= $contains;
63             }
64              
65             # assign to or cache
66             $self->{branches}{$type} = [
67             sort _alphanum_sort
68 222         1399 map { /^[*]?\s+(?:remotes\/)?(.*?)\s*$/xms }
69 222         601 grep {!/HEAD/}
70             $self->git->branch(@options)
71 120 100       462 ] if !$self->{branches}{$type};
72              
73 119         237 return @{ $self->{branches}{$type} };
  119         397  
74             }
75              
76             sub tags {
77 4     4 1 42 my ($self) = @_;
78             # assign to or cache
79             $self->{tags} = [
80             sort _alphanum_sort
81             #map { /^(.*?)\s*$/xms }
82             $self->git->tag
83 4 50 66     36 ] if !$self->{tags} || !@{ $self->{tags} };
  2         9  
84              
85 4         10 return @{ $self->{tags} };
  4         17  
86             }
87              
88             sub current {
89 9     9 1 22 my ($self) = @_;
90             # get the git directory
91 9         27 my $git_dir = $self->git->rev_parse("--show-toplevel");
92 9         20 chomp $git_dir;
93              
94             # read the HEAD file to find what branch or id we are on
95 9         56 open my $fh, '<', "$git_dir/$self->{GIT_DIR}/HEAD";
96 9         9463 my $head = <$fh>;
97 9         70 close $fh;
98 9         3330 chomp $head;
99              
100 9 100       43 if ($head =~ m{ref: refs/heads/(.*)$}) {
101 1         9 return ('branch', $1);
102             }
103              
104             # try to identify the commit as it's not a local branch
105 8         59 open $fh, '<', "$git_dir/$self->{GIT_DIR}/FETCH_HEAD";
106 8         942 while (my $line = <$fh>) {
107 10 100       136 next if $line !~ /^$head/;
108              
109 7         96 my ($type, $name, $remote) = $line =~ /(tag|branch) \s+ '([^']+)' \s+ of \s+ (.*?) $/xms;
110             # TODO calculate the remote's alias rather than assume that it is "origin"
111 7 50       180 return ($type, $type eq 'branch' ? "origin/$name" : $name);
112             }
113              
114             # not on a branch or commit
115 1         18 return ('sha', $head);
116             }
117              
118             sub config {
119 88     88 1 218 my ($self, $name, $default) = @_;
120 88         217 my $value = $self->git->config($name);
121              
122 88   66     572 return $value || $default;
123             }
124              
125             sub match_commits {
126 24     24 1 86 my ($self, $type, $regex, %option) = @_;
127 24   100     107 $option{max_history} ||= 1;
128 24 100       72 $option{branches} = defined $option{branches} ? $option{branches} : 1;
129 24 100       110 my @commits = grep {/$regex/} $type eq 'tag' ? $self->tags() : $self->branches('both');
  60         297  
130              
131 24 100       107 my $oldest = @commits > $option{max_history} ? -$option{max_history} : -scalar @commits;
132 24         80 return map { $self->commit_details($_, branches => $option{branches}) } @commits[ $oldest .. -1 ];
  24         88  
133             }
134              
135             sub release {
136 10     10 1 23 my ($self, $tag_or_branch, $local, $search) = @_;
137             my @things
138 10 100       43 = $tag_or_branch eq 'branch'
    100          
139             ? $self->branches($local ? 'local' : 'remote')
140             : $self->tags();
141 10         32 my ($release) = reverse grep {/$search/} @things;
  24         118  
142 10         19 chomp $release;
143              
144 10         25 return $release;
145             }
146              
147             sub releases {
148 24     24 1 94 my ($self, %option) = @_;
149 24         61 my ($type, $regex);
150 24 100       105 if ($option{tag}) {
    100          
151 1         4 $type = 'tag';
152 1         3 $regex = $option{tag};
153             }
154             elsif ($option{branch}) {
155 1         2 $type = 'branch';
156 1         3 $regex = $option{branch};
157             }
158             else {
159 22   66     60 my $prod = $self->config('workflow.prod') || ( $option{local} ? 'branch=^master$' : 'branch=^origin/master$' );
160 22         122 ($type, $regex) = split /\s*=\s*/, $prod;
161 22 100       68 if ( !$regex ) {
162 1         2 $type = 'branch';
163 1         3 $regex = '^origin/master$';
164             }
165             }
166              
167 24         100 my @releases = $self->match_commits($type, $regex, %option);
168 24 50       84 die "Could not find any historic releases for $type /$regex/!\n" if !@releases;
169 24         90 return @releases;
170             }
171              
172             sub commit_details {
173 77     77 1 293 my ($self, $name, %options) = @_;
174 77         162 my $split = "\1";
175 77 100       311 my $fmt = $options{user} ? "%ct$split%H$split%an$split%ae$split" : "%ct$split%H$split$split$split";
176             my ($time, $sha, $user, $email, $files)
177             = split /$split/, $self->git->log(
178             "--format=format:$fmt",
179             -1,
180 77 100       325 ($options{files} ? '--name-only' : ()),
181             $name
182             );
183              
184             return {
185             name => $name,
186             sha => $sha,
187             time => $time,
188             user => $user,
189             email => $email,
190 22         75 files => { map {$_ => 1} grep {$_} split "\n", $files || '' },
  22         40  
191 74 100 100     738 branches => $options{branches} ? { map { $_ => 1 } $self->branches('both', $sha) } : {},
  56         326  
192             };
193             }
194              
195             sub files_from_sha {
196 1     1 1 4 my ($self, $sha) = @_;
197 1         4 my $show = $self->git->show('--name-status', $sha);
198 1         7 $show =~ s/\A.*\n\n//xms;
199 1         3 my %files;
200 1         4 for my $file (split /\n/, $show) {
201 2         9 my ($state, $file) = split /\s+/, $file, 2;
202 2         6 $files{$file} = $state;
203             }
204              
205 1         4 return \%files;
206             }
207              
208             sub slurp {
209 2     2 1 7 my ($self, $file) = @_;
210 2         13 open my $fh, '<', $file;
211              
212 2 100       3093 return wantarray ? <$fh> : do { local $/; <$fh> };
  1         5  
  1         65  
213             }
214              
215             sub spew {
216 11     11 1 1100 my ($self, $file, @out) = @_;
217 11 50       30 die "No file passed!" if !$file;
218 11         40 open my $fh, '>', $file;
219              
220 11         6620 print $fh @out;
221             }
222              
223             sub settings {
224 4     4 1 9 my ($self) = @_;
225 4 50       13 return $self->{settings} if $self->{settings};
226              
227 4         9 my $key = $self->git->config('remote.origin.url');
228 4 50       12 chomp $key if $key;
229 4 50       17 if ( !$key ) {
230 4         12 $key = $self->git->rev_parse("--show-toplevel");
231 4         8 chomp $key;
232             }
233 4         17 $key = $self->_url_encode($key);
234              
235 4         16 $self->{settings_file} = "$self->{settings_dir}/$key";
236              
237             $self->{settings}
238             = -f $self->{settings_file}
239             ? do $self->{settings_file}
240 4 100       536 : {};
241              
242 4 50 66     54 if ( $self->{settings}->{version} && version->new($self->{settings}->{version}) > $App::Git::Workflow::VERSION ) {
243 0         0 die "Current settings created with newer version than this program!\n";
244             }
245              
246 4         16 return $self->{settings};
247             }
248              
249             sub save_settings {
250 107     107 1 255 my ($self) = @_;
251 107 100       690 return if !$self->{settings_file};
252 10         23 local $Data::Dumper::Indent = 1;
253 10         18 local $Data::Dumper::Sortkeys = 1;
254 10         20 $self->{settings}->{version} = $App::Git::Workflow::VERSION;
255 10         20 $self->{settings}->{date} = time;
256 10         33 $self->spew($self->{settings_file}, 'my ' . Dumper $self->{settings});
257             }
258              
259             sub _url_encode {
260 6     6   16 my ($self, $url) = @_;
261 6         30 $url =~ s/([^-\w.:])/sprintf "%%%x", ord $1/egxms;
  5         44  
262 6         23 return $url;
263             }
264              
265             sub DESTROY {
266 100     100   269 my ($self) = @_;
267 100         429 $self->save_settings();
268             }
269              
270             1;
271              
272             __END__
273              
274             =head1 NAME
275              
276             App::Git::Workflow - Git workflow tools
277              
278             =head1 VERSION
279              
280             This documentation refers to App::Git::Workflow version 1.1.16
281              
282             =head1 SYNOPSIS
283              
284             use App::Git::Workflow qw/branches tags/;
285              
286             # Get all local branches
287             my @branches = $self->branches();
288             # or
289             @branches = $self->branches('local');
290              
291             # remote branches
292             @branches = $self->branches('remote');
293              
294             # both remote and local branches
295             @branches = $self->branches('both');
296              
297             # similarly for tags
298             my @tags = $self->tags();
299              
300             =head1 DESCRIPTION
301              
302             This module contains helper functions for the command line scripts.
303              
304             =head1 SUBROUTINES/METHODS
305              
306             =head2 C<new (%params)>
307              
308             Create a new C<App::Git::Workflow::Pom> object
309              
310             =head2 C<git ()>
311              
312             Get the git repository object
313              
314             =head2 C<branches ([ $type ])>
315              
316             Param: C<$type> - one of local, remote or both
317              
318             Returns a list of all branches of the specified type. (Default type is local)
319              
320             =head2 C<tags ()>
321              
322             Returns a list of all tags.
323              
324             =head2 C<_alphanum_sort ()>
325              
326             Does sorting (for the building C<sort>) in a alpha numerical fashion.
327             Specifically all numbers are converted for the comparison to 14 digit strings
328             with leading zeros.
329              
330             =head2 C<children ($dir)>
331              
332             Get the child files of C<$dir>
333              
334             =head2 C<config ($name, $default)>
335              
336             Get the git config value of C<$name>, or if not set C<$default>
337              
338             =head2 C<current ()>
339              
340             Get the current branch/tag or commit
341              
342             =head2 C<match_commits ($type, $regex, $max)>
343              
344             =head2 C<release ($tag_or_branch, $local, $search)>
345              
346             =head2 C<releases (%option)>
347              
348             =head2 C<commit_details ($name)>
349              
350             Get info from C<git show $name>
351              
352             =head2 C<files_from_sha ($sha)>
353              
354             Get the files changed by the commit
355              
356             =head2 C<slurp ($file)>
357              
358             Return the contents of C<$file>
359              
360             =head2 C<spew ( $file, @data )>
361              
362             Write C<@data> to the file C<$file>
363              
364             =head2 C<settings ()>
365              
366             Get the saved settings for the current repository
367              
368             =head2 C<save_settings ()>
369              
370             Save any changed settings for the current repository
371              
372             =head1 DIAGNOSTICS
373              
374             =head1 CONFIGURATION AND ENVIRONMENT
375              
376             =head1 DEPENDENCIES
377              
378             =head1 INCOMPATIBILITIES
379              
380             =head1 BUGS AND LIMITATIONS
381              
382             There are no known bugs in this module.
383              
384             Please report problems to Ivan Wills (ivan.wills@gmail.com).
385              
386             Patches are welcome.
387              
388             =head1 AUTHOR
389              
390             Ivan Wills - (ivan.wills@gmail.com)
391              
392             =head1 LICENSE AND COPYRIGHT
393              
394             Copyright (c) 2014 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW Australia 2077).
395             All rights reserved.
396              
397             This module is free software; you can redistribute it and/or modify it under
398             the same terms as Perl itself. See L<perlartistic>. This program is
399             distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
400             without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
401             PARTICULAR PURPOSE.
402              
403             =cut