File Coverage

blib/lib/Cogit.pm
Criterion Covered Total %
statement 219 254 86.2
branch 44 76 57.8
condition 18 48 37.5
subroutine 44 47 93.6
pod 6 23 26.0
total 331 448 73.8


line stmt bran cond sub pod time code
1             package Cogit;
2             $Cogit::VERSION = '0.001001';
3             # ABSTRACT: A truly Pure Perl interface to Git repositories
4              
5 4     4   199024 use Moo;
  4         33299  
  4         15  
6 4     4   4144 use Carp 'confess';
  4         5  
  4         145  
7 4     4   1454 use Check::ISA;
  4         41138  
  4         14  
8 4     4   3123 use MooX::Types::MooseLike::Base qw( InstanceOf ArrayRef Str );
  4         17374  
  4         312  
9 4     4   1835 use Data::Stream::Bulk::Array;
  4         1615289  
  4         142  
10 4     4   2147 use Data::Stream::Bulk::Path::Class;
  4         247157  
  4         167  
11 4     4   2486 use File::Find::Rule;
  4         24838  
  4         29  
12 4     4   1690 use Cogit::Config;
  4         13  
  4         153  
13 4     4   2009 use Cogit::Loose;
  4         14  
  4         135  
14 4     4   1678 use Cogit::Object::Blob;
  4         11  
  4         117  
15 4     4   1595 use Cogit::Object::Commit;
  4         14  
  4         142  
16 4     4   2040 use Cogit::Object::Tag;
  4         8  
  4         125  
17 4     4   1777 use Cogit::Object::Tree;
  4         10  
  4         106  
18 4     4   1678 use Cogit::Pack::WithIndex;
  4         13  
  4         119  
19 4     4   1867 use Cogit::Pack::WithoutIndex;
  4         11  
  4         121  
20 4     4   1498 use Cogit::Protocol;
  4         12  
  4         118  
21 4     4   22 use Path::Class;
  4         5  
  4         188  
22 4     4   16 use namespace::clean;
  4         4  
  4         17  
23              
24             has directory => (
25             is => 'ro',
26             isa => InstanceOf ['Path::Class::Dir'],
27             coerce => sub { return dir($_[0]); },
28             );
29              
30             has gitdir => (
31             is => 'ro',
32             isa => InstanceOf ['Path::Class::Dir'],
33             coerce => sub { return dir($_[0]); },
34             required => 1,
35             );
36              
37             has loose => (
38             is => 'rw',
39             isa => InstanceOf ['Cogit::Loose'],
40             lazy => 1,
41             builder => '_build_loose',
42             );
43              
44             has packs => (
45             is => 'rw',
46             isa => ArrayRef [InstanceOf ['Cogit::Pack']],
47             lazy => 1,
48             builder => '_build_packs',
49             );
50              
51             has description => (
52             is => 'rw',
53             isa => Str,
54             lazy => 1,
55             default => sub {
56             my $self = shift;
57             file($self->gitdir, 'description')->slurp(chomp => 1);
58             });
59              
60             has config => (
61             is => 'ro',
62             isa => InstanceOf ['Cogit::Config'],
63             lazy => 1,
64             default => sub {
65             my $self = shift;
66             Cogit::Config->new(git => $self);
67             });
68              
69             sub BUILDARGS {
70 8     8 0 22692 my $class = shift;
71 8         112 my $params = $class->SUPER::BUILDARGS(@_);
72              
73 8   66     143 $params->{'gitdir'} ||= dir($params->{'directory'}, '.git');
74 8         657 return $params;
75             }
76              
77             sub BUILD {
78 8     8 0 936 my $self = shift;
79              
80 8 50       64 unless (-d $self->gitdir) {
81 0         0 confess $self->gitdir . ' is not a directory';
82             }
83 8 50 66     446 unless (not defined $self->directory or -d $self->directory) {
84 0         0 confess $self->directory . ' is not a directory';
85             }
86             }
87              
88             sub _build_loose {
89 8     8   68 my $self = shift;
90 8         33 my $loose_dir = dir($self->gitdir, 'objects');
91 8         384 return Cogit::Loose->new(directory => $loose_dir);
92             }
93              
94             sub _build_packs {
95 8     8   69 my $self = shift;
96 8         42 my $pack_dir = dir($self->gitdir, 'objects', 'pack');
97 8         312 my @packs;
98 8         47 for my $filename ($pack_dir->children) {
99 4 100       739 next unless $filename =~ /\.pack$/;
100 2         80 push @packs, Cogit::Pack::WithIndex->new(filename => $filename);
101             }
102 8         1563 return \@packs;
103             }
104              
105             sub _ref_names_recursive {
106 22     22   33 my ($dir, $base, $names) = @_;
107              
108 22         61 for my $file ($dir->children) {
109 5 50       1182 if (-d $file) {
110 0         0 my $reldir = $file->relative($dir);
111 0         0 my $subbase = $base . $reldir . "/";
112 0         0 _ref_names_recursive($file, $subbase, $names);
113             } else {
114 5         220 push @$names, $base . $file->basename;
115             }
116             }
117             }
118              
119             sub ref_names {
120 11     11 0 12967 my $self = shift;
121 11         19 my @names;
122 11         30 for my $type (qw(heads remotes tags)) {
123 33         1049 my $dir = dir($self->gitdir, 'refs', $type);
124 33 100       1346 next unless -d $dir;
125 22         571 my $base = "refs/$type/";
126 22         53 _ref_names_recursive($dir, $base, \@names);
127             }
128 11         1213 my $packed_refs = file($self->gitdir, 'packed-refs');
129 11 100       638 if (-f $packed_refs) {
130 6         209 for my $line ($packed_refs->slurp(chomp => 1)) {
131 12 100       944 next if $line =~ /^#/;
132 6 50       17 next if $line =~ /^\^/;
133 6         20 my ($sha1, $name) = split ' ', $line;
134 6         15 push @names, $name;
135             }
136             }
137 11         273 return @names;
138             }
139              
140             sub refs_sha1 {
141 3     3 0 653 my $self = shift;
142 3         9 return map { $self->ref_sha1($_) } $self->ref_names;
  3         13  
143             }
144              
145             sub refs {
146 3     3 0 6 my $self = shift;
147 3         9 return map { $self->ref($_) } $self->ref_names;
  3         8  
148             }
149              
150             sub ref_sha1 {
151 31     31 0 55 my ($self, $wantref) = @_;
152 31         155 my $dir = dir($self->gitdir, 'refs');
153 31 50       1498 return unless -d $dir;
154              
155 31 50       1039 if ($wantref eq "HEAD") {
156 0         0 my $file = file($self->gitdir, 'HEAD');
157 0   0     0 my $sha1 = file($file)->slurp
158             || confess("Error reading $file: $!");
159 0         0 chomp $sha1;
160 0         0 return _ensure_sha1_is_sha1($self, $sha1);
161             }
162              
163 31         285 for my $file (File::Find::Rule->new->file->in($dir)) {
164 17         20517 my $ref = 'refs/' . file($file)->relative($dir)->as_foreign('Unix');
165 17 50       8211 if ($ref eq $wantref) {
166 17   33     59 my $sha1 = file($file)->slurp
167             || confess("Error reading $file: $!");
168 17         4333 chomp $sha1;
169 17         63 return _ensure_sha1_is_sha1($self, $sha1);
170             }
171             }
172              
173 14         12897 my $packed_refs = file($self->gitdir, 'packed-refs');
174 14 50       864 if (-f $packed_refs) {
175 14         455 my $last_name;
176             my $last_sha1;
177 14         46 for my $line ($packed_refs->slurp(chomp => 1)) {
178 28 100       2127 next if $line =~ /^#/;
179 14         48 my ($sha1, $name) = split ' ', $line;
180 14         27 $sha1 =~ s/^\^//;
181 14   33     31 $name ||= $last_name;
182              
183 14 0 33     36 return _ensure_sha1_is_sha1($self, $last_sha1)
      33        
184             if $last_name
185             and $last_name eq $wantref
186             and $name ne $wantref;
187              
188 14         21 $last_name = $name;
189 14         19 $last_sha1 = $sha1;
190             }
191 14 50       51 return _ensure_sha1_is_sha1($self, $last_sha1) if $last_name eq $wantref;
192             }
193 0         0 return undef;
194             }
195              
196             sub _ensure_sha1_is_sha1 {
197 31     31   51 my ($self, $sha1) = @_;
198 31 50       97 return $self->ref_sha1($1) if $sha1 =~ /^ref: (.*)/;
199 31         221 return $sha1;
200             }
201              
202             sub ref {
203 22     22 0 13268 my ($self, $wantref) = @_;
204 22         72 return $self->get_object($self->ref_sha1($wantref));
205             }
206              
207             sub master_sha1 {
208 3     3 0 90 my $self = shift;
209 3         9 return $self->ref_sha1('refs/heads/master');
210             }
211              
212             sub master {
213 10     10 1 176 my $self = shift;
214 10         45 return $self->ref('refs/heads/master');
215             }
216              
217             sub head_sha1 {
218 0     0 0 0 my $self = shift;
219 0         0 return $self->ref_sha1('HEAD');
220             }
221              
222             sub head {
223 0     0 0 0 my $self = shift;
224 0         0 return $self->ref('HEAD');
225             }
226              
227             sub get_object {
228 125     125 1 2863 my ($self, $sha1) = @_;
229 125 100       294 return unless $sha1;
230 118   66     262 return $self->get_object_packed($sha1) || $self->get_object_loose($sha1);
231             }
232              
233             sub get_objects {
234 9     9 0 36 my ($self, @sha1s) = @_;
235 9         22 return map { $self->get_object($_) } @sha1s;
  49         3075  
236             }
237              
238             sub get_object_packed {
239 118     118 1 132 my ($self, $sha1) = @_;
240              
241 118         134 for my $pack (@{$self->packs}) {
  118         2206  
242 46         454 my ($kind, $size, $content) = $pack->get_object($sha1);
243 46 50 33     261 if (defined($kind) && defined($size) && defined($content)) {
      33        
244 46         98 return $self->create_object($sha1, $kind, $size, $content);
245             }
246             }
247             }
248              
249             sub get_object_loose {
250 72     72 1 828 my ($self, $sha1) = @_;
251              
252 72         1227 my ($kind, $size, $content) = $self->loose->get_object($sha1);
253 72 50 33     624 if (defined($kind) && defined($size) && defined($content)) {
      33        
254 72         226 return $self->create_object($sha1, $kind, $size, $content);
255             }
256             }
257              
258             sub create_object {
259 118     118 1 205 my ($self, $sha1, $kind, $size, $content) = @_;
260 118 100       354 if ($kind eq 'commit') {
    100          
    50          
    0          
261 48         1266 return Cogit::Object::Commit->new(
262             sha1 => $sha1,
263             kind => $kind,
264             size => $size,
265             content => $content,
266             git => $self,
267             );
268             } elsif ($kind eq 'tree') {
269 31         791 return Cogit::Object::Tree->new(
270             sha1 => $sha1,
271             kind => $kind,
272             size => $size,
273             content => $content,
274             git => $self,
275             );
276             } elsif ($kind eq 'blob') {
277 39         990 return Cogit::Object::Blob->new(
278             sha1 => $sha1,
279             kind => $kind,
280             size => $size,
281             content => $content,
282             git => $self,
283             );
284             } elsif ($kind eq 'tag') {
285 0         0 return Cogit::Object::Tag->new(
286             sha1 => $sha1,
287             kind => $kind,
288             size => $size,
289             content => $content,
290             git => $self,
291             );
292             } else {
293 0         0 confess "unknown kind $kind: $content";
294             }
295             }
296              
297             sub all_sha1s {
298 18     18 1 9202 my $self = shift;
299 18         129 my $dir = dir($self->gitdir, 'objects');
300              
301 18         895 my @streams;
302 18         417 push @streams, $self->loose->all_sha1s;
303              
304 18         3949 for my $pack (@{$self->packs}) {
  18         346  
305 4         38 push @streams, $pack->all_sha1s;
306             }
307              
308 18         1111 return Data::Stream::Bulk::Cat->new(streams => \@streams);
309             }
310              
311             sub all_objects {
312 9     9 0 11606 my $self = shift;
313 9         30 my $stream = $self->all_sha1s;
314             return Data::Stream::Bulk::Filter->new(
315 9     9   1227 filter => sub { return [$self->get_objects(@$_)] },
316 9         1556 stream => $stream,
317             );
318             }
319              
320             sub put_object {
321 14     14 0 2159 my ($self, $object, $ref) = @_;
322 14         299 $self->loose->put_object($object);
323              
324 14 100       745 if ($object->kind eq 'commit') {
325 4 50       24 $ref = 'master' unless $ref;
326 4         98 $self->update_ref($ref, $object->sha1);
327             }
328             }
329              
330             sub update_ref {
331 4     4 0 33 my ($self, $refname, $sha1) = @_;
332 4         24 my $ref = file($self->gitdir, 'refs', 'heads', $refname);
333 4         291 $ref->parent->mkpath;
334 4         349 my $ref_fh = $ref->openw;
335 4 50       632 $ref_fh->print($sha1) || die "Error writing to $ref";
336              
337             # FIXME is this always what we want?
338 4         54 my $head = file($self->gitdir, 'HEAD');
339 4         247 my $head_fh = $head->openw;
340 4 50       619 $head_fh->print("ref: refs/heads/$refname")
341             || die "Error writing to $head";
342             }
343              
344             sub init {
345 2     2 0 2222 my ($class, %arguments) = @_;
346              
347 2         5 my $directory = $arguments{directory};
348 2         3 my $git_dir;
349              
350 2 100       8 unless (defined $directory) {
351             $git_dir = $arguments{gitdir}
352 1   33     6 || confess "init() needs either a 'directory' or a 'gitdir' argument";
353             } else {
354 1 50       3 if (not defined $arguments{gitdir}) {
355 1         6 $git_dir = $arguments{gitdir} = dir($directory, '.git');
356             }
357 1         109 dir($directory)->mkpath;
358             }
359              
360 2         94 dir($git_dir)->mkpath;
361 2         316 dir($git_dir, 'refs', 'tags')->mkpath;
362 2         496 dir($git_dir, 'objects', 'info')->mkpath;
363 2         392 dir($git_dir, 'objects', 'pack')->mkpath;
364 2         256 dir($git_dir, 'branches')->mkpath;
365 2         252 dir($git_dir, 'hooks')->mkpath;
366              
367 2 100       243 my $bare = defined($directory) ? 'false' : 'true';
368 2         10 $class->_add_file(
369             file($git_dir, 'config'),
370             "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = $bare\n\tlogallrefupdates = true\n"
371             );
372 2         111 $class->_add_file(file($git_dir, 'description'),
373             "Unnamed repository; edit this file to name it for gitweb.\n");
374 2         61 $class->_add_file(file($git_dir, 'hooks', 'applypatch-msg'),
375             "# add shell script and make executable to enable\n");
376 2         62 $class->_add_file(file($git_dir, 'hooks', 'post-commit'),
377             "# add shell script and make executable to enable\n");
378 2         104 $class->_add_file(file($git_dir, 'hooks', 'post-receive'),
379             "# add shell script and make executable to enable\n");
380 2         59 $class->_add_file(file($git_dir, 'hooks', 'post-update'),
381             "# add shell script and make executable to enable\n");
382 2         59 $class->_add_file(file($git_dir, 'hooks', 'pre-applypatch'),
383             "# add shell script and make executable to enable\n");
384 2         59 $class->_add_file(file($git_dir, 'hooks', 'pre-commit'),
385             "# add shell script and make executable to enable\n");
386 2         63 $class->_add_file(file($git_dir, 'hooks', 'pre-rebase'),
387             "# add shell script and make executable to enable\n");
388 2         58 $class->_add_file(file($git_dir, 'hooks', 'update'),
389             "# add shell script and make executable to enable\n");
390              
391 2         61 dir($git_dir, 'info')->mkpath;
392 2         309 $class->_add_file(file($git_dir, 'info', 'exclude'), "# *.[oa]\n# *~\n");
393              
394 2         101 return $class->new(%arguments);
395             }
396              
397             sub checkout {
398 5     5 0 13951 my ($self, $directory, $tree) = @_;
399 5   33     34 $directory ||= $self->directory;
400 5   33     73 $tree ||= $self->master->tree;
401 5 50       501 confess("Missing tree") unless $tree;
402 5         10 for my $directory_entry (@{$tree->directory_entries}) {
  5         110  
403 7         178 my $filename = file($directory, $directory_entry->filename);
404 7         422 my $sha1 = $directory_entry->sha1;
405 7         20 my $mode = $directory_entry->mode;
406 7         18 my $object = $self->get_object($sha1);
407 7 50       584 if ($object->kind eq 'blob') {
    0          
408 7         120 $self->_add_file($filename, $object->content);
409 7 50       330 chmod(oct('0' . $mode), $filename)
410             || die "Error chmoding $filename to $mode: $!";
411             } elsif ($object->kind eq 'tree') {
412 0         0 dir($filename)->mkpath;
413 0         0 $self->checkout($filename, $object);
414             } else {
415 0         0 die $object->kind;
416             }
417             }
418             }
419              
420             sub clone {
421 0     0 0 0 my $self = shift;
422              
423 0         0 my $remote;
424 0 0       0 if (@_ == 2) {
425              
426             # For backwards compatibility
427 0         0 $remote = "git://$_[0]";
428 0 0       0 $remote .= "/" unless $_[1] =~ m{^/};
429 0         0 $remote .= $_[1];
430             } else {
431 0         0 $remote = shift;
432             }
433              
434 0         0 my $protocol = Cogit::Protocol->new(remote => $remote)->connect;
435              
436 0         0 my $sha1s = $protocol->fetch;
437 0         0 my $head = $sha1s->{HEAD};
438 0         0 my $data = $protocol->fetch_pack($head);
439              
440 0         0 my $filename =
441             file($self->gitdir, 'objects', 'pack', 'pack-' . $head . '.pack');
442 0         0 $self->_add_file($filename, $data);
443              
444 0         0 my $pack = Cogit::Pack::WithoutIndex->new(filename => $filename);
445 0         0 $pack->create_index();
446              
447 0         0 $self->update_ref(master => $head);
448             }
449              
450             sub _add_file {
451 29     29   1413 my ($class, $filename, $contents) = @_;
452 29   33     64 my $fh = $filename->openw || confess "Error opening to $filename: $!";
453 29         4160 binmode($fh); #important for Win32
454 29 50       122 $fh->print($contents) || confess "Error writing to $filename: $!";
455 29 50       294 $fh->close || confess "Error closing $filename: $!";
456             }
457              
458             1;
459              
460             __END__
461              
462             =pod
463              
464             =encoding UTF-8
465              
466             =head1 NAME
467              
468             Cogit - A truly Pure Perl interface to Git repositories
469              
470             =head1 VERSION
471              
472             version 0.001001
473              
474             =head1 SYNOPSIS
475              
476             my $git = Cogit->new(
477             directory => '/path/to/git/'
478             );
479             $git->master->committer;
480             $git->master->comment;
481             $git->get_object($git->master->tree);
482              
483             =head1 DESCRIPTION
484              
485             This module is a Pure Perl interface to Git repositories.
486              
487             It was mostly based on Grit L<http://grit.rubyforge.org/>.
488              
489             =head1 HERE BE DRAGONS
490              
491             This module's API is not yet battle tested. Feel free to try it out, but don't
492             depend on it for serious stuff yet. Comments regarding the API very welcome.
493              
494             =head1 METHODS
495              
496             =over 4
497              
498             =item master
499              
500             =item get_object
501              
502             =item get_object_packed
503              
504             =item get_object_loose
505              
506             =item create_object
507              
508             =item all_sha1s
509              
510             =back
511              
512             =head1 FORK
513              
514             This module was forked from L<Git::PurePerl> for a couple reasons. First and
515             foremost, C<Git::PurePerl> is based on L<Moose>, which is not pure perl.
516             Secondarily the API was very weird, with differentiations made based on whether
517             or not an object was in the repo or not.
518              
519             =head1 CONTRIBUTORS
520              
521             =over 4
522              
523             =item Alex Vandiver
524              
525             =item Chris Reinhardt
526              
527             =item Dagfinn Ilmari MannsE<aring>ker
528              
529             =item Dan (broquaint) Brook
530              
531             =item Leon Brocard
532              
533             =item Tomas (t0m) Doran
534              
535             =back
536              
537             =head1 AUTHOR
538              
539             Arthur Axel "fREW" Schmidt <cogit@afoolishmanifesto.com>
540              
541             =head1 COPYRIGHT AND LICENSE
542              
543             This software is copyright (c) 2017 by Arthur Axel "fREW" Schmidt.
544              
545             This is free software; you can redistribute it and/or modify it under
546             the same terms as the Perl 5 programming language system itself.
547              
548             =cut