line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package GitStore; |
2
|
|
|
|
|
|
|
BEGIN { |
3
|
6
|
|
|
6
|
|
167216
|
$GitStore::AUTHORITY = 'cpan:YANICK'; |
4
|
|
|
|
|
|
|
} |
5
|
|
|
|
|
|
|
{ |
6
|
|
|
|
|
|
|
$GitStore::VERSION = '0.15'; |
7
|
|
|
|
|
|
|
} |
8
|
|
|
|
|
|
|
#ABSTRACT: Git as versioned data store in Perl |
9
|
|
|
|
|
|
|
|
10
|
6
|
|
|
6
|
|
9212
|
use Moose; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
use Moose::Util::TypeConstraints; |
12
|
|
|
|
|
|
|
use Git::PurePerl; |
13
|
|
|
|
|
|
|
use Carp; |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
use Path::Class qw/ dir file /; |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
use List::Util qw/ first /; |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
use GitStore::Revision; |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
no warnings qw/ uninitialized /; |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
subtype 'PurePerlActor' => |
24
|
|
|
|
|
|
|
as 'Git::PurePerl::Actor'; |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
coerce PurePerlActor |
27
|
|
|
|
|
|
|
=> from 'Str' |
28
|
|
|
|
|
|
|
=> via { |
29
|
|
|
|
|
|
|
s/<(.*?)>//; |
30
|
|
|
|
|
|
|
Git::PurePerl::Actor->new( name => $_, email => $1 ); |
31
|
|
|
|
|
|
|
}; |
32
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
has 'repo' => ( is => 'ro', isa => 'Str', required => 1 ); |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
has 'branch' => ( is => 'rw', isa => 'Str', default => 'master' ); |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
has author => ( |
38
|
|
|
|
|
|
|
is => 'rw', |
39
|
|
|
|
|
|
|
isa => 'PurePerlActor', |
40
|
|
|
|
|
|
|
default => sub { |
41
|
|
|
|
|
|
|
Git::PurePerl::Actor->new( |
42
|
|
|
|
|
|
|
name => 'anonymous', |
43
|
|
|
|
|
|
|
email => 'anon@127.0.0.1' |
44
|
|
|
|
|
|
|
); |
45
|
|
|
|
|
|
|
} ); |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
has serializer => ( |
48
|
|
|
|
|
|
|
is => 'ro', |
49
|
|
|
|
|
|
|
default => sub { |
50
|
|
|
|
|
|
|
require Storable; |
51
|
|
|
|
|
|
|
return sub { return Storable::nfreeze($_[2]); } |
52
|
|
|
|
|
|
|
}, |
53
|
|
|
|
|
|
|
); |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
has deserializer => ( |
56
|
|
|
|
|
|
|
is => 'ro', |
57
|
|
|
|
|
|
|
default => sub { |
58
|
|
|
|
|
|
|
require Storable; |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
return sub { |
61
|
|
|
|
|
|
|
my $data = $_[2]; |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
my $magic = eval { Storable::read_magic($data); }; |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
return $data unless $magic && $magic->{major} && $magic->{major} >= 2 && $magic->{major} <= 5; |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
my $thawed = eval { Storable::thaw($data) }; |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
# false alarm... looked like a Storable, but wasn't. |
70
|
|
|
|
|
|
|
return $@ ? $data : $thawed; |
71
|
|
|
|
|
|
|
} |
72
|
|
|
|
|
|
|
}, |
73
|
|
|
|
|
|
|
); |
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
has autocommit => ( |
76
|
|
|
|
|
|
|
is => 'ro', |
77
|
|
|
|
|
|
|
isa => 'Bool', |
78
|
|
|
|
|
|
|
default => 0, |
79
|
|
|
|
|
|
|
); |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
sub _clean_directories { |
82
|
|
|
|
|
|
|
my ( $self, $dir ) = @_; |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
$dir ||= $self->root; |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
my $nbr_files = keys %{ $dir->{FILES} }; |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
for my $d ( keys %{ $dir->{DIRS} } ) { |
89
|
|
|
|
|
|
|
if( my $f = $self->_clean_directories( $dir->{DIRS}{$d} ) ) { |
90
|
|
|
|
|
|
|
$nbr_files += $f; |
91
|
|
|
|
|
|
|
} |
92
|
|
|
|
|
|
|
else { |
93
|
|
|
|
|
|
|
delete $dir->{DIRS}{$d}; |
94
|
|
|
|
|
|
|
} |
95
|
|
|
|
|
|
|
} |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
return $nbr_files; |
98
|
|
|
|
|
|
|
} |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
sub _expand_directories { |
101
|
|
|
|
|
|
|
my( $self, $object ) = @_; |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
my %dir = ( DIRS => {}, FILES => {} ); |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
for my $entry ( map { $_->directory_entries } $object ) { |
106
|
|
|
|
|
|
|
if ( $entry->object->isa( 'Git::PurePerl::Object::Tree' ) ) { |
107
|
|
|
|
|
|
|
$dir{DIRS}{$entry->filename} |
108
|
|
|
|
|
|
|
= $self->_expand_directories( $entry->object ); |
109
|
|
|
|
|
|
|
} |
110
|
|
|
|
|
|
|
else { |
111
|
|
|
|
|
|
|
$dir{FILES}{$entry->filename} = $entry->sha1; |
112
|
|
|
|
|
|
|
} |
113
|
|
|
|
|
|
|
} |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
return \%dir; |
116
|
|
|
|
|
|
|
} |
117
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
has 'root' => ( is => 'rw', isa => 'HashRef', default => sub { {} } ); |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
has 'git' => ( |
121
|
|
|
|
|
|
|
is => 'ro', |
122
|
|
|
|
|
|
|
isa => 'Git::PurePerl', |
123
|
|
|
|
|
|
|
lazy => 1, |
124
|
|
|
|
|
|
|
default => sub { |
125
|
|
|
|
|
|
|
my $repo = $_[0]->repo; |
126
|
|
|
|
|
|
|
return Git::PurePerl->new( |
127
|
|
|
|
|
|
|
( $repo =~ m/\.git$/ ? 'gitdir' : 'directory') => $repo |
128
|
|
|
|
|
|
|
); |
129
|
|
|
|
|
|
|
} |
130
|
|
|
|
|
|
|
); |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
sub BUILD { |
133
|
|
|
|
|
|
|
my $self = shift; |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
$self->load(); |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
} |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
sub BUILDARGS { |
140
|
|
|
|
|
|
|
my $class = shift; |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
if ( @_ == 1 && ! ref $_[0] ) { |
143
|
|
|
|
|
|
|
return { repo => $_[0] }; |
144
|
|
|
|
|
|
|
} else { |
145
|
|
|
|
|
|
|
return $class->SUPER::BUILDARGS(@_); |
146
|
|
|
|
|
|
|
} |
147
|
|
|
|
|
|
|
} |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
sub branch_head { |
150
|
|
|
|
|
|
|
my ( $self, $branch ) = @_; |
151
|
|
|
|
|
|
|
$branch ||= $self->branch; |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
return $self->git->ref_sha1('refs/heads/' . $branch); |
154
|
|
|
|
|
|
|
} |
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
# Load the current head version from repository. |
157
|
|
|
|
|
|
|
sub load { |
158
|
|
|
|
|
|
|
my $self = shift; |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
my $head = $self->branch_head or do { |
161
|
|
|
|
|
|
|
$self->root({ DIRS => {}, FILES => {} }); |
162
|
|
|
|
|
|
|
return; |
163
|
|
|
|
|
|
|
}; |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
my $commit = $self->git->get_object($head); |
166
|
|
|
|
|
|
|
my $tree = $commit->tree; |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
my $root = $self->_expand_directories( $tree ); |
169
|
|
|
|
|
|
|
$self->root($root); |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
} |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
sub _normalize_path { |
174
|
|
|
|
|
|
|
my ( $self, $path ) = @_; |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
$path = join '/', @$path if ref $path eq 'ARRAY'; |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
# Git doesn't like paths prefixed with a '/' |
179
|
|
|
|
|
|
|
$path =~ s#^/+##; |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
return $path; |
182
|
|
|
|
|
|
|
} |
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
sub get_revision { |
185
|
|
|
|
|
|
|
my ( $self, $path ) = @_; |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
$path = file( $self->_normalize_path($path) ); |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
my $head = $self->branch_head |
190
|
|
|
|
|
|
|
or return; |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
my $commit = $self->git->get_object($head); |
193
|
|
|
|
|
|
|
my @q = ( $commit ); |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
my $file = $self->_find_file( $commit->tree, $path ) |
196
|
|
|
|
|
|
|
or return; |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
my $latest_file_sha1 = $file->object->sha1; |
199
|
|
|
|
|
|
|
my $last_commit; |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
while ( @q ) { |
202
|
|
|
|
|
|
|
push @q, $q[0]->parents; |
203
|
|
|
|
|
|
|
$last_commit = $commit; |
204
|
|
|
|
|
|
|
$commit = shift @q; |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
my $f = $self->_find_file( $commit->tree, file($path) ) |
207
|
|
|
|
|
|
|
or last; |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
last if $f->object->sha1 ne $latest_file_sha1; |
210
|
|
|
|
|
|
|
} |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
return GitStore::Revision->new( |
213
|
|
|
|
|
|
|
gitstore => $self, |
214
|
|
|
|
|
|
|
path => $path, |
215
|
|
|
|
|
|
|
sha1 => $last_commit->sha1, |
216
|
|
|
|
|
|
|
); |
217
|
|
|
|
|
|
|
} |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
sub get { |
220
|
|
|
|
|
|
|
my ( $self, $path ) = @_; |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
$path = file( $self->_normalize_path($path) ); |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
my $dir = $self->_cd_dir($path) or return; |
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
my $sha1 = $dir->{FILES}{$path->basename} or return; |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
my $object = $self->git->get_object($sha1) or return; |
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
return $self->deserializer->($self,$path,$object->content); |
231
|
|
|
|
|
|
|
} |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
sub set { |
234
|
|
|
|
|
|
|
my ( $self, $path, $content ) = @_; |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
$path = file( $self->_normalize_path($path) ); |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
my $dir = $self->_cd_dir($path,1) or return; |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
$content = $self->serializer->( $self, $path, $content ) if ref $content; |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
my $blob = Git::PurePerl::NewObject::Blob->new( content => $content ); |
243
|
|
|
|
|
|
|
$self->git->put_object($blob); |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
return $dir->{FILES}{$path->basename} = $blob->sha1; |
246
|
|
|
|
|
|
|
} |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
after [ 'set', 'delete' ] => sub { |
249
|
|
|
|
|
|
|
my $self = shift; |
250
|
|
|
|
|
|
|
$self->commit if $self->autocommit; |
251
|
|
|
|
|
|
|
}; |
252
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
*remove = \&delete; |
254
|
|
|
|
|
|
|
sub delete { |
255
|
|
|
|
|
|
|
my ( $self, $path ) = @_; |
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
$path = file( $self->_normalize_path($path) ); |
258
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
my $dir = $self->_cd_dir($path) or return; |
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
return delete $dir->{FILES}{$path->basename}; |
262
|
|
|
|
|
|
|
} |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
sub _cd_dir { |
265
|
|
|
|
|
|
|
my( $self, $path, $create ) = @_; |
266
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
my $dir = $self->root; |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
for ( grep { !/^\.$/ } $path->dir->dir_list ) { |
270
|
|
|
|
|
|
|
if ( $dir->{DIRS}{$_} ) { |
271
|
|
|
|
|
|
|
$dir = $dir->{DIRS}{$_}; |
272
|
|
|
|
|
|
|
} |
273
|
|
|
|
|
|
|
else { |
274
|
|
|
|
|
|
|
return unless $create; |
275
|
|
|
|
|
|
|
$dir = $dir->{DIRS}{$_} = { DIRS => {}, FILES => {} }; |
276
|
|
|
|
|
|
|
} |
277
|
|
|
|
|
|
|
} |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
return $dir; |
280
|
|
|
|
|
|
|
} |
281
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
sub _build_new_directory_entry { |
283
|
|
|
|
|
|
|
my( $self, $dir ) = @_; |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
my @children; |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
while ( my( $filename, $sha1 ) = each %{ $dir->{FILES} } ) { |
288
|
|
|
|
|
|
|
push @children, |
289
|
|
|
|
|
|
|
Git::PurePerl::NewDirectoryEntry->new( |
290
|
|
|
|
|
|
|
mode => '100644', |
291
|
|
|
|
|
|
|
filename => $filename, |
292
|
|
|
|
|
|
|
sha1 => $sha1, |
293
|
|
|
|
|
|
|
); |
294
|
|
|
|
|
|
|
} |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
while ( my( $dirname, $dir ) = each %{ $dir->{DIRS} } ) { |
297
|
|
|
|
|
|
|
my $tree = $self->_build_new_directory_entry($dir); |
298
|
|
|
|
|
|
|
push @children, Git::PurePerl::NewDirectoryEntry->new( |
299
|
|
|
|
|
|
|
mode => '040000', |
300
|
|
|
|
|
|
|
filename => $dirname, |
301
|
|
|
|
|
|
|
sha1 => $tree->sha1, |
302
|
|
|
|
|
|
|
); |
303
|
|
|
|
|
|
|
} |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
my $tree = Git::PurePerl::NewObject::Tree->new( |
306
|
|
|
|
|
|
|
directory_entries => \@children, |
307
|
|
|
|
|
|
|
); |
308
|
|
|
|
|
|
|
$self->git->put_object($tree); |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
return $tree; |
311
|
|
|
|
|
|
|
} |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
sub commit { |
314
|
|
|
|
|
|
|
my ( $self, $message ) = @_; |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
unless ( $self->_clean_directories ) { |
317
|
|
|
|
|
|
|
# TODO surely there's a better way? |
318
|
|
|
|
|
|
|
$self->set( '.gitignore/dummy', 'dummy file to keep git happy' ); |
319
|
|
|
|
|
|
|
} |
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
# TODO only commit if there were changes |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
my $tree = $self->_build_new_directory_entry( $self->root ); |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
# there might not be a parent, if it's a new branch |
326
|
|
|
|
|
|
|
my $parent = eval { $self->git->ref( 'refs/heads/'.$self->branch )->sha1 }; |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
my $timestamp = DateTime->now; |
329
|
|
|
|
|
|
|
my $commit = Git::PurePerl::NewObject::Commit->new( |
330
|
|
|
|
|
|
|
( parent => $parent ) x !!$parent, |
331
|
|
|
|
|
|
|
tree => $tree->sha1, |
332
|
|
|
|
|
|
|
author => $self->author, |
333
|
|
|
|
|
|
|
committer => $self->author, |
334
|
|
|
|
|
|
|
comment => $message||'', |
335
|
|
|
|
|
|
|
authored_time => $timestamp, |
336
|
|
|
|
|
|
|
committed_time => $timestamp, |
337
|
|
|
|
|
|
|
); |
338
|
|
|
|
|
|
|
$self->git->put_object($commit); |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
# reload |
341
|
|
|
|
|
|
|
$self->load; |
342
|
|
|
|
|
|
|
} |
343
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
sub discard { |
345
|
|
|
|
|
|
|
my $self = shift; |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
$self->load; |
348
|
|
|
|
|
|
|
} |
349
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
sub _find_file { |
351
|
|
|
|
|
|
|
my( $self, $tree, $path ) = @_; |
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
my @path = grep { !/^\.$/ } $path->dir->dir_list; |
354
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
if ( my $part = shift @path ) { |
356
|
|
|
|
|
|
|
my $entry = first { $_->filename eq $part } $tree->directory_entries |
357
|
|
|
|
|
|
|
or return; |
358
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
my $object = $self->git->get_object( $entry->sha1 ); |
360
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
return unless ref $object eq 'Git::PurePerl::Object::Tree'; |
362
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
return $self->_find_file( $object, file(@path,$path->basename) ); |
364
|
|
|
|
|
|
|
} |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
return first { $_->filename eq $path->basename } $tree->directory_entries; |
367
|
|
|
|
|
|
|
} |
368
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
sub history { |
370
|
|
|
|
|
|
|
my ( $self, $path ) = @_; |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
my $head = $self->branch_head |
373
|
|
|
|
|
|
|
or return; |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
my @q = ( $self->git->get_object($head) ); |
376
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
my @commits; |
378
|
|
|
|
|
|
|
while ( @q ) { |
379
|
|
|
|
|
|
|
push @q, $q[0]->parents; |
380
|
|
|
|
|
|
|
unshift @commits, shift @q; |
381
|
|
|
|
|
|
|
} |
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
my @history_commits; |
384
|
|
|
|
|
|
|
my %sha1_seen; |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
for my $c ( @commits ) { |
387
|
|
|
|
|
|
|
my $file = $self->_find_file( $c->tree, file($path) ) or next; |
388
|
|
|
|
|
|
|
push @history_commits, $c unless $sha1_seen{ $file->object->sha1 }++; |
389
|
|
|
|
|
|
|
} |
390
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
return map { |
392
|
|
|
|
|
|
|
GitStore::Revision->new( |
393
|
|
|
|
|
|
|
path => $path, |
394
|
|
|
|
|
|
|
gitstore => $self, |
395
|
|
|
|
|
|
|
sha1 => $_->sha1, |
396
|
|
|
|
|
|
|
) |
397
|
|
|
|
|
|
|
} @history_commits; |
398
|
|
|
|
|
|
|
|
399
|
|
|
|
|
|
|
} |
400
|
|
|
|
|
|
|
|
401
|
|
|
|
|
|
|
sub list { |
402
|
|
|
|
|
|
|
my( $self, $regex ) = @_; |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
croak "'$regex' is not a a regex" |
405
|
|
|
|
|
|
|
if $regex and ref $regex ne 'Regexp'; |
406
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
my $head = $self->branch_head or return; |
408
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
my $commit = $self->git->get_object($head); |
410
|
|
|
|
|
|
|
my $tree = $commit->tree; |
411
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
my $root = $self->_expand_directories( $tree ); |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
my @dirs = ( [ '', $root ] ); |
415
|
|
|
|
|
|
|
my @entries; |
416
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
while( my $dir = shift @dirs ) { |
418
|
|
|
|
|
|
|
my $path = $dir->[0]; |
419
|
|
|
|
|
|
|
$dir = $dir->[1]; |
420
|
|
|
|
|
|
|
push @dirs, [ "$path/$_" => $dir->{DIRS}{$_} ] |
421
|
|
|
|
|
|
|
for sort keys %{$dir->{DIRS}}; |
422
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
for ( sort keys %{$dir->{FILES}} ) { |
424
|
|
|
|
|
|
|
my $f = "$path/$_"; |
425
|
|
|
|
|
|
|
$f =~ s#^/##; # TODO improve this |
426
|
|
|
|
|
|
|
next if $regex and $f !~ $regex; |
427
|
|
|
|
|
|
|
push @entries, $f; |
428
|
|
|
|
|
|
|
} |
429
|
|
|
|
|
|
|
} |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
return @entries; |
432
|
|
|
|
|
|
|
} |
433
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
no Moose; |
436
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable; |
437
|
|
|
|
|
|
|
|
438
|
|
|
|
|
|
|
1; |
439
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
__END__ |
441
|
|
|
|
|
|
|
|
442
|
|
|
|
|
|
|
=pod |
443
|
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
=head1 NAME |
445
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
GitStore - Git as versioned data store in Perl |
447
|
|
|
|
|
|
|
|
448
|
|
|
|
|
|
|
=head1 VERSION |
449
|
|
|
|
|
|
|
|
450
|
|
|
|
|
|
|
version 0.15 |
451
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
=head1 SYNOPSIS |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
use GitStore; |
455
|
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
my $gs = GitStore->new('/path/to/repo'); |
457
|
|
|
|
|
|
|
$gs->set( 'users/obj.txt', $obj ); |
458
|
|
|
|
|
|
|
$gs->set( ['config', 'wiki.txt'], { hash_ref => 1 } ); |
459
|
|
|
|
|
|
|
$gs->commit(); |
460
|
|
|
|
|
|
|
$gs->set( 'yyy/xxx.log', 'Log me' ); |
461
|
|
|
|
|
|
|
$gs->discard(); |
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
# later or in another pl |
464
|
|
|
|
|
|
|
my $val = $gs->get( 'user/obj.txt' ); # $val is the same as $obj |
465
|
|
|
|
|
|
|
my $val = $gs->get( 'config/wiki.txt' ); # $val is { hashref => 1 } ); |
466
|
|
|
|
|
|
|
my $val = $gs->get( ['yyy', 'xxx.log' ] ); # $val is undef since discard |
467
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
=head1 DESCRIPTION |
469
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
It is inspired by the Python and Ruby binding. check SEE ALSO |
471
|
|
|
|
|
|
|
|
472
|
|
|
|
|
|
|
=head1 METHODS |
473
|
|
|
|
|
|
|
|
474
|
|
|
|
|
|
|
=head2 new |
475
|
|
|
|
|
|
|
|
476
|
|
|
|
|
|
|
GitStore->new('/path/to/repo'); |
477
|
|
|
|
|
|
|
GitStore->new( repo => '/path/to/repo', branch => 'mybranch' ); |
478
|
|
|
|
|
|
|
GitStore->new( repo => '/path/to/repo', author => 'Someone Unknown <unknown\@what.com>' ); |
479
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
=over 4 |
481
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
=item repo |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
your git directory or work directory (I<GitStore> will assume it's a work |
485
|
|
|
|
|
|
|
directory if it doesn't end with C<.git>). |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
=item branch |
488
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
your branch name, default is 'master' |
490
|
|
|
|
|
|
|
|
491
|
|
|
|
|
|
|
=item author |
492
|
|
|
|
|
|
|
|
493
|
|
|
|
|
|
|
It is used in the commit info |
494
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
=item serializer |
496
|
|
|
|
|
|
|
|
497
|
|
|
|
|
|
|
Can be used to define a serializing function that will be used if the value to |
498
|
|
|
|
|
|
|
save is a reference. When invoked, the function will be passed a reference to |
499
|
|
|
|
|
|
|
the store object, the path under which the value will be saved, and the value |
500
|
|
|
|
|
|
|
itself. For example, one could do different serialization via: |
501
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
my $store = GitStore->new( |
503
|
|
|
|
|
|
|
repo => '/path/to/repo', |
504
|
|
|
|
|
|
|
serializer => sub { |
505
|
|
|
|
|
|
|
my( $store, $path, $value ) = @_; |
506
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
if ( $path =~ m#^json# ) { |
508
|
|
|
|
|
|
|
return encode_json($value); |
509
|
|
|
|
|
|
|
} |
510
|
|
|
|
|
|
|
else { |
511
|
|
|
|
|
|
|
# defaults to YAML |
512
|
|
|
|
|
|
|
return YAML::Dump($value); |
513
|
|
|
|
|
|
|
} |
514
|
|
|
|
|
|
|
}, |
515
|
|
|
|
|
|
|
); |
516
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
The default serializer uses L<Storable/nfreeze>. |
518
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
=item deserializer |
520
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
Called when a value is picked from the store to be (potentially) deserialized. |
522
|
|
|
|
|
|
|
Just like the serializer function, it is passed three arguments: the store |
523
|
|
|
|
|
|
|
object, the path of the value to deserialize and the value itself. To revisit |
524
|
|
|
|
|
|
|
the example for C<serializer>, the full serializer/deserializer dance would |
525
|
|
|
|
|
|
|
be: |
526
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
my $store = GitStore->new( |
528
|
|
|
|
|
|
|
repo => '/path/to/repo', |
529
|
|
|
|
|
|
|
serializer => sub { |
530
|
|
|
|
|
|
|
my( $store, $path, $value ) = @_; |
531
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
return $path =~ m#^json# |
533
|
|
|
|
|
|
|
? encode_json($value) |
534
|
|
|
|
|
|
|
: YAML::Dump($value) |
535
|
|
|
|
|
|
|
; |
536
|
|
|
|
|
|
|
}, |
537
|
|
|
|
|
|
|
deserializer => sub { |
538
|
|
|
|
|
|
|
my( $store, $path, $value ) = @_; |
539
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
return $path =~ #^json# |
541
|
|
|
|
|
|
|
?decode_json($value) |
542
|
|
|
|
|
|
|
: YAML::Load($value) |
543
|
|
|
|
|
|
|
; |
544
|
|
|
|
|
|
|
}, |
545
|
|
|
|
|
|
|
); |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
The default deserializer will try to deserialize the value |
548
|
|
|
|
|
|
|
retrieved from the store via L<Storable/thaw> and, if this fails, |
549
|
|
|
|
|
|
|
return the value verbatim. |
550
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
=item autocommit |
552
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
If set to C<true>, any call to C<set()> or C<delete()> will automatically call an |
554
|
|
|
|
|
|
|
implicit C<commit()>. Defaults to C<false>. |
555
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
=back |
557
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
=head2 set($path, $val) |
559
|
|
|
|
|
|
|
|
560
|
|
|
|
|
|
|
$gs->set( 'yyy/xxx.log', 'Log me' ); |
561
|
|
|
|
|
|
|
$gs->set( ['config', 'wiki.txt'], { hash_ref => 1 } ); |
562
|
|
|
|
|
|
|
$gs->set( 'users/obj.txt', $obj ); |
563
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
Store $val as a $path file in Git |
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
$path can be String or ArrayRef. Any leading slashes ('/') in the path |
567
|
|
|
|
|
|
|
will be stripped, as to make it a valid Git path. The same |
568
|
|
|
|
|
|
|
grooming is done for the C<get()> and C<delete()> methods. |
569
|
|
|
|
|
|
|
|
570
|
|
|
|
|
|
|
$val can be String or Ref[HashRef|ArrayRef|Ref[Ref]] or blessed Object |
571
|
|
|
|
|
|
|
|
572
|
|
|
|
|
|
|
=head2 get($path) |
573
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
$gs->get( 'user/obj.txt' ); |
575
|
|
|
|
|
|
|
$gs->get( ['yyy', 'xxx.log' ] ); |
576
|
|
|
|
|
|
|
|
577
|
|
|
|
|
|
|
Get $val from the $path file |
578
|
|
|
|
|
|
|
|
579
|
|
|
|
|
|
|
$path can be String or ArrayRef |
580
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
=head2 get_revision( $path ) |
582
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
Like C<get()>, but returns the L<GitStore::Revision> object corresponding to |
584
|
|
|
|
|
|
|
the latest Git revision on the monitored branch for which C<$path> changed. |
585
|
|
|
|
|
|
|
|
586
|
|
|
|
|
|
|
=head2 delete($path) |
587
|
|
|
|
|
|
|
|
588
|
|
|
|
|
|
|
=head2 remove($path) |
589
|
|
|
|
|
|
|
|
590
|
|
|
|
|
|
|
remove $path from Git store |
591
|
|
|
|
|
|
|
|
592
|
|
|
|
|
|
|
=head2 commit |
593
|
|
|
|
|
|
|
|
594
|
|
|
|
|
|
|
$gs->commit(); |
595
|
|
|
|
|
|
|
$gs->commit('Your Comments Here'); |
596
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
commit the B<set> changes into Git |
598
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
=head2 discard |
600
|
|
|
|
|
|
|
|
601
|
|
|
|
|
|
|
$gs->discard(); |
602
|
|
|
|
|
|
|
|
603
|
|
|
|
|
|
|
discard the B<set> changes |
604
|
|
|
|
|
|
|
|
605
|
|
|
|
|
|
|
=head2 history($path) |
606
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
Returns a list of L<GitStore::Revision> objects representing the changes |
608
|
|
|
|
|
|
|
brought to the I<$path>. The changes are returned in ascending commit order. |
609
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
=head2 list($regex) |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
@entries = $gs->list( qr/\.txt$/ ); |
613
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
Returns a list of all entries in the repository, possibly filtered by the |
615
|
|
|
|
|
|
|
optional I<$regex>. |
616
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
=head1 FAQ |
618
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
=head2 why the files are B<not> there? |
620
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
run |
622
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
git checkout |
624
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
=head2 any example? |
626
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
# if you just need a local repo, that's all you need. |
628
|
|
|
|
|
|
|
mkdir sandbox |
629
|
|
|
|
|
|
|
cd sandbox |
630
|
|
|
|
|
|
|
git init |
631
|
|
|
|
|
|
|
# use GitStore->new('/path/to/this/sandbox') |
632
|
|
|
|
|
|
|
# set something |
633
|
|
|
|
|
|
|
git checkout |
634
|
|
|
|
|
|
|
|
635
|
|
|
|
|
|
|
# follows are for remote git url |
636
|
|
|
|
|
|
|
git remote add origin git@github.com:fayland/sandbox2.git |
637
|
|
|
|
|
|
|
git push origin master |
638
|
|
|
|
|
|
|
# do more GitStore->new('/path/to/this/sandbox') later |
639
|
|
|
|
|
|
|
git checkout |
640
|
|
|
|
|
|
|
git pull origin master |
641
|
|
|
|
|
|
|
git push |
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
=head1 KNOWN BUGS |
644
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
If all files are deleted from the repository, a 'dummy' file |
646
|
|
|
|
|
|
|
will be created to keep Git happy. |
647
|
|
|
|
|
|
|
|
648
|
|
|
|
|
|
|
=head1 SEE ALSO |
649
|
|
|
|
|
|
|
|
650
|
|
|
|
|
|
|
=over 4 |
651
|
|
|
|
|
|
|
|
652
|
|
|
|
|
|
|
=item Article |
653
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
L<http://www.newartisans.com/2008/05/using-git-as-a-versioned-data-store-in-python.html> |
655
|
|
|
|
|
|
|
|
656
|
|
|
|
|
|
|
=item Python binding |
657
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
L<http://github.com/jwiegley/git-issues/tree/master> |
659
|
|
|
|
|
|
|
|
660
|
|
|
|
|
|
|
=item Ruby binding |
661
|
|
|
|
|
|
|
|
662
|
|
|
|
|
|
|
L<http://github.com/georgi/git_store/tree/master> |
663
|
|
|
|
|
|
|
|
664
|
|
|
|
|
|
|
=back |
665
|
|
|
|
|
|
|
|
666
|
|
|
|
|
|
|
=head1 Git URL |
667
|
|
|
|
|
|
|
|
668
|
|
|
|
|
|
|
L<http://github.com/fayland/perl-git-store/tree/master> |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
=head1 AUTHORS |
671
|
|
|
|
|
|
|
|
672
|
|
|
|
|
|
|
=over 4 |
673
|
|
|
|
|
|
|
|
674
|
|
|
|
|
|
|
=item * |
675
|
|
|
|
|
|
|
|
676
|
|
|
|
|
|
|
Fayland Lam <fayland@gmail.com> |
677
|
|
|
|
|
|
|
|
678
|
|
|
|
|
|
|
=item * |
679
|
|
|
|
|
|
|
|
680
|
|
|
|
|
|
|
Yanick Champoux <yanick@cpan.org> |
681
|
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
=back |
683
|
|
|
|
|
|
|
|
684
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
685
|
|
|
|
|
|
|
|
686
|
|
|
|
|
|
|
This software is copyright (c) 2013 by Fayland Lam <fayland@gmail.com>. |
687
|
|
|
|
|
|
|
|
688
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
689
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
690
|
|
|
|
|
|
|
|
691
|
|
|
|
|
|
|
=cut |