File Coverage

blib/lib/Git/Wrapper.pm
Criterion Covered Total %
statement 182 202 90.1
branch 74 108 68.5
condition 16 24 66.6
subroutine 30 35 85.7
pod 17 17 100.0
total 319 386 82.6


line stmt bran cond sub pod time code
1 7     7   381602 use 5.006;
  7         61  
2 7     7   30 use strict;
  7         8  
  7         102  
3 7     7   23 use warnings;
  7         14  
  7         417  
4              
5             package Git::Wrapper;
6             #ABSTRACT: Wrap git(7) command-line interface
7             $Git::Wrapper::VERSION = '0.048_090'; # TRIAL
8              
9             $Git::Wrapper::VERSION = '0.048090';our $DEBUG=0;
10              
11             # Prevent ANSI color with extreme prejudice
12             # https://github.com/genehack/Git-Wrapper/issues/13
13             delete $ENV{GIT_PAGER_IN_USE};
14              
15 7     7   2526 use File::chdir;
  7         16414  
  7         561  
16 7     7   1657 use File::Temp;
  7         46742  
  7         313  
17 7     7   2659 use IPC::Run3 ();
  7         60271  
  7         153  
18 7     7   41 use Scalar::Util qw(blessed);
  7         10  
  7         246  
19 7     7   2489 use Sort::Versions;
  7         3743  
  7         527  
20 7     7   42 use Symbol;
  7         10  
  7         266  
21              
22 7     7   1949 use Git::Wrapper::Exception;
  7         10  
  7         152  
23 7     7   1893 use Git::Wrapper::File::RawModification;
  7         12  
  7         144  
24 7     7   1812 use Git::Wrapper::Log;
  7         9  
  7         143  
25 7     7   1892 use Git::Wrapper::Statuses;
  7         11  
  7         13218  
26              
27             sub new {
28 9     9 1 30255 my $class = shift;
29              
30             # three calling conventions
31             # 1: my $gw = Git::Wrapper->new( $dir )
32             # 2: my $gw = Git::Wrapper->new( $dir , %options )
33             # 3: my $gw = Git::Wrapper->new({ dir => $dir , %options });
34              
35 9         18 my $args;
36              
37 9 100       27 if ( scalar @_ == 1 ) {
38 7         9 my $arg = shift;
39 7 100       40 if ( ref $arg eq 'HASH' ) { $args = $arg }
  2 100       3  
    100          
40 2         6 elsif ( blessed $arg ) { $args = { dir => "$arg" } } # my objects, let me
41             # show you them.
42 1         3 elsif ( ! ref $arg ) { $args = { dir => $arg } }
43 2         17 else { die "Single arg must be hashref, scalar, or stringify-able object" }
44             }
45             else {
46 2         9 my( $dir , %opts ) = @_;
47 2 50       19 $dir = "$dir" if blessed $dir; # we can stringify it for you wholesale
48 2         17 $args = { dir => $dir , %opts }
49             }
50              
51 7         48 my $self = bless $args => $class;
52              
53 7 100       24 die "usage: $class->new(\$dir)" unless $self->dir;
54              
55 5         13 return $self;
56             }
57              
58             sub AUTOLOAD {
59 56     56   22814 my $self = shift;
60              
61 56         670 (my $meth = our $AUTOLOAD) =~ s/.+:://;
62 56 100       936 return if $meth eq 'DESTROY';
63              
64 49         100 $meth =~ tr/_/-/;
65              
66 49         401 return $self->RUN($meth, @_);
67             }
68              
69 0     0 1 0 sub ERR { shift->{err} }
70 0     0 1 0 sub OUT { shift->{out} }
71              
72             sub AUTOPRINT {
73 0     0 1 0 my $self = shift;
74              
75 0 0       0 $self->{autoprint} = shift if @_;
76              
77 0         0 return $self->{autoprint};
78             }
79              
80             sub RUN {
81 181     181 1 1887 my $self = shift;
82              
83 181         323 delete $self->{err};
84 181         285 delete $self->{out};
85              
86 181         349 my $cmd = shift;
87              
88 181         1173 my( $parts , $stdin ) = _parse_args( $cmd , @_ );
89              
90 181         511 my @cmd = ( $self->git , @$parts );
91 181         237 my( @out , @err );
92              
93             {
94 181 50       174 local $CWD = $self->dir unless $cmd eq 'clone';
  181         566  
95              
96 181 50       8251 print STDERR join(' ',@cmd),"\n" if $DEBUG;
97              
98 181         222 my ($stdout, $stderr);
99             # Prevent commands from running interactively
100 181         1487 local $ENV{GIT_EDITOR} = ' ';
101 181         713 IPC::Run3::run3(\@cmd, \$stdin, \$stdout, \$stderr);
102              
103             @out = map {
104 181         647585 chomp;
  2284         2658  
105 2284         3643 $_;
106             } split(/\n/, $stdout);
107              
108             @err = map {
109 181         7291 chomp;
  2052         2120  
110 2052         2648 $_;
111             } split(/\n/, $stderr);
112             };
113              
114 181 50       7465 print "status: $?\n" if $DEBUG;
115              
116             # In earlier gits (1.5, 1.6, I'm not sure when it changed), "git status"
117             # would exit 1 if there was nothing to commit, or in other cases. This is
118             # basically insane, and has been fixed, but if we don't require git 1.7, we
119             # should cope with it. -- rjbs, 2012-03-31
120 181   66     2392 my $stupid_status = $cmd eq 'status' && @out && ! @err;
121              
122 181 100 66     2052 if ($? && ! $stupid_status) {
123 1         32 die Git::Wrapper::Exception->new(
124             output => \@out,
125             error => \@err,
126             status => $? >> 8,
127             );
128             }
129              
130 180         1321 $self->{err} = \@err;
131 180         931 $self->{out} = \@out;
132              
133 180 50       477 if( $self->{autoprint} ) {
134 0         0 print $_, "\n" for @out;
135              
136 0         0 warn $_, "\n" for @err;
137             }
138              
139 180         3424 return @out;
140             }
141              
142             sub branch {
143 1     1 1 21 my $self = shift;
144              
145 1 50       13 my $opt = ref $_[0] eq 'HASH' ? shift : {};
146 1         13 $opt->{no_color} = 1;
147              
148 1         15 return $self->RUN(branch => $opt,@_);
149             }
150              
151 188     188 1 759 sub dir { shift->{dir} }
152              
153             sub git {
154 181     181 1 228 my $self = shift;
155              
156 181 100       348 return $self->{git_binary} if defined $self->{git_binary};
157              
158 179 50       515 return ( defined $ENV{GIT_WRAPPER_GIT} ) ? $ENV{GIT_WRAPPER_GIT} : 'git';
159             }
160              
161             sub has_git_in_path {
162 0     0 1 0 require IPC::Cmd;
163 0         0 IPC::Cmd::can_run('git');
164             }
165              
166             sub log {
167 12     12 1 1548 my $self = shift;
168              
169 12 100       101 if ( grep /format=/, @_ ) {
170 1         25 die Git::Wrapper::Exception->new(
171             error => [qw/--format not allowed. Use the RUN() method if you with to use a custom log format./],
172             output => undef,
173             status => 255 ,
174             );
175             }
176              
177 11 100       49 my $opt = ref $_[0] eq 'HASH' ? shift : {};
178 11         65 $opt->{no_color} = 1;
179 11         87 $opt->{pretty} = 'medium';
180 11         80 $opt->{no_abbrev} = 1; # https://github.com/genehack/Git-Wrapper/issues/67
181              
182 11 50       114 $opt->{no_abbrev_commit} = 1
183             if $self->supports_log_no_abbrev_commit;
184 11 50       832 $opt->{no_expand_tabs} = 1
185             if $self->supports_log_no_expand_tabs;
186              
187 11   66     815 my $raw = defined $opt->{raw} && $opt->{raw};
188              
189 11         79 my @out = $self->RUN(log => $opt, @_);
190              
191 11         27 my @logs;
192 11         40 while (my $line = shift @out) {
193 14 100       168 die "unhandled: $line" unless $line =~ /^commit (\S+)/;
194              
195 13         292 my $current = Git::Wrapper::Log->new($1);
196              
197 13         27 $line = shift @out; # next line;
198              
199 13         65 while ($line =~ /^(\S+):\s+(.+)$/) {
200 26         57 $current->attr->{lc $1} = $2;
201 26         91 $line = shift @out; # next line;
202             }
203              
204 13 50       24 die "no blank line separating head from message" if $line;
205              
206 13 50       95 my ( $initial_indent ) = $out[0] =~ /^(\s*)/ if @out;
207              
208 13         23 my $message = '';
209 13   100     183 while (
      100        
210             @out
211             and $out[0] !~ /^commit (\S+)/
212             and length($line = shift @out)
213             ) {
214 20         143 $line =~ s/^$initial_indent//; # strip just the indenting added by git
215 20         103 $message .= "$line\n";
216             }
217              
218 13         48 $current->message($message);
219              
220 13 100       27 if ($raw) {
221 3         13 my @modifications;
222              
223             # example outputs:
224             # regular:
225             # :000000 100644 0000000000000000000000000000000000000000 ce013625030ba8dba906f756967f9e9ca394464a A foo/bar
226             # with score value after file type (see https://github.com/genehack/Git-Wrapper/issues/70):
227             # :100644 100644 c659037... c659037... R100 foo bar
228 3   66     42 while(@out and $out[0] =~ m/^\:(\d{6}) (\d{6}) (\w{40}) (\w{40}) (\w{1}[0-9]*)\t(.*)$/) {
229 3         48 push @modifications, Git::Wrapper::File::RawModification->new($6,$5,$1,$2,$3,$4);
230 3         11 shift @out;
231             }
232 3 50       19 $current->modifications(@modifications) if @modifications;
233             }
234              
235 13         34 push @logs, $current;
236              
237 13 100       36 last unless @out; # handle running out of log
238 3 50       22 shift @out unless $out[0] =~ /^commit/; # blank line at end of entry, except merge commits;
239             }
240              
241 10         81 return @logs;
242             }
243              
244             my %STATUS_CONFLICTS = map { $_ => 1 } qw
;
245              
246             sub status {
247 39     39 1 24285 my $self = shift;
248              
249 39 50       201 return $self->RUN('status' , @_ )
250             unless $self->supports_status_porcelain;
251              
252 39 50       129 my $opt = ref $_[0] eq 'HASH' ? shift : {};
253 39         308 $opt->{$_} = 1 for qw;
254              
255 39         143 my @out = $self->RUN(status => $opt, @_);
256              
257 39         608 my $statuses = Git::Wrapper::Statuses->new;
258              
259 39 100       280 return $statuses if !@out;
260              
261 26         64 for (@out) {
262 31         431 my ($x, $y, $from, $to) = $_ =~ /\A(.)(.) (.*?)(?: -> (.*))?\z/;
263              
264 31 50 66     296 if ($STATUS_CONFLICTS{"$x$y"}) {
    100          
265 0         0 $statuses->add('conflict', "$x$y", $from, $to);
266             }
267             elsif ($x eq '?' && $y eq '?') {
268 10         47 $statuses->add('unknown', '?', $from, $to);
269             }
270             else {
271 21 100       100 $statuses->add('changed', $y, $from, $to)
272             if $y ne ' ';
273 21 100       142 $statuses->add('indexed', $x, $from, $to)
274             if $x ne ' ';
275             }
276             }
277 26         409 return $statuses;
278             }
279              
280             sub supports_hash_object_filters {
281 1     1 1 906 my $self = shift;
282              
283             # The '--no-filters' option to 'git-hash-object' was added in version 1.6.1
284 1 50       11 return 0 if ( versioncmp( $self->version , '1.6.1' ) == -1 );
285 1         64 return 1;
286             }
287              
288             sub supports_log_no_abbrev_commit {
289 12     12 1 805 my $self = shift;
290              
291             # The '--no-abbrev-commit' option to 'git log' was added in version 1.7.6
292 12 50       33 return ( versioncmp( $self->version , '1.7.6' ) == -1 ) ? 0 : 1;
293             }
294              
295             sub supports_log_no_expand_tabs {
296 11     11 1 51 my $self = shift;
297              
298             # The '--no-expand-tabs' option to git log was added in version 2.9.0
299 11 50       34 return 0 if ( versioncmp( $self->version , '2.9' ) == -1 );
300 0         0 return 1;
301             }
302              
303             sub supports_log_raw_dates {
304 2     2 1 1404 my $self = shift;
305              
306             # The '--date=raw' option to 'git log' was added in version 1.6.2
307 2 50       15 return 0 if ( versioncmp( $self->version , '1.6.2' ) == -1 );
308 2         140 return 1;
309             }
310              
311             sub supports_status_porcelain {
312 41     41 1 104 my $self = shift;
313              
314             # The '--porcelain' option to git status was added in version 1.7.0
315 41 50       99 return 0 if ( versioncmp( $self->version , '1.7' ) == -1 );
316 41         2339 return 1;
317             }
318              
319             sub version {
320 76     76 1 2652 my $self = shift;
321              
322 76         169 my ($version) = $self->RUN('version');
323              
324 76         808 $version =~ s/^git version //;
325              
326 76         993 return $version;
327             }
328              
329             sub _message_tempfile {
330 0     0   0 my ( $message ) = @_;
331              
332 0         0 my $tmp = File::Temp->new( UNLINK => 0 );
333 0         0 $tmp->print( $message );
334              
335 0         0 return ( "file", '"'.$tmp->filename.'"' );
336             }
337              
338             sub _opt_and_val {
339 137     137   338 my( $name , $val ) = @_;
340              
341 137         247 $name =~ tr/_/-/;
342 137 100       367 my $opt = length($name) == 1
343             ? "-$name"
344             : "--$name"
345             ;
346              
347             return
348 137 100       564 $val eq '1' ? ($opt)
    100          
349             : length($name) == 1 ? ($opt, $val)
350             : "$opt=$val";
351             }
352              
353             sub _parse_args {
354 194     194   8055 my $cmd = shift;
355 194 50       319 die "initial argument must not be a reference\n"
356             if ref $cmd;
357              
358 194         245 my( $stdin , @pre_cmd , @post_cmd );
359              
360 194         395 foreach ( @_ ) {
361 159 100       635 if ( ref $_ eq 'HASH' ) {
    100          
    50          
362             $stdin = delete $_->{-STDIN}
363 85 100       364 if exists $_->{-STDIN};
364              
365 85         644 for my $name ( sort keys %$_ ) {
366 137         329 my $val = delete $_->{$name};
367 137 50       294 next if $val eq '0';
368              
369 137 100       330 if ( $name =~ s/^-// ) {
370 10         16 push @pre_cmd , _opt_and_val( $name , $val );
371             }
372             else {
373 127 50       299 ( $name, $val ) = _message_tempfile( $val )
374             if _win32_multiline_commit_msg( $cmd, $name, $val );
375              
376 127         383 push @post_cmd , _opt_and_val( $name , $val );
377             }
378             }
379             }
380             elsif ( blessed $_ ) {
381 2         14 push @post_cmd , "$_"; # here be anteaters
382             }
383             elsif ( ref $_ ) {
384 0         0 die "Git::Wrapper command arguments must be plain scalars, hashrefs, "
385             . "or stringify-able objects.\n";
386             }
387 72         194 else { push @post_cmd , $_; }
388             }
389              
390 194         629 return( [ @pre_cmd , $cmd , @post_cmd ] , $stdin );
391             }
392              
393             sub _win32_multiline_commit_msg {
394 127     127   325 my ( $cmd, $name, $val ) = @_;
395              
396 127 50       615 return 0 if $^O ne "MSWin32";
397 0 0         return 0 if $cmd ne "commit";
398 0 0 0       return 0 if $name ne "m" and $name ne "message";
399 0 0         return 0 if $val !~ /\n/;
400              
401 0           return 1;
402             }
403              
404             __END__