File Coverage

blib/lib/Git/ObjectStore.pm
Criterion Covered Total %
statement 190 209 90.9
branch 44 68 64.7
condition 2 3 66.6
subroutine 20 22 90.9
pod 14 14 100.0
total 270 316 85.4


line stmt bran cond sub pod time code
1             package Git::ObjectStore;
2             $Git::ObjectStore::VERSION = '0.007';
3 1     1   13219 use strict;
  1         2  
  1         23  
4 1     1   3 use warnings;
  1         1  
  1         21  
5              
6 1     1   384 use Git::Raw;
  1         17251  
  1         28  
7 1     1   5 use Git::Raw::Object;
  1         1  
  1         15  
8 1     1   3 use Carp;
  1         1  
  1         146  
9 1     1   424 use File::Spec::Functions qw(catfile);
  1         595  
  1         1413  
10              
11              
12             # ABSTRACT: abstraction layer for Git::Raw and libgit2
13              
14              
15              
16              
17             sub new
18             {
19 3     3 1 1110 my ($class, %args) = @_;
20 3         6 my $self = bless {}, $class;
21              
22 3         12 $self->{'author_name'} = 'ObjectStore';
23 3         5 $self->{'author_email'} = 'ObjectStore@localhost';
24              
25 3         7 foreach my $arg (qw(repodir branchname)) {
26 6 50       14 if ( defined( $args{$arg} ) ) {
27 6         11 $self->{$arg} = $args{$arg};
28             } else {
29 0         0 croak('Mandatory argument missing: ' . $arg);
30             }
31             }
32              
33 3         5 foreach my $arg (qw(writer author_name author_email)) {
34 9 100       18 if ( defined( $args{$arg} ) ) {
35 1         3 $self->{$arg} = $args{$arg};
36             }
37             }
38              
39 3 50 66     11 if ( $self->{'writer'} and $args{'goto'} ) {
40 0         0 croak('Cannot use goto in writer mode');
41             }
42              
43 3         4 my $branchname = $self->{'branchname'};
44 3         4 my $repodir = $self->{'repodir'};
45              
46 3 100       54 if ( not -e $repodir . '/config' ) {
47 1 50       3 if( $self->{'writer'} ) {
48 1         1592 Git::Raw::Repository->init($repodir, 1);
49             } else {
50 0         0 croak($repodir . ' does not contain a bare Git repository');
51             }
52             }
53              
54 3         391 my $repo = $self->{'repo'} = Git::Raw::Repository->open($repodir);
55 3         17 my $objdir = catfile($repodir, 'objects');
56              
57             # We do not use loose backend, so we explicitly exclude it from ODB.
58             {
59 3         4 my $odb = Git::Raw::Odb->new();
  3         18  
60 3         250 $odb->add_backend(Git::Raw::Odb::Backend::Pack->new($objdir), 10);
61 3         38 $repo->odb($odb);
62             }
63              
64 3 100       14 if ( $self->{'writer'} ) {
65              
66 1         3 my $refname = 'refs/heads/' . $branchname;
67              
68             # in-memory store that will write a single pack file for all objects
69 1         5 $self->{'packdir'} = catfile($objdir, 'pack');
70 1         8 my $mempack = $self->{'mempack'} = Git::Raw::Mempack->new;
71 1         8 $repo->odb->add_backend($mempack, 99);
72              
73 1         139 my $branch = Git::Raw::Branch->lookup($repo, $branchname, 1);
74              
75 1 50       5 if( defined($branch) )
76             {
77             # If previous run of the writer crashed, we have a reference that
78             # points to nonexistent commit, because mempack was not written.
79 0         0 eval { $branch->peel('commit') };
  0         0  
80 0 0       0 if( $@ )
81             {
82 0         0 $branch = undef;
83 0         0 Git::Raw::Reference->lookup($refname, $repo)->delete();
84             }
85             }
86            
87 1 50       3 if ( not defined($branch) ) {
88             # This is a fresh repo, create the branch
89 1         16 my $builder = Git::Raw::Tree::Builder->new($repo);
90 1         62 my $tree = $builder->write();
91 1         5 my $me = $self->_signature();
92 1         9 my $commit = $repo->commit("Initial empty commit in $branchname",
93             $me, $me, [], $tree, $refname);
94 1         296 $self->{'created_init_commit'} = $commit;
95 1         47 $branch = Git::Raw::Branch->lookup($repo, $branchname, 1);
96             }
97              
98 1 50       5 croak('expected a branch') unless defined($branch);
99              
100             # in-memory index for preparing a commit
101 1         11 my $index = Git::Raw::Index->new();
102              
103             # assign the index to our repo
104 1         9 $repo->index($index);
105              
106             # initiate the index with the top of the branch
107 1         17 my $commit = $branch->peel('commit');
108 1         29 $index->read_tree($commit->tree());
109              
110             # memorize the index for quick write access
111 1         4 $self->{'gitindex'} = $index;
112              
113 1         12 $self->{'current_commit_id'} = $commit->id();
114              
115             } else {
116             # open the repo for read-only access
117 2         3 my $commit;
118 2 50       6 if ( defined($args{'goto'}) ) {
119             # read from a specified commit
120 0         0 $commit = Git::Raw::Commit->lookup($repo, $args{'goto'});
121 0 0       0 croak('Cannot lookup commit ' . $args{'goto'})
122             unless defined($commit);
123             } else {
124             # read from the top of the branch
125 2         147 my $branch = Git::Raw::Branch->lookup($repo, $branchname, 1);
126 2         146 $commit = $branch->peel('commit');
127             }
128              
129             # memorize the tree that we will read
130 2         21 $self->{'gittree'} = $commit->tree();
131              
132 2         11 $self->{'current_commit_id'} = $commit->id();
133             }
134              
135 3         14 return $self;
136             }
137              
138              
139             sub _signature
140             {
141 3     3   4 my $self = shift;
142             return Git::Raw::Signature->now
143 3         115 ($self->{'author_name'}, $self->{'author_email'});
144             }
145              
146              
147              
148             sub created_init_commit
149             {
150 0     0 1 0 my $self = shift;
151 0         0 return $self->{'created_init_commit'};
152             }
153              
154              
155              
156              
157             sub repo
158             {
159 0     0 1 0 my $self = shift;
160 0         0 return $self->{'repo'};
161             }
162              
163              
164              
165              
166              
167             sub read_file
168             {
169 6     6 1 1177 my $self = shift;
170 6         7 my $filename = shift;
171              
172 6 100       12 if ( $self->{'writer'} ) {
173 2         19 my $entry = $self->{'gitindex'}->find($filename);
174 2 100       18 if ( defined($entry) ) {
175 1         14 return $entry->blob()->content();
176             } else {
177 1         3 return undef;
178             }
179             } else {
180 4         56 my $entry = $self->{'gittree'}->entry_bypath($filename);
181 4 50       12 if ( defined($entry) ) {
182 4         69 return $entry->object()->content();
183             } else {
184 0         0 return undef;
185             }
186             }
187             }
188              
189              
190              
191             sub file_exists
192             {
193 2     2 1 216 my $self = shift;
194 2         3 my $filename = shift;
195              
196 2 50       6 if ( $self->{'writer'} ) {
197 2         24 return defined($self->{'gitindex'}->find($filename));
198             } else {
199 0         0 return defined($self->{'gittree'}->entry_bypath($filename));
200             }
201             }
202              
203              
204             sub current_commit_id
205             {
206 4     4 1 808 my $self = shift;
207 4         14 return $self->{'current_commit_id'};
208             }
209              
210              
211              
212             sub write_and_check
213             {
214 2     2 1 184 my $self = shift;
215 2         2 my $filename = shift;
216 2         2 my $data = shift;
217              
218             croak('write_and_check() is called for a read-only ObjectStore object')
219 2 50       6 unless $self->{'writer'};
220              
221 2         3 my $prev_blob_id = '';
222 2 100       17 if( defined(my $entry = $self->{'gitindex'}->find($filename)) ) {
223 1         9 $prev_blob_id = $entry->blob()->id();
224             }
225              
226 2         122 my $entry = $self->{'gitindex'}->add_frombuffer($filename, $data);
227 2         23 my $new_blob_id = $entry->blob()->id();
228              
229 2         16 return ($new_blob_id ne $prev_blob_id);
230             }
231              
232              
233              
234             sub write_file
235             {
236 7     7 1 1796 my $self = shift;
237 7         7 my $filename = shift;
238 7         3 my $data = shift;
239              
240             croak('write_file() is called for a read-only ObjectStore object')
241 7 50       16 unless $self->{'writer'};
242              
243 7         275 $self->{'gitindex'}->add_frombuffer($filename, $data);
244 7         45 return;
245             }
246              
247              
248              
249             sub delete_file
250             {
251 2     2 1 185 my $self = shift;
252 2         2 my $path = shift;
253              
254             croak('delete_file() is called for a read-only ObjectStore object')
255 2 50       6 unless $self->{'writer'};
256 2         15 $self->{'gitindex'}->remove($path);
257 2         3 return;
258             }
259              
260              
261              
262             sub create_commit
263             {
264 3     3 1 3 my $self = shift;
265 3         2 my $msg = shift;
266              
267             croak('create_commit() is called for a read-only ObjectStore object')
268 3 50       8 unless $self->{'writer'};
269              
270 3 50       5 if( not defined($msg) ) {
271 3         67 $msg = scalar(localtime(time()));
272             }
273              
274 3         4 my $branchname = $self->{'branchname'};
275 3         3 my $repo = $self->{'repo'};
276 3         4 my $index = $self->{'gitindex'};
277              
278 3         100 my $branch = Git::Raw::Branch->lookup($self->{'repo'}, $branchname, 1);
279 3         19 my $parent = $branch->peel('commit');
280              
281             # this creates a new tree object from changes in the index
282 3         320 my $tree = $index->write_tree();
283              
284 3 100       28 if( $tree->id() eq $parent->tree()->id() ) {
285             # The tree identifier has not changed, hence there are no
286             # changes in content
287 1         12 return 0;
288             }
289              
290 2         11 my $me = $self->_signature();
291 2         19 my $commit = $repo->commit
292             ($msg, $me, $me, [$parent], $tree, $branch->name());
293              
294             # re-initialize the index
295 2         589 $index->clear();
296 2         38 $index->read_tree($tree);
297              
298 2         12 $self->{'current_commit_id'} = $commit->id();
299              
300 2         26 return 1;
301             }
302              
303              
304              
305             sub write_packfile
306             {
307 3     3 1 2 my $self = shift;
308              
309             croak('write_packfile() is called for a read-only ObjectStore object')
310 3 50       11 unless $self->{'writer'};
311              
312 3         4 my $repo = $self->{'repo'};
313 3         9 my $tp = Git::Raw::TransferProgress->new();
314 3         242 my $indexer = Git::Raw::Indexer->new($self->{'packdir'}, $repo->odb());
315              
316 3         1237 $indexer->append($self->{'mempack'}->dump($repo), $tp);
317 3         552 $indexer->commit($tp);
318 3         15 $self->{'mempack'}->reset;
319 3         38 return;
320             }
321              
322              
323              
324             sub create_commit_and_packfile
325             {
326 3     3 1 582 my $self = shift;
327 3         3 my $msg = shift;
328              
329 3 100       6 if( $self->create_commit($msg) ) {
    50          
330 2         6 $self->write_packfile();
331 2         6 return 1;
332             } elsif ( defined($self->{'created_init_commit'}) ) {
333 1         3 $self->write_packfile();
334             }
335              
336 1         6 return 0;
337             }
338              
339              
340              
341             sub recursive_read
342             {
343 4     4 1 3043 my $self = shift;
344 4         6 my $path = shift;
345 4         3 my $callback = shift;
346 4         4 my $no_content = shift;
347            
348             croak('recursive_read() is called for a read-write ObjectStore object')
349 4 50       12 if $self->{'writer'};
350              
351 4 100       8 if( $path eq '' )
352             {
353 2         21 foreach my $entry ($self->{'gittree'}->entries()) {
354 9         33 $self->_do_recursive_read
355             ($entry, $entry->name(), $callback, $no_content);
356             }
357             } else {
358 2         28 my $entry = $self->{'gittree'}->entry_bypath($path);
359 2 50       7 if( defined($entry) ) {
360 2         5 $self->_do_recursive_read($entry, $path, $callback, $no_content);
361             }
362             else
363             {
364 0         0 croak("No such path in the branch: $path");
365             }
366             }
367 4         18 return;
368             }
369              
370              
371             sub _do_recursive_read
372             {
373 26     26   22 my $self = shift;
374 26         15 my $entry = shift; # Git::Raw::Tree::Entry object
375 26         23 my $path = shift;
376 26         19 my $callback = shift;
377 26         21 my $no_content = shift;
378              
379 26         43 $entry->type();
380              
381 26 100       80 if( $entry->type() == Git::Raw::Object->TREE ) {
382             # this is a subtree, we read it recursively
383 15         156 foreach my $child_entry ($entry->object()->entries()) {
384 15         61 $self->_do_recursive_read
385             ($child_entry, $path . '/' . $child_entry->name(), $callback);
386             }
387             } else {
388 11 50       54 if( $no_content ) {
389 0         0 &{$callback}($path);
  0         0  
390             } else {
391 11         115 &{$callback}($path, $entry->object()->content());
  11         25  
392             }
393             }
394              
395 26         189 return;
396             }
397              
398              
399              
400              
401              
402             sub read_updates
403             {
404 2     2 1 612 my $self = shift;
405 2         4 my $old_commit_id = shift;
406 2         2 my $cb_updated = shift;
407 2         2 my $cb_deleted = shift;
408 2         2 my $no_content = shift;
409              
410 2         39 my $old_commit = Git::Raw::Commit->lookup($self->{'repo'}, $old_commit_id);
411 2 50       7 croak("Cannot lookup commit $old_commit_id") unless defined($old_commit);
412 2         16 my $old_tree = $old_commit->tree();
413              
414 2         3 my $new_tree = $self->{'gittree'};
415              
416 2         237 my $diff = $old_tree->diff
417             (
418             {
419             'tree' => $new_tree,
420             'flags' => {
421             'skip_binary_check' => 1,
422             },
423             }
424             );
425              
426 2         47 my @deltas = $diff->deltas();
427 2         6 foreach my $delta (@deltas) {
428              
429 10         109 my $path = $delta->new_file()->path();
430              
431 10 100       47 if( $delta->status() eq 'deleted') {
432 4         4 &{$cb_deleted}($path);
  4         33  
433             } else {
434 6 100       12 if( $no_content ) {
435 3         3 &{$cb_updated}($path);
  3         6  
436             } else {
437 3         19 my $entry = $new_tree->entry_bypath($path);
438 3         40 &{$cb_updated}($path, $entry->object()->content());
  3         10  
439             }
440             }
441             }
442              
443 2         43 return;
444             }
445              
446              
447              
448              
449              
450              
451              
452             1;
453              
454             # Local Variables:
455             # mode: cperl
456             # indent-tabs-mode: nil
457             # cperl-indent-level: 4
458             # cperl-continued-statement-offset: 4
459             # cperl-continued-brace-offset: -4
460             # cperl-brace-offset: 0
461             # cperl-label-offset: -2
462             # End:
463              
464             __END__