File Coverage

blib/lib/Cogit.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


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