File Coverage

blib/lib/Git/ObjectStore.pm
Criterion Covered Total %
statement 183 197 92.8
branch 42 62 67.7
condition 2 3 66.6
subroutine 20 22 90.9
pod 14 14 100.0
total 261 298 87.5


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