File Coverage

blib/lib/Git/Wrapper.pm
Criterion Covered Total %
statement 183 211 86.7
branch 74 112 66.0
condition 16 27 59.2
subroutine 30 35 85.7
pod 17 17 100.0
total 320 402 79.6


line stmt bran cond sub pod time code
1 5     5   366767 use 5.006;
  5         68  
2 5     5   35 use strict;
  5         12  
  5         149  
3 5     5   33 use warnings;
  5         12  
  5         485  
4              
5             package Git::Wrapper;
6             #ABSTRACT: Wrap git(7) command-line interface
7             $Git::Wrapper::VERSION = '0.047_090'; # TRIAL
8              
9             $Git::Wrapper::VERSION = '0.047090';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 5     5   2567 use File::chdir;
  5         17720  
  5         646  
16 5     5   1658 use File::Temp;
  5         48756  
  5         419  
17 5     5   2663 use IPC::Open3 qw();
  5         16146  
  5         163  
18 5     5   46 use Scalar::Util qw(blessed);
  5         13  
  5         300  
19 5     5   2671 use Sort::Versions;
  5         4355  
  5         645  
20 5     5   64 use Symbol;
  5         12  
  5         347  
21              
22 5     5   1961 use Git::Wrapper::Exception;
  5         17  
  5         203  
23 5     5   2070 use Git::Wrapper::File::RawModification;
  5         14  
  5         170  
24 5     5   1743 use Git::Wrapper::Log;
  5         13  
  5         182  
25 5     5   1798 use Git::Wrapper::Statuses;
  5         16  
  5         16403  
