File Coverage

blib/lib/Config/Versioned.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             ## Config::Versioned
2             ##
3             ## Written 2011-2012 by Scott T. Hardin for the OpenXPKI project
4             ## Copyright (C) 2010-2012 by The OpenXPKI Project
5             ##
6             ## Was based on the CPAN module App::Options, but the import() stuff
7             ## bit me so we're turning into a Moose.
8             ##
9             ## vim: syntax=perl
10              
11             package Config::Versioned;
12              
13 5     5   801917 use Moose;
  0            
  0            
14             use namespace::autoclean;
15              
16             =head1 NAME
17              
18             Config::Versioned - Simple, versioned access to configuration data
19              
20             =cut
21              
22             our $VERSION = '1.01';
23              
24             use Carp;
25             use Config::Std;
26             use Data::Dumper;
27             use DateTime;
28             use Git::PurePerl;
29             use Path::Class;
30              
31             has 'path' => ( is => 'ro', isa => 'ArrayRef', default => sub { [qw( . )] } );
32             has 'filename' => ( is => 'ro', isa => 'Str' );
33             has 'dbpath' =>
34             ( is => 'ro', default => 'cfgver.git', required => 1 );
35             has 'author_name' => ( is => 'ro', isa => 'Str', default => "process: $@" );
36             has 'author_mail' => (
37             is => 'ro',
38             isa => 'Str',
39             default => $ENV{GIT_AUTHOR_EMAIL} || $ENV{USER} . '@localhost'
40             );
41             has 'autocreate' => ( is => 'ro', isa => 'Bool', default => 0 );
42             has 'commit_time' => ( is => 'ro', isa => 'DateTime' );
43             has 'comment' => ( is => 'rw', isa => 'Str' );
44             has 'delimiter' => ( is => 'ro', isa => 'Str', default => '.' );
45             has 'delimiter_regex' =>
46             ( is => 'ro', isa => 'RegexpRef', default => sub { qr{ \. }xms } );
47             has 'log_get_callback' => ( is => 'ro' );
48             has '_git' => ( is => 'rw' );
49             has 'debug' => ( is => 'rw', isa => 'Int', default => 0 );
50              
51             # a reference to the singleton Config::Versioned object that parsed the command line
52             #my ($default_option_processor);
53              
54             #my (%path_is_secure);
55              
56             =head1 SYNOPSIS
57              
58             use Config::Versioned;
59              
60             my $cfg = Config::Versioned->new();
61             my $param1 = $cfg->get('subsystem1.group.param1');
62             my $old1 = $cfg->get('subsystem1.group.param1', $version);
63             my @keys = $cfg->list('subsys1.db');
64              
65              
66             =head1 DESCRIPTION
67              
68             Config::Versioned allows an application to access configuration parameters
69             not only by parameter name, but also by version number. This allows for
70             the configuration subsystem to store previous versions of the configuration
71             parameters. When requesting the value for a specific attribute, the programmer
72             specifies whether to fetch the most recent value or a previous value.
73              
74             This is useful for long-running tasks such as in a workflow-based application
75             where task-specific values (e.g.: profiles) are static over the life of a
76             workflow, while global values (e.g.: name of an LDAP server to be queried)
77             should always be the most recent.
78              
79             Config::Versioned handles the versions by storing the configuration data
80             in an internal Git repository. Each import of configuration files into
81             the repository is documented with a commit. When a value is fetched, it is
82             this commit that is referenced directly when specifying the version.
83              
84             The access to the individual attributes is via a named-parameter scheme, where
85             the key is a dot-separated string.
86              
87             Currently, C<Config::Std> is used for the import of the data files into the
88             internal Git repository. Support for other configuration modules (e.g.:
89             C<Config::Any>) is planned.
90              
91             =head1 METHODS
92              
93             =head2 init()
94              
95             This is invoked automatically via import(). It is called when running the
96             following code:
97              
98             use Config::Versioned;
99              
100             The init() method reads the configuration data from the configuration files
101             and populates an internal data structure.
102              
103             Optionally, parameters may be passed to init(). The following
104             named-parameters are supported:
105              
106             =over 8
107              
108             =item path
109              
110             Specifies an anonymous array contianing the names of the directories to
111             check for the configuration files.
112              
113             path => qw( /etc/yourapp/etc /etc/yourapp/local/etc . ),
114              
115             The default path is just the current directory.
116              
117             =item filename
118              
119             Specifies the name of the configuration file to be found in the given path.
120              
121             filename => qw( yourapp.conf ),
122              
123             If no filename is given, no new configuration data will be imported and
124             the internal git repository will be used.
125              
126             =item dbpath
127              
128             The directory for the internal git repository that stores the config.
129              
130             dbpath => qw( config.git ),
131              
132             The default is "cfgver.git".
133              
134             =item author_name, author_mail
135              
136             The name and e-mail address to use in the internal git repository for
137             commits.
138              
139             =item autocreate
140              
141             If no internal git repository exists, it will be created during code
142             initialization. Note that if an import filename is specified, this
143             automatically sets autocreate to true.
144              
145             autocreate => 1,
146              
147             The default is "0".
148              
149             Note: this option might become deprecated. I just wanted some extra
150             "insurance" during the early stages of development.
151              
152             =item commit_time
153              
154             This sets the time to use for the commits in the internal git repository.
155             It is used for debugging purposes only!
156              
157             Note: this must be a DateTime object instance.
158              
159             =item delimiter
160              
161             Specifies the delimiter used to separate the different levels in the
162             string used to designate the location of a configuration parameter. [Default: '.']
163              
164             =item delimiter_regex
165              
166             Specifies the delimiter used to separate the different levels in the
167             string used to designate the location of a configuration parameter.
168             [Default: qr/ \. /xms]
169              
170             =item log_get_callback
171              
172             Specifies a callback function to be called by get() after fetching
173             the value for the given key. The subroutine should accept the
174             parameters LOCATION, VERSION, VALUE. The VALUE may either be a single
175             scalar value or an array reference containing a list of values.
176              
177             sub cb_log_get {
178             my $self = shift;
179             my $loc = shift;
180             my $ver = shift;
181             my $val = shift;
182              
183             warn "Access config parameter: $loc ($ver) => ",
184             ref($val) eq 'ARRAY'
185             ? join(', ', @{ $val })
186             : $val,
187             "\n";
188             }
189             my $cfg = Config::Versioned->new( { log_get_callback => 'cb_log_get' } );
190              
191             Note: if log_get_callback is a code ref, it will be called as a function.
192             Otherwise, the log_get_callback will specify a method name that is to be
193             called on the current object instance.
194              
195             =back
196              
197             =head2 BUILD( { PARAMS } )
198              
199             NOTE: This is used internally, so the typical user shouldn't bother with this.
200              
201             This is called after an object is created. When cloning, it is important that
202             the new instance gets a reference to the same Git::PurePerl instance. This
203             will prevent two instances from getting out of sync if modifications are made
204             to the configuration data at runtime. To handle this, the parameter 'GITREF'
205             must be passed when cloning.
206              
207             Note 2: this should be handled automatically in the _near_ future.
208              
209             my $cv2 = $cv1->new( GITREF => $cv1->_git() );
210              
211             =cut
212              
213             sub BUILD {
214             my $self = shift;
215             my $args = shift;
216              
217             if ( defined $ENV{CONFIG_VERSIONED_DEBUG} ) {
218             $self->debug( $ENV{CONFIG_VERSIONED_DEBUG} );
219             }
220              
221             if ( not $self->_init_repo() ) {
222             return;
223             }
224             # if ( not $self->_git() ) {
225             # if ( $args->{GITREF} ) {
226             # $self->_git( $args->{GITREF} );
227             # }
228             # else {
229             # if ( not $self->_init_repo() ) {
230             # return;
231             # }
232             # }
233             # }
234             #
235             # $self->parser($args);
236              
237             return ($self);
238             }
239              
240             =head2 get( LOCATION [, VERSION ] )
241              
242             This is the accessor for fetching the value(s) of the given parameter. The
243             value may either be zero or more elements.
244              
245             In list context, the values are returned. In scalar context, C<undef> is
246             returned if the variable is empty. Otherwise, the first element is returned.
247              
248             Optionally, a VERSION may be specified to return the value for that
249             specific version.
250              
251             =cut
252              
253             sub get {
254             my $self = shift;
255             my $location = shift;
256             my $version = shift;
257             my $cb = $self->log_get_callback();
258             my ( $obj, $deobj ) = $self->_findobjx( $location, $version );
259              
260             if ( not defined $obj ) {
261             $self->$cb( $location, $version, '<undefined>' ) if $cb;
262             return;
263             }
264              
265             if ( $obj->kind eq 'blob' ) {
266             $self->$cb( $location, $version, $obj->content ) if $cb;
267             if ( $deobj->mode() == 120000 ) {
268             my $tmp = $obj->content;
269             return \$tmp;
270             }
271             else {
272             return $obj->content;
273             }
274             }
275             elsif ( $obj->kind eq 'tree' ) {
276             my @entries = $obj->directory_entries;
277             my @ret = ();
278             foreach my $de (@entries) {
279             push @ret, $de->filename;
280             }
281             my @sorted =
282             sort { ( $a =~ /^\d+$/ and $b =~ /^\d+$/ ) ? $a <=> $b : $a cmp $b }
283             @ret;
284             $self->$cb( $location, $version, \@sorted ) if $cb;
285             return @sorted;
286             }
287             else {
288             $self->$cb( $location, $version,
289             "<error: non-blob object '" . $obj->kind . "' not supported>" )
290             if $cb;
291             warn "# DEBUG: get() was asked to return a non-blob object [kind=",
292             $obj->kind, "]\n" if $self->debug();
293             return;
294             }
295             }
296              
297             =head2 kind ( LOCATION [, VERSION ] )
298              
299             The get() method tries to return a scalar when the location corresponds
300             to a single value and a list when the location has child nodes. Sometimes,
301             however, it is helpful to have a definitive answer on what a location
302             contains.
303              
304             The kind() method returns the object type that the given location accesses.
305             This can be one of the following values:
306              
307             =over
308              
309             =item tree
310              
311             The given location contains a tree object containing zero or more child
312             objects. The get() method will return a list of the entry names.
313              
314             =item blob
315              
316             The data node that usually contains a scalar value, but in future implementations
317             may contain other encoded data.
318              
319             =back
320              
321             B<Note:> As a side-effect, this can be used to test whether the given location
322             exists at all in the configuration. If not found, C<undef> is returned.
323              
324             =cut
325              
326             sub kind {
327             my $self = shift;
328             my $location = shift;
329             my $version = shift;
330              
331             my $obj = $self->_findobj( $location, $version );
332              
333             if ( not defined $obj ) {
334             return; # if nothing found, just return undef
335             }
336              
337             if ( $obj->kind eq 'blob' ) {
338             return 'blob';
339             }
340             elsif ( $obj->kind eq 'tree' ) {
341             return 'tree';
342             }
343             else {
344             $@ = "Internal object error (expected tree or blob): [gpp kind="
345             . $obj->kind . "]\n";
346             warn "# DEBUG: " . $@ if $self->debug();
347             return;
348             }
349              
350             }
351              
352             =head2 listattr( LOCATION [, VERSION ] )
353              
354             This fetches a list of the parameters available for a given location in the
355             configuration tree.
356              
357             =cut
358              
359             sub listattr {
360             my $self = shift;
361             my $location = shift;
362             my $version = shift;
363              
364             my $obj = $self->_findobj( $location, $version );
365             if ( $obj and $obj->kind eq 'tree' ) {
366             my @entries = $obj->directory_entries;
367             my @ret = ();
368             foreach my $de (@entries) {
369             push @ret, $de->filename;
370             }
371             return @ret;
372             }
373             else {
374             $@ = "obj at $location not found";
375             return;
376             }
377             }
378              
379             =head2 dumptree( [ VERSION ] )
380              
381             This fetches the entire tree for the given version (default: newest version)
382             and returns a hashref to a named-parameter list.
383              
384             =cut
385              
386             sub dumptree {
387             my $self = shift;
388             my $version = shift;
389             my $cfg = $self->_git();
390              
391             # If no version hash was given, default to the HEAD of master
392              
393             if ( not $version ) {
394             my $master = $self->_git()->ref('refs/heads/master');
395             if ( $master ) {
396             $version = $master->sha1;
397             } else {
398             # if no sha1s are in repo, there's nothing to return
399             return;
400             }
401             }
402              
403             my $obj = $cfg->get_object($version);
404             if ( not $obj ) {
405             $@ = "No object found for SHA1 " . $version ? $version : '';
406             return;
407             }
408              
409             if ( $obj->kind eq 'commit' ) {
410             $obj = $obj->tree;
411             }
412              
413             my $ret = {};
414              
415             my @directory_entries = $obj->directory_entries;
416              
417             foreach my $de (@directory_entries) {
418             my $child = $cfg->get_object( $de->sha1 );
419              
420             # warn "DEBUG: dump - child name = ", $de->filename, "\n";
421             # warn "DEBUG: dump - child kind = ", $child->kind, "\n";
422              
423             if ( $child->kind eq 'tree' ) {
424             my $subret = $self->dumptree( $de->sha1 );
425             foreach my $key ( keys %{$subret} ) {
426             $ret->{ $de->filename . $self->delimiter() . $key } =
427             $subret->{$key};
428             }
429             }
430             elsif ( $child->kind eq 'blob' ) {
431             $ret->{ $de->filename } = $child->content;
432             }
433             else {
434             die "ERROR: unexpected kind: ", $child->kind, "\n";
435             }
436              
437             }
438             return $ret;
439             }
440              
441             =head2 version
442              
443             This returns the current version of the configuration database, which
444             happens to be the SHA1 hash of the HEAD of the internal git repository.
445              
446             Optionally, a version hash may be passed and version() will return a true
447             value if it is found.
448              
449             =cut
450              
451             sub version {
452             my $self = shift;
453             my $version = shift;
454             my $cfg = $self->_git();
455              
456             if ($version) {
457             my $obj = $cfg->get_object($version);
458             if ( $obj and $obj->sha1 eq $version ) {
459             return $version;
460             }
461             else {
462             return;
463             }
464             }
465             else {
466             my $head = $cfg->head;
467             return $head->sha1;
468             }
469             }
470              
471             =head1 INTERNALS
472              
473             =head2 _init_repo
474              
475             Initializes the internal git repository used for storing the config
476             values.
477              
478             If the I<objects> directory in the C<dbpath> does not exist, an
479             C<init()> on the C<Git::PurePerl> class is run. Otherwise, the
480             instance is initialized using the existing bare repository.
481              
482             On error, it returns C<undef> and the reason is in C<$@>.
483              
484             =cut
485              
486             sub _init_repo {
487             my $self = shift;
488              
489             my $git;
490              
491             # if ( not $init_args->{dbpath} ) {
492             # die "ERROR: dbpath not set";
493             # }
494              
495             if ( not -d $self->dbpath() . '/objects' ) {
496             if ( $self->filename() || $self->autocreate() ) {
497             if ( not -d $self->dbpath() ) {
498             if ( not dir( $self->dbpath() )->mkpath ) {
499             die 'Error creating directory ' . $self->dbpath() . ': ' . $!;
500             }
501             }
502             $git = Git::PurePerl->init( gitdir => $self->dbpath() );
503             } else {
504             die 'Error: dbpath (' . $self->dbpath() . ') does not exist';
505             }
506             }
507             else {
508             $git = Git::PurePerl->new( gitdir => $self->dbpath() );
509             }
510             $self->_git($git);
511             $self->parser();
512             return $self;
513             }
514              
515             =head2 _get_anon_scalar
516              
517             Creates an anonymous scalar for representing symlinks in the tree structure.
518              
519             =cut
520              
521             sub _get_anon_scalar {
522             my $temp = shift;
523             return \$temp;
524             }
525              
526             =head2 parser ARGS
527              
528             Imports the configuration read and writes it to the internal database. If no
529             filename is passed as an argument, then it will quietly skip the commit.
530              
531             Note: if you override this method in a child class, it must create an
532             anonymous hash tree and pass the reference to the commit() method. Here
533             is a simple example:
534              
535             sub parser {
536             my $self = shift;
537             my $args = shift;
538             $args->{comment} = 'import from my perl hash';
539            
540             my $cfg = {
541             group1 => {
542             subgroup1 => {
543             param1 => 'val1',
544             param2 => 'val2',
545             },
546             },
547             group2 => {
548             subgroup1 => {
549             param3 => 'val3',
550             param4 => 'val4',
551             },
552             },
553             # This creates a symlink from 'group3.subgroup3' to 'connector1/group4'.
554             # Note the use of the scalar reference using the backslash.
555             group3 => {
556             subgroup3 => \'connector1/group4',
557             },
558              
559             };
560            
561             # pass original args, appended with a comment string for the commit
562             $self->commit( $cfg, $args );
563             }
564              
565             In the comment, you should include details on where the config came from
566             (i.e.: the filename or directory).
567              
568             =cut
569              
570             sub parser {
571             my $self = shift;
572             my $args = shift;
573              
574             foreach
575             my $key (qw( comment filename path author_name author_mail commit_time ))
576             {
577             if ( not exists $args->{$key} ) {
578             $args->{$key} = $self->$key();
579             }
580             }
581              
582             # If no filename was specified, then there is no import of
583             # configuration files needed. Quietly exit method.
584              
585             if ( not $args->{filename} ) {
586             return $self;
587             }
588              
589             # Read the configuration from the import files
590              
591             my %cfg = ();
592             $self->_read_config_path( $args->{filename}, \%cfg, @{ $args->{path} } );
593              
594             $args->{comment} ||= "Import config from "
595             . $self->_which( $args->{filename}, @{ $args->{path} } );
596              
597             # convert the foreign data structure to a simple hash tree,
598             # where the value is either a scalar or a hash reference.
599              
600             my $tmphash = {};
601             foreach my $sect ( keys %cfg ) {
602              
603             # build up the underlying branch for these leaves
604              
605             my @sectpath = split( $self->delimiter_regex(), $sect );
606             my $sectref = $tmphash;
607             foreach my $nodename (@sectpath) {
608             $sectref->{$nodename} ||= {};
609             $sectref = $sectref->{$nodename};
610             }
611              
612             # now add the leaves
613              
614             foreach my $leaf ( keys %{ $cfg{$sect} } ) {
615              
616             # If the leaf start or ends with an '@', treat it as
617             # a symbolic link.
618             if ( $leaf =~
619             m{ (?: \A @ (.*?) @ \z | \A @ (.*) | (.*?) @ \z ) }xms )
620             {
621             my $match = $1 || $2 || $3;
622              
623             # make it a ref to an anonymous scalar so we know it's a symlink
624             #my $t = _get_anon_scalar($1);
625             $sectref->{$match} = \( $cfg{$sect}{$leaf} );
626             }
627             else {
628             $sectref->{$leaf} = $cfg{$sect}{$leaf};
629             }
630             }
631              
632             }
633              
634             $self->commit( $tmphash, $args );
635             }
636              
637             =head2 commit CFGHASH[, ARGS]
638              
639             Import the configuration tree in the CFGHASH anonymous hash and commit
640             the modifications to the internal git bare repository.
641              
642             ARGS is a ref to a named-parameter list (e.g. HASH) that may contain the
643             following keys to override the instance defaults:
644              
645             author_name, author_mail, comment, commit_time
646              
647             =cut
648              
649             sub commit {
650             my $self = shift;
651             my $hash = shift;
652             my $args = shift;
653              
654             if ( ref($hash) ne 'HASH' ) {
655             confess "ERR: commit() - arg not hash ref [$hash]";
656             }
657              
658             my $parent = undef;
659             my $master = undef;
660              
661             $master = $self->_git()->ref('refs/heads/master');
662             if ( $master ) {
663             $parent = $master->sha1;
664             }
665              
666             # warn "# author_name: ", $self->author_name(), "\n";
667             my $tree = $self->_hash2tree($hash);
668              
669             if ( $self->debug() ) {
670             print join( "\n# ", '', $self->_debugtree($tree) ), "\n";
671             }
672              
673             #
674             # Now that we have a "staging" tree, compare its hash with
675             # that of the current top-level tree. If they are the same,
676             # there were no changes made to the config and we should
677             # not create a commit object
678             #
679              
680             if ( $parent and $master->tree->sha1 eq $tree->sha1 ) {
681             if ( $self->debug() ) {
682             carp("Nothing to commit (index matches HEAD)");
683             }
684             return $self;
685             }
686              
687             #
688             # Prepare and execute the commit
689             #
690              
691             my $actor = Git::PurePerl::Actor->new(
692             name => $args->{author_name} || $self->author_name,
693             email => $args->{author_mail} || $self->author_mail,
694             );
695              
696             my $time = $args->{commit_time} || $self->commit_time || DateTime->now;
697              
698             my @commit_attrs = (
699             tree => $tree->sha1,
700             author => $actor,
701             authored_time => $time,
702             committer => $actor,
703             committed_time => $time,
704             comment => $args->{comment} || $self->comment(),
705             );
706             if ($parent) {
707             push @commit_attrs, parent => $parent;
708             }
709              
710             my $commit = Git::PurePerl::NewObject::Commit->new(@commit_attrs);
711             $self->_git()->put_object($commit);
712              
713             }
714              
715             sub _hash2tree {
716             my $self = shift;
717             my $hash = shift;
718              
719             if ( ref($hash) ne 'HASH' ) {
720             confess "ERR: _hash2tree() - arg not hash ref [$hash]";
721             }
722             if ( $self->debug() ) {
723             warn "Entered _hash2tree( $hash ): ", join( ', ', %{$hash} ), "\n";
724             }
725              
726             my @dir_entries = ();
727              
728             foreach my $key ( keys %{$hash} ) {
729             if ( $self->debug() ) {
730             warn "# _hash2tree() processing $key -> ", $hash->{$key}, "\n";
731             }
732             if ( ref( $hash->{$key} ) eq 'HASH' ) {
733             if ( $self->debug() ) {
734             warn "# _hash2tree() adding subtree for $key\n";
735             }
736             my $subtree = $self->_hash2tree( $hash->{$key} );
737            
738             next unless($subtree);
739              
740             my $local_key = $key;
741             if ( $] > 5.007 && utf8::is_utf8($local_key) ) {
742             utf8::downgrade($local_key);
743             }
744              
745             my $de = Git::PurePerl::NewDirectoryEntry->new(
746             mode => '40000',
747             filename => $local_key,
748             sha1 => $subtree->sha1(),
749             );
750             push @dir_entries, $de;
751             }
752             elsif ( ref( $hash->{$key} ) eq 'SCALAR' ) {
753              
754             # Support for symbolic links
755             if ( $self->debug() ) {
756             warn "# _hash2tree() adding symlink for $key\n";
757             }
758             my $obj =
759             Git::PurePerl::NewObject::Blob->new(
760             content => ${ $hash->{$key} } );
761             $self->_git()->put_object($obj);
762             my $local_key = $key;
763             if ( $] > 5.007 && utf8::is_utf8($local_key) ) {
764             utf8::downgrade($local_key);
765             }
766             my $de = Git::PurePerl::NewDirectoryEntry->new(
767             mode => '120000', # symlink
768             filename => $local_key,
769             sha1 => $obj->sha1(),
770             );
771             push @dir_entries, $de;
772             }
773             elsif ( defined $hash->{$key} ) {
774             my $obj =
775             Git::PurePerl::NewObject::Blob->new( content => $hash->{$key} );
776              
777             my $local_key = $key;
778             if ( $] > 5.007 && utf8::is_utf8($local_key) ) {
779             utf8::downgrade($local_key);
780             }
781              
782             warn "# created blob for '$key' with sha " . $obj->sha1() if $self->debug();
783             warn "# '$key' utf8 flag: ", utf8::is_utf8($key) if $self->debug();
784             $self->_git()->put_object($obj);
785             my $de = Git::PurePerl::NewDirectoryEntry->new(
786             mode => '100644', # plain file
787             filename => $local_key,
788             sha1 => $obj->sha1(),
789             );
790             push @dir_entries, $de;
791             } else {
792             warn "# _hash2tree() value is undef for key $key\n" if $self->debug();
793             }
794             }
795              
796             if (!scalar @dir_entries) {
797             warn "# _hash2tree() nothing to push\n" if $self->debug();;
798             return undef;
799             }
800              
801             my $tree =
802             Git::PurePerl::NewObject::Tree->new( directory_entries =>
803             [ sort { $a->filename cmp $b->filename } @dir_entries ] );
804              
805             if ( $self->debug() ) {
806             my $content = $tree->content;
807             $content =~ s/(.)/sprintf("%x",ord($1))/eg;
808             warn "# Added tree with dir entries: ",
809             join( ', ', map { $_->filename } @dir_entries ), "\n";
810             warn "# content: ", $content, "\n";
811             warn "# size: ", $tree->size, "\n";
812             warn "# kind: ", $tree->kind, "\n";
813             warn "# sha1: ", $tree->sha1, "\n";
814              
815             }
816              
817             $self->_git()->put_object($tree);
818              
819             return $tree;
820             }
821              
822             =head2 _mknode LOCATION
823              
824             Creates a node at the given LOCATION, creating parent nodes if necessary.
825              
826             A reference to the node at the LOCATION is returned.
827              
828             =cut
829              
830             sub _mknode {
831             my $self = shift;
832             my $location = shift;
833             my $ref = $self->_git();
834             foreach my $key ( split( $self->delimiter_regex(), $location ) ) {
835             if ( not exists $ref->{$key} ) {
836             $ref->{$key} = {};
837             }
838             elsif ( ref( $ref->{$key} ) ne 'HASH' ) {
839              
840             # TODO: fix this ugly error to something more appropriate
841             die "Location at $key in $location already assigned to non-HASH";
842             }
843             $ref = $ref->{$key};
844             }
845             return $ref;
846             }
847              
848             =head2 _findobjx LOCATION [, VERSION ]
849              
850             Returns the Git::PurePerl and Git::PurePerl::DirectoryEntry objects found in
851             the file path at LOCATION.
852              
853             my ($ref1, $de1) = $cfg->_findnode("smartcard.ldap.uri");
854             my $ref2, $de2) = $cfg->_findnode("certs.signature.duration", $wfcfgver);
855              
856             In most cases, the C<_findobj> version is sufficient. This extended version
857             is used to look at the attribtes of the directory entry for things like whether
858             the blob is a symlink.
859              
860             =cut
861              
862             sub _findobjx {
863             my $self = shift;
864             my $location = shift;
865             my $ver = shift;
866             my $cfg = $self->_git();
867             my ( $obj, $deobj );
868              
869             # If no version hash was given, default to the HEAD of master
870              
871             if ( not $ver ) {
872             my $master = $self->_git()->ref('refs/heads/master');
873             if ( $master ) {
874             $ver = $master->sha1;
875             } else {
876             # if no sha1s are in repo, there's nothing to return
877             return;
878             }
879              
880             }
881              
882             # TODO: is this the way we want to handle the error of not finding
883             # the given object?
884              
885             $obj = $cfg->get_object($ver);
886             if ( not $obj ) {
887             $@ = "No object found for SHA1 $ver";
888             return;
889             }
890              
891             if ( $obj->kind eq 'commit' ) {
892             $obj = $obj->tree;
893             }
894             my @keys = split $self->delimiter_regex(), $location;
895              
896             # iterate thru the levels in the location
897              
898             while (@keys) {
899             my $key = shift @keys;
900              
901             # if the object is a blob, we already reached the leaf
902             if ($obj->kind eq 'blob') {
903             return undef;
904             }
905              
906             # $obj should contain the parent tree object.
907              
908             my @directory_entries = $obj->directory_entries;
909              
910             # find the corresponding child object
911              
912             my $found = 0;
913             foreach my $de (@directory_entries) {
914             if ( $de->filename eq $key ) {
915             $found++;
916             $obj = $cfg->get_object( $de->sha1 );
917             $deobj = $de;
918             last;
919             }
920             }
921              
922             if ( not $found ) {
923             return;
924             }
925             }
926             return $obj, $deobj;
927              
928             }
929              
930             =head2 _findobj LOCATION [, VERSION ]
931              
932             Returns the Git::PurePerl object found in the file path at LOCATION.
933              
934             my $ref1 = $cfg->_findnode("smartcard.ldap.uri");
935             my $ref2 = $cfg->_findnode("certs.signature.duration", $wfcfgver);
936              
937             =cut
938              
939             sub _findobj {
940             my $self = shift;
941             my ( $obj, $deobj ) = $self->_findobjx(@_);
942             if ( defined $obj ) {
943             return $obj;
944             }
945             else {
946             return;
947             }
948             }
949              
950             =head2 _get_sect_key LOCATION
951              
952             Returns the section and key needed by Config::Std to access the
953             configuration values. The given LOCATION is split on the last delimiter.
954             The resulting section and key are returned as a list.
955              
956             =cut
957              
958             sub _get_sect_key {
959             my $self = shift;
960             my $key = shift;
961              
962             # Config::Std uses section/key, so we need to split up the
963             # given key
964              
965             my @tokens = split( $self->delimiter_regex(), $key );
966             $key = pop @tokens;
967             my $sect = join( $self->delimiter(), @tokens );
968              
969             return $sect, $key;
970             }
971              
972             =head2 _which( NAME, DIR ... )
973              
974             Searches the directory list DIR, returning the full path in which the file NAME was
975             found.
976              
977             =cut
978              
979             sub _which {
980             my $self = shift;
981             my $name = shift;
982             my @dirs = @_;
983              
984             foreach (@dirs) {
985             my $path = $_ . '/' . $name;
986             if ( -f $path ) {
987             return $path;
988             }
989             }
990             return;
991             }
992              
993             =head2 _read_config_path SELF, FILENAME, CFGREF, PATH
994              
995             Searches for FILENAME in the given directories in PATH. When found,
996             the file is parsed and a data structure is written to the location
997             in CFGREF.
998              
999             Note: this is the wrapper around the underlying libs that read the
1000             configuration data from the files.
1001              
1002             =cut
1003              
1004             sub _read_config_path {
1005             my $self = shift;
1006             my $cfgname = shift;
1007             my $cfgref = shift;
1008              
1009             my $cfgfile = $self->_which( $cfgname, @_ );
1010             if ( not $cfgfile ) {
1011             die "ERROR: couldn't find $cfgname in ", join( ', ', @_ );
1012             }
1013              
1014             read_config( $cfgfile => %{$cfgref} );
1015             }
1016              
1017             =head2 _debugtree( OBJREF | SHA1 )
1018              
1019             This fetches the entire tree for the given SHA1 and dumps it in a
1020             human-readable format.
1021              
1022             =cut
1023              
1024             sub _debugtree {
1025             my $self = shift;
1026             my $start = shift;
1027             my $indent = shift || 0;
1028             my $cfg = $self->_git();
1029             my @out = ();
1030              
1031             my $tabsize = 2;
1032             my $obj;
1033              
1034             # Soooo, let's see what we've been fed...
1035             if ( not $start ) { # default to the HEAD of master
1036             my $master = $cfg->ref('refs/heads/master');
1037             if ( $master ) {
1038             $obj = $cfg->get_object( $master->sha1 );
1039             }
1040             else {
1041             push @out, "NO SHA1s IN TREE";
1042             return @out; # if no sha1s are in repo, there's nothing to return
1043             }
1044              
1045             }
1046             elsif ( not ref($start) ) { # possibly a sha1
1047             $obj = $cfg->get_object($start);
1048             if ( not $obj ) {
1049             $@ = "No object found for SHA1 " . $start ? $start : '';
1050             return $@;
1051             }
1052             }
1053             elsif ( ref($start) =~ /^(REF|SCALAR|ARRAY|HASH|CODE|GLOB)$/ ) {
1054             croak( "_debugtree doesn't support ref type " . ref($start) );
1055             }
1056             else {
1057             $obj = $start;
1058             }
1059              
1060             # At this point, we should have a Git::PurePerl (new) Object.
1061             # Let's double-check.
1062              
1063             if ( $obj->can('kind') ) {
1064              
1065             # push @out, ( ' ' x ( $tabsize * $indent ) ) . ('=' x 40);
1066             #foreach my $attr (qw( kind size content sha1 git )) {
1067             foreach my $attr (qw( kind size sha1 )) {
1068             if ( $obj->can($attr) ) {
1069             push @out,
1070             ( ' ' x ( $tabsize * $indent ) ) . $attr . ': ' . $obj->$attr;
1071             }
1072             }
1073             }
1074             elsif ($obj->isa('Git::PurePerl::NewDirectoryEntry')
1075             or $obj->isa('Git::PurePerl::DirectoryEntry') )
1076             {
1077             foreach my $attr (qw( mode filename sha1 )) {
1078             if ( $obj->can($attr) ) {
1079             push @out,
1080             ( ' ' x ( $tabsize * $indent ) ) . $attr . ': ' . $obj->$attr;
1081             }
1082             }
1083             push @out, $self->_debugtree( $obj->sha1, $indent + 1 );
1084             return @out;
1085             }
1086             else {
1087             die "Obj $obj doesn't seem to be supported";
1088             }
1089              
1090             if ( $obj->kind eq 'commit' ) {
1091             foreach my $attr (
1092             qw( tree_sha1 parent_sha1s author authored_time committer
1093             commited_time comment encoding )
1094             )
1095             {
1096             if ( $obj->can($attr) ) {
1097             push @out,
1098             ( ' ' x ( $tabsize * $indent ) ) . $attr . ': ' . $obj->$attr;
1099             }
1100             }
1101             push @out, $self->_debugtree( $obj->tree, $indent + 1 );
1102             }
1103             elsif ( $obj->kind eq 'tree' ) {
1104              
1105             push @out, ( ' ' x ( $tabsize * $indent ) ) . 'raw: ';
1106             push @out, map {
1107             chomp $_;
1108             ( ' ' x ( $tabsize * $indent ) ) . $_
1109             } hdump( $obj->kind . ' ' . $obj->size . "\0" . $obj->content );
1110              
1111             my $sha1a = Digest::SHA->new;
1112             $sha1a->add( $obj->kind . ' ' . $obj->size . "\0" . $obj->content );
1113              
1114             push @out,
1115             ( ' ' x ( $tabsize * $indent ) )
1116             . 'my sha1 from Digest::SHA: '
1117             . $sha1a->hexdigest;
1118              
1119             my @directory_entries = $obj->directory_entries;
1120              
1121             foreach my $de (@directory_entries) {
1122             push @out,
1123             ( ' ' x ( $tabsize * $indent ) )
1124             . 'Directory Entry: '; # . $de->filename;
1125              
1126             push @out, $self->_debugtree( $de, $indent + 1 );
1127             }
1128             }
1129             elsif ( $obj->kind eq 'blob' ) {
1130             push @out, ' ' x ( $tabsize * ($indent) ) . 'content: ';
1131             push @out, ( ' ' x ( $tabsize * ( $indent + 1 ) ) )
1132             . join(
1133             "\n" . ( ' ' x ( $tabsize * ( $indent + 1 ) ) ),
1134             split( /\n/, $obj->content )
1135             );
1136             }
1137             else {
1138             push @out,
1139             ' ' x ( $tabsize * $indent )
1140             . 'Dump object kind '
1141             . $obj->kind
1142             . ' not implemented';
1143             }
1144             return @out;
1145              
1146             }
1147              
1148             =head2 hdump
1149              
1150             Return hexdump of given data.
1151              
1152             =cut
1153              
1154             sub hdump {
1155             my $offset = 0;
1156             my @out = ();
1157             my ( @array, $format );
1158             foreach
1159             my $data ( unpack( "a16" x ( length( $_[0] ) / 16 ) . "a*", $_[0] ) )
1160             {
1161             my ($len) = length($data);
1162             if ( $len == 16 ) {
1163             @array = unpack( 'N4', $data );
1164             $format = "0x%08x (%05d) %08x %08x %08x %08x %s\n";
1165             }
1166             else {
1167             @array = unpack( 'C*', $data );
1168             $_ = sprintf "%2.2x", $_ for @array;
1169             push( @array, ' ' ) while $len++ < 16;
1170             $format =
1171             "0x%08x (%05d)" . " %s%s%s%s %s%s%s%s %s%s%s%s %s%s%s%s %s\n";
1172             }
1173             $data =~ tr/\0-\37\177-\377/./;
1174             push @out, sprintf $format, $offset, $offset, @array, $data;
1175             $offset += 16;
1176             }
1177             return @out;
1178             }
1179              
1180             =head1 ACKNOWLEDGEMENTS
1181              
1182             Was based on the CPAN module App::Options, but since been converted to Moose.
1183              
1184             =head1 AUTHOR
1185              
1186             Scott T. Hardin, C<< <mrscotty at cpan.org> >>
1187              
1188             Martin Bartosch
1189              
1190             Oliver Welter
1191              
1192             =head1 BUGS
1193              
1194             Please report any bugs or feature requests to C<bug-config-versioned at
1195             rt.cpan.org>, or through the web interface at
1196             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Config-Versioned>.
1197             I will be notified, and then you'll automatically be notified of progress
1198             on your bug as I make changes.
1199              
1200             =head1 SUPPORT
1201              
1202             You can find documentation for this module with the perldoc command.
1203              
1204             perldoc Config::Versioned
1205              
1206              
1207             You can also look for information at:
1208              
1209             =over 4
1210              
1211             =item * RT: CPAN's request tracker
1212              
1213             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Config-Versioned>
1214              
1215             =item * AnnoCPAN: Annotated CPAN documentation
1216              
1217             L<http://annocpan.org/dist/Config-Versioned>
1218              
1219             =item * CPAN Ratings
1220              
1221             L<http://cpanratings.perl.org/d/Config-Versioned>
1222              
1223             =item * Search CPAN
1224              
1225             L<http://search.cpan.org/dist/Config-Versioned/>
1226              
1227             =back
1228              
1229              
1230             =head1 COPYRIGHT
1231              
1232             Copyright 2011 Scott T. Hardin, all rights reserved.
1233              
1234             This program is free software; you can redistribute it
1235             and/or modify it under the same terms as Perl itself.
1236              
1237             =cut
1238              
1239             __PACKAGE__->meta->make_immutable;
1240              
1241             1; # End of Config::Versioned
1242