26              
27             sub new {
28 7     7 1 31341 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 7         17 my $args;
36              
37 7 100       31 if ( scalar @_ == 1 ) {
38 6         13 my $arg = shift;
39 6 100       43 if ( ref $arg eq 'HASH' ) { $args = $arg }
  2 100       4  
    100          
40 1         9 elsif ( blessed $arg ) { $args = { dir => "$arg" } } # my objects, let me
41             # show you them.
42 1         6 elsif ( ! ref $arg ) { $args = { dir => $arg } }
43 2         78 else { die "Single arg must be hashref, scalar, or stringify-able object" }
44             }
45             else {
46 1         6 my( $dir , %opts ) = @_;
47 1 50       14 $dir = "$dir" if blessed $dir; # we can stringify it for you wholesale
48 1         7 $args = { dir => $dir , %opts }
49             }
50              
51 5         80 my $self = bless $args => $class;
52              
53 5 100       25 die "usage: $class->new(\$dir)" unless $self->dir;
54              
55 3         13 return $self;
56             }
57              
58             sub AUTOLOAD {
59 44     44   31696 my $self = shift;
60              
61 44         960 (my $meth = our $AUTOLOAD) =~ s/.+:://;
62 44 100       991 return if $meth eq 'DESTROY';
63              
64 39         190 $meth =~ tr/_/-/;
65              
66 39         223 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 94     94 1 3272 my $self = shift;
82              
83 94         332 delete $self->{err};
84 94         283 delete $self->{out};
85              
86 94         263 my $cmd = shift;
87              
88 94         940 my( $parts , $stdin ) = _parse_args( $cmd , @_ );
89              
90 94         595 my @cmd = ( $self->git , @$parts );
91              
92 94         266 my( @out , @err );
93              
94             {
95 94 50       225 local $CWD = $self->dir unless $cmd eq 'clone';
  94         616  
96              
97 94         7326 my ($wtr, $rdr, $err);
98              
99 94         399 local *TEMP;
100 94 50 33     1299 if ($^O eq 'MSWin32' && defined $stdin) {
101 0         0 my $file = File::Temp->new;
102 0         0 $file->autoflush(1);
103 0         0 $file->print($stdin);
104 0         0 $file->seek(0,0);
105 0         0 open TEMP, '<&=', $file;
106 0         0 $wtr = '<&TEMP';
107 0         0 undef $stdin;
108             }
109              
110 94         1224 $err = Symbol::gensym;
111              
112 94 50       3392 print STDERR join(' ',@cmd),"\n" if $DEBUG;
113              
114             # Prevent commands from running interactively
115 94         1048 local $ENV{GIT_EDITOR} = ' ';
116              
117 94         628 my $pid = IPC::Open3::open3($wtr, $rdr, $err, @cmd);
118 94 100       462345 print $wtr $stdin
119             if defined $stdin;
120              
121 94         1327 close $wtr;
122 94         1395463 chomp(@out = <$rdr>);
123 94         2539 chomp(@err = <$err>);
124              
125 94         12432 waitpid $pid, 0;
126             };
127              
128 94 50       8122 print "status: $?\n" if $DEBUG;
129              
130             # In earlier gits (1.5, 1.6, I'm not sure when it changed), "git status"
131             # would exit 1 if there was nothing to commit, or in other cases. This is
132             # basically insane, and has been fixed, but if we don't require git 1.7, we
133             # should cope with it. -- rjbs, 2012-03-31
134 94   66     969 my $stupid_status = $cmd eq 'status' && @out && ! @err;
135              
136 94 100 66     1820 if ($? && ! $stupid_status) {
137 1         42 die Git::Wrapper::Exception->new(
138             output => \@out,
139             error => \@err,
140             status => $? >> 8,
141             );
142             }
143              
144 93         1431 $self->{err} = \@err;
145 93         1029 $self->{out} = \@out;
146              
147 93 50       547 if( $self->{autoprint} ) {
148 0         0 print $_, "\n" for @out;
149              
150 0         0 warn $_, "\n" for @err;
151             }
152              
153 93         3217 return @out;
154             }
155              
156             sub branch {
157 1     1 1 44 my $self = shift;
158              
159 1 50       21 my $opt = ref $_[0] eq 'HASH' ? shift : {};
160 1         18 $opt->{no_color} = 1;
161              
162 1         21 return $self->RUN(branch => $opt,@_);
163             }
164              
165 99     99 1 796 sub dir { shift->{dir} }
166              
167             sub git {
168 94     94 1 260 my $self = shift;
169              
170 94 100       401 return $self->{git_binary} if defined $self->{git_binary};
171              
172 93 50       610 return ( defined $ENV{GIT_WRAPPER_GIT} ) ? $ENV{GIT_WRAPPER_GIT} : 'git';
173             }
174              
175             sub has_git_in_path {
176 0     0 1 0 require IPC::Cmd;
177 0         0 IPC::Cmd::can_run('git');
178             }
179              
180             sub log {
181 12     12 1 2630 my $self = shift;
182              
183 12 100       148 if ( grep /format=/, @_ ) {
184 1         33 die Git::Wrapper::Exception->new(
185             error => [qw/--format not allowed. Use the RUN() method if you with to use a custom log format./],
186             output => undef,
187             status => 255 ,
188             );
189             }
190              
191 11 100       134 my $opt = ref $_[0] eq 'HASH' ? shift : {};
192 11         110 $opt->{no_color} = 1;
193 11         131 $opt->{pretty} = 'medium';
194 11         52 $opt->{no_abbrev} = 1; # https://github.com/genehack/Git-Wrapper/issues/67
195              
196 11 50       141 $opt->{no_abbrev_commit} = 1
197             if $self->supports_log_no_abbrev_commit;
198 11 50       1290 $opt->{no_expand_tabs} = 1
199             if $self->supports_log_no_expand_tabs;
200              
201 11   66     1726 my $raw = defined $opt->{raw} && $opt->{raw};
202              
203 11         95 my @out = $self->RUN(log => $opt, @_);
204              
205 11         75 my @logs;
206 11         74 while (my $line = shift @out) {
207 14 100       261 die "unhandled: $line" unless $line =~ /^commit (\S+)/;
208              
209 13         404 my $current = Git::Wrapper::Log->new($1);
210              
211 13         64 $line = shift @out; # next line;
212              
213 13         117 while ($line =~ /^(\S+):\s+(.+)$/) {
214 26         127 $current->attr->{lc $1} = $2;
215 26         134 $line = shift @out; # next line;
216             }
217              
218 13 50       45 die "no blank line separating head from message" if $line;
219              
220 13 50       170 my ( $initial_indent ) = $out[0] =~ /^(\s*)/ if @out;
221              
222 13         74 my $message = '';
223 13   100     213 while (
      100        
224             @out
225             and $out[0] !~ /^commit (\S+)/
226             and length($line = shift @out)
227             ) {
228 20         197 $line =~ s/^$initial_indent//; # strip just the indenting added by git
229 20         130 $message .= "$line\n";
230             }
231              
232 13         130 $current->message($message);
233              
234 13 100       47 if ($raw) {
235 3         22 my @modifications;
236              
237             # example outputs:
238             # regular:
239             # :000000 100644 0000000000000000000000000000000000000000 ce013625030ba8dba906f756967f9e9ca394464a A foo/bar
240             # with score value after file type (see https://github.com/genehack/Git-Wrapper/issues/70):
241             # :100644 100644 c659037... c659037... R100 foo bar
242 3   66     71 while(@out and $out[0] =~ m/^\:(\d{6}) (\d{6}) (\w{40}) (\w{40}) (\w{1}[0-9]*)\t(.*)$/) {
243 3         85 push @modifications, Git::Wrapper::File::RawModification->new($6,$5,$1,$2,$3,$4);
244 3         18 shift @out;
245             }
246 3 50       21 $current->modifications(@modifications) if @modifications;
247             }
248              
249 13         33 push @logs, $current;
250              
251 13 100       43 last unless @out; # handle running out of log
252 3 50       31 shift @out unless $out[0] =~ /^commit/; # blank line at end of entry, except merge commits;
253             }
254              
255 10         114 return @logs;
256             }
257              
258             my %STATUS_CONFLICTS = map { $_ => 1 } qw
;
259              
260             sub status {
261 2     2 1 34 my $self = shift;
262              
263 2 50       19 return $self->RUN('status' , @_ )
264             unless $self->supports_status_porcelain;
265              
266 2 50       42 my $opt = ref $_[0] eq 'HASH' ? shift : {};
267 2         39 $opt->{$_} = 1 for qw;
268              
269 2         34 my @out = $self->RUN(status => $opt, @_);
270              
271 2         87 my $statuses = Git::Wrapper::Statuses->new;
272              
273 2 100       67 return $statuses if !@out;
274              
275 1         6 for (@out) {
276 1         26 my ($x, $y, $from, $to) = $_ =~ /\A(.)(.) (.*?)(?: -> (.*))?\z/;
277              
278 1 50 33     22 if ($STATUS_CONFLICTS{"$x$y"}) {
    50          
279 0         0 $statuses->add('conflict', "$x$y", $from, $to);
280             }
281             elsif ($x eq '?' && $y eq '?') {
282 0         0 $statuses->add('unknown', '?', $from, $to);
283             }
284             else {
285 1 50       11 $statuses->add('changed', $y, $from, $to)
286             if $y ne ' ';
287 1 50       19 $statuses->add('indexed', $x, $from, $to)
288             if $x ne ' ';
289             }
290             }
291 1         19 return $statuses;
292             }
293              
294             sub supports_hash_object_filters {
295 1     1 1 2031 my $self = shift;
296              
297             # The '--no-filters' option to 'git-hash-object' was added in version 1.6.1
298 1 50       16 return 0 if ( versioncmp( $self->version , '1.6.1' ) == -1 );
299 1         340 return 1;
300             }
301              
302             sub supports_log_no_abbrev_commit {
303 12     12 1 1823 my $self = shift;
304              
305             # The '--no-abbrev-commit' option to 'git log' was added in version 1.7.6
306 12 50       119 return ( versioncmp( $self->version , '1.7.6' ) == -1 ) ? 0 : 1;
307             }
308              
309             sub supports_log_no_expand_tabs {
310 11     11 1 81 my $self = shift;
311              
312             # The '--no-expand-tabs' option to git log was added in version 2.9.0
313 11 50       82 return 0 if ( versioncmp( $self->version , '2.9' ) == -1 );
314 0         0 return 1;
315             }
316              
317             sub supports_log_raw_dates {
318 2     2 1 2803 my $self = shift;
319              
320             # The '--date=raw' option to 'git log' was added in version 1.6.2
321 2 50       58 return 0 if ( versioncmp( $self->version , '1.6.2' ) == -1 );
322 2         222 return 1;
323             }
324              
325             sub supports_status_porcelain {
326 4     4 1 59 my $self = shift;
327              
328             # The '--porcelain' option to git status was added in version 1.7.0
329 4 50       49 return 0 if ( versioncmp( $self->version , '1.7' ) == -1 );
330 4         544 return 1;
331             }
332              
333             sub version {
334 37     37 1 4565 my $self = shift;
335              
336 37         205 my ($version) = $self->RUN('version');
337              
338 37         737 $version =~ s/^git version //;
339              
340 37         732 return $version;
341             }
342              
343             sub _message_tempfile {
344 0     0   0 my ( $message ) = @_;
345              
346 0         0 my $tmp = File::Temp->new( UNLINK => 0 );
347 0         0 $tmp->print( $message );
348              
349 0         0 return ( "file", '"'.$tmp->filename.'"' );
350             }
351              
352             sub _opt_and_val {
353 98     98   317 my( $name , $val ) = @_;
354              
355 98         244 $name =~ tr/_/-/;
356 98 100       358 my $opt = length($name) == 1
357             ? "-$name"
358             : "--$name"
359             ;
360              
361             return
362 98 100       537 $val eq '1' ? ($opt)
    100          
363             : length($name) == 1 ? ($opt, $val)
364             : "$opt=$val";
365             }
366              
367             sub _parse_args {
368 107     107   10042 my $cmd = shift;
369 107 50       378 die "initial argument must not be a reference\n"
370             if ref $cmd;
371              
372 107         284 my( $stdin , @pre_cmd , @post_cmd );
373              
374 107         389 foreach ( @_ ) {
375 106 100       699 if ( ref $_ eq 'HASH' ) {
    100          
    50          
376             $stdin = delete $_->{-STDIN}
377 46 100       215 if exists $_->{-STDIN};
378              
379 46         470 for my $name ( sort keys %$_ ) {
380 98         305 my $val = delete $_->{$name};
381 98 50       473 next if $val eq '0';
382              
383 98 100       414 if ( $name =~ s/^-// ) {
384 10         16 push @pre_cmd , _opt_and_val( $name , $val );
385             }
386             else {
387 88 50       264 ( $name, $val ) = _message_tempfile( $val )
388             if _win32_multiline_commit_msg( $cmd, $name, $val );
389              
390 88         321 push @post_cmd , _opt_and_val( $name , $val );
391             }
392             }
393             }
394             elsif ( blessed $_ ) {
395 2         25 push @post_cmd , "$_"; # here be anteaters
396             }
397             elsif ( ref $_ ) {
398 0         0 die "Git::Wrapper command arguments must be plain scalars, hashrefs, "
399             . "or stringify-able objects.\n";
400             }
401 58         277 else { push @post_cmd , $_; }
402             }
403              
404 107         811 return( [ @pre_cmd , $cmd , @post_cmd ] , $stdin );
405             }
406              
407             sub _win32_multiline_commit_msg {
408 88     88   282 my ( $cmd, $name, $val ) = @_;
409              
410 88 50       607 return 0 if $^O ne "MSWin32";
411 0 0         return 0 if $cmd ne "commit";
412 0 0 0       return 0 if $name ne "m" and $name ne "message";
413 0 0         return 0 if $val !~ /\n/;
414              
415 0           return 1;
416             }
417              
418             __END__