From: Sam Vilain <sam.vilain@catalyst.net.nz>
To: git@vger.kernel.org
Cc: Petr Baudis <pasky@suse.cz>, Sam Vilain <sam.vilain@catalyst.net.nz>
Subject: [PATCH] perl: add new module Git::Config for cached 'git config' access
Date: Mon, 6 Apr 2009 11:46:15 +1200 [thread overview]
Message-ID: <1238975176-14354-1-git-send-email-sam.vilain@catalyst.net.nz> (raw)
Add a new module, Git::Config, for a better Git configuration API.
Signed-off-by: Sam Vilain <sam.vilain@catalyst.net.nz>
---
perl/Git/Config.pm | 465 ++++++++++++++++++++++++++++++++++++++++++++++++++
perl/Makefile.PL | 5 +-
t/t9700-perl-git.sh | 8 +
t/t9700/TestUtils.pm | 77 +++++++++
t/t9700/config.t | 127 ++++++++++++++
5 files changed, 681 insertions(+), 1 deletions(-)
create mode 100644 perl/Git/Config.pm
create mode 100644 t/t9700/TestUtils.pm
create mode 100644 t/t9700/config.t
diff --git a/perl/Git/Config.pm b/perl/Git/Config.pm
new file mode 100644
index 0000000..a0a6a41
--- /dev/null
+++ b/perl/Git/Config.pm
@@ -0,0 +1,465 @@
+
+package Git::Config;
+
+use 5.006;
+use strict;
+use warnings;
+
+use Git;
+use Error qw(:try);
+
+=head1 NAME
+
+Git::Config - caching interface to git-config state
+
+=head1 SYNOPSIS
+
+ use Git::Config;
+
+ my $conf = Git::Config->new( $git );
+
+ # return single config items
+ my $value = $conf->config( VARIABLE );
+
+ # return multi-config items
+ my @values = $conf->config( VARIABLE );
+
+ # manipulate type of slot, for special interpretation
+ $conf->type( VARIABLE => $type );
+
+ # change value
+ $conf->config( VARIABLE => $value );
+
+ # read or configure a global or system variable
+ $conf->global( VARIABLE => $value );
+ $conf->system( VARIABLE => $value );
+
+ # default is to autoflush
+ $conf->autoflush( 0 );
+
+ # if autoflush is disabled, flush explicitly.
+ $conf->flush;
+
+ # update cache
+ $conf->read;
+
+=head1 DESCRIPTION
+
+This module provides a cached interface to the B<git config> command
+in Perl.
+
+=head1 CONSTRUCTION
+
+=head2 B<Git::Config-E<gt>new( I<$git> )>
+
+Creates a new B<Git::Config> object. Does not read the configuration
+file yet.
+
+=cut
+
+sub new {
+ my $class = shift;
+ my $git = shift || Git->repository;
+ my $self = bless { git => $git }, $class;
+ while ( my ($item, $value) = splice @_, 0, 2 ) {
+ try {
+ $self->$item($value);
+ }
+ catch {
+ throw Error::Simple (
+ "Invalid constructor arg '$item'",
+ );
+ };
+ }
+ return $self;
+}
+
+=head1 METHODS
+
+=head2 B<$conf-E<gt>config( C<item> )>
+
+Reads the value of a particular configuration item.
+When called in B<list context>, returns all values of multiple value
+items.
+When called in B<scalar context>, raises an error if the item is
+specified multiple times.
+
+=head2 B<$conf-E<gt>config( C<item> =E<gt> I<$value> )>
+
+Sets the value of an item.
+If an B<array reference> is specified, the entire list of items is
+replaced with the values specified.
+If a single item is passed but the item is specified multiple times in
+the configuration file, raises an error.
+Returns the old value.
+
+=cut
+
+sub config {
+ return $_[0]->_config("", @_[1..$#_]);
+}
+
+sub _config {
+ my $self = shift;
+ my $which = shift;
+ my $item = shift;
+ my $value_passed = @_;
+ my $value = shift;
+
+ my $type = $self->type( $item );
+
+ my $_which = $which ? "${which}_" : "";
+ my $_read = "read_${_which}state";
+ my $_state = "${_which}state";
+
+ if (!$self->{$_read}) {
+ $self->read($which);
+ }
+
+ my $state;
+ if (exists $self->{$_state}{$item}) {
+ $state = $self->{$_state}{$item};
+ }
+ else {
+ $state = $self->{$_read}{$item};
+ }
+
+ if ($value_passed) {
+ if (!ref $value and defined $value and ref $state) {
+ throw Error::Simple (
+ "'$item' is specified multiple times",
+ );
+ }
+ if ( $type ) {
+ if (ref $value) {
+ $value = [ map { $self->freeze($type, $_) }
+ @$value ];
+ }
+ else {
+ $value = $self->freeze($type, $value);
+ }
+ }
+ $self->{$_state}{$item} = $value;
+ $self->flush() if $self->autoflush;
+ }
+
+ if (defined wantarray) {
+ my @values = ref $state ? @$state :
+ defined $state ? ($state) : ();
+
+ if ( my $type = $self->type( $item ) ) {
+ @values = map { $self->thaw($type, $_) }
+ @values;
+ }
+
+ if ( @values > 1 and
+ ( ($value_passed and not ref $value) or
+ (not $value_passed and not wantarray) )
+ ) {
+ throw Error::Simple (
+ "'$item' is specified multiple times",
+ );
+ }
+
+ return (wantarray ? @values : $values[0]);
+ }
+}
+
+=head2 B<$conf-E<gt>read()>
+
+Reads the current state of the configuration file.
+
+=cut
+
+sub read {
+ my $self = shift;
+ my $which = shift;
+
+ my $git = $self->{git};
+
+ my ($fh, $c) = $git->command_output_pipe(
+ 'config', ( $which ? ("--$which") : () ),
+ '--list',
+ );
+ my $read_state = {};
+
+ while (<$fh>) {
+ my ($item, $value) = m{(.*?)=(.*)};
+ my $sl = \( $read_state->{$item} );
+ if (!defined $$sl) {
+ $$sl = $value;
+ }
+ elsif (!ref $$sl) {
+ $$sl = [ $$sl, $value ];
+ }
+ else {
+ push @{ $$sl }, $value;
+ }
+ }
+
+ $git->command_close_pipe($fh, $c);
+
+ if ( $which ) {
+ $which .= "_";
+ }
+ else {
+ $which = "";
+ };
+
+ $self->{"read_${which}state"} = $read_state;
+}
+
+=head2 type( VARIABLE => $type )
+
+Specifies which set of rules to use when returning to and from the
+config file. C<$type> may be C<string>, C<boolean> or C<integer>.
+Globs such as C<*> are allowed which match everything except C<.>
+(full stop).
+
+=head2 type( VARIABLE )
+
+Returns the first matching defined type rule.
+
+=cut
+
+sub type {
+ my $self = shift;
+ my $item = shift;
+ my $got_type = @_;
+ my $type = shift;
+ $type ||= "string";
+
+ my $types = $self->{types} ||= [];
+ if ( $got_type ) {
+ $item =~ s{([\.(?])}{\\$1}g;
+ $item =~ s{\*}{[^\\.]*}g;
+ $item = qr{^$item$};
+ @$types = grep { $_->[0] ne $item }
+ @$types;
+ push @$types, [ $item, $type ];
+ }
+ else {
+ type:
+ for (@$types) {
+ if ($item =~ m{$_->[0]}) {
+ $type = $_->[1];
+ last type;
+ }
+ }
+
+ $type;
+ }
+}
+
+=head2 global( VARIABLE )
+
+=head2 system( VARIABLE )
+
+Return the value of the given variable in the global or system
+configuration (F<~/.gitconfig> and F</etc/gitconfig> on Unix).
+
+=head2 global( VARIABLE => $value )
+
+=head2 system( VARIABLE => $value )
+
+Set the value of the given variable in the global or system
+configuration.
+
+=cut
+
+sub global { return $_[0]->_config("global", @_[1..$#_]) }
+sub system { return $_[0]->_config("system", @_[1..$#_]) }
+
+=head2 autoflush
+
+Returns 0 or 1 depending on whether autoflush is enabled. Defaults to
+1.
+
+=head2 autoflush ( $value )
+
+Set the value of autoflush. If set to 1, flushes immediately.
+
+=cut
+
+sub autoflush {
+ my $self = shift;
+ if (@_) {
+ $self->{autoflush} = shift;
+ }
+ not (defined $self->{autoflush} and not $self->{autoflush});
+}
+
+=head2 flush
+
+Flushes all changed configuration values to the config file(s).
+
+=cut
+
+sub flush {
+ my $self = shift;
+
+ for my $which ("", "global", "system") {
+ my $st = $which . ($which ? "_" : "") . "state";;
+ my $read = "read_$st";
+ if (my $new = delete $self->{$st}) {
+ $self->_write($which, $new, $self->{$read});
+ $self->read($which);
+ }
+ }
+}
+
+sub _write {
+ my $self = shift;
+ my $which = shift;
+ my $state = shift;
+ my $read_state = shift;
+
+ my $git = $self->{git};
+
+ while (my ($item, $value) = each %$state) {
+ my $old_value = $read_state->{$item};
+ my @cmd = ($which ? ("--$which") : () );
+ my $type = $self->type($item);
+ if ($type ne "string") {
+ push @cmd, "--$type";
+ }
+ if (ref $value) {
+ $git->command_oneline (
+ "config", @cmd, "--replace-all",
+ $item, $value->[0],
+ );
+ for my $i (1..$#$value) {
+ $git->command_oneline (
+ "config", @cmd, "--add",
+ $item, $value->[$i],
+ );
+ };
+ }
+ elsif (defined $value) {
+ $git->command_oneline (
+ "config", @cmd, $item, $value,
+ );
+ }
+ elsif ($read_state->{$item}) {
+ $git->command_oneline (
+ "config", "--unset-all", @cmd,
+ $item, $value,
+ );
+ }
+ else {
+ # nothing to do - already not in config
+ }
+ }
+}
+
+sub _dispatch {
+ no strict 'refs';
+ my $self = shift;
+ my $func = shift;
+ my $type = shift;
+ my $sym = __PACKAGE__."::${type}::$func";
+ defined &{$sym}
+ or throw Error::Simple "Bad type '$type' in $func";
+ &{$sym}(@_, $self);
+}
+
+sub freeze {
+ my $self = shift;
+ $self->_dispatch("freeze", @_);
+}
+
+sub thaw {
+ my $self = shift;
+ $self->_dispatch("thaw", @_);
+}
+
+{
+ package Git::Config::string;
+ sub freeze { shift }
+ sub thaw { shift }
+}
+{
+ package Git::Config::integer;
+ our @mul = ("", "k", "M", "G");
+ sub freeze {
+ my $val = shift;
+ my $scale = 0;
+ while ( (my $num = int($val/1024))*1024 == $val ) {
+ $scale++;
+ $val = $num;
+ last if $scale == $#mul;
+ }
+ $val.$mul[$scale];
+ }
+ our $mul_re = qr/^(\d+)\s*${\( "(".join("|", @mul).")" )}$/i;
+ sub thaw {
+ my $val = shift;
+ $val =~ m{$mul_re}
+ or throw Error::Simple "Bad value for integer: '$val'";
+ my $num = $1;
+ if ($2) {
+ my $scale = 0;
+ do { $num = $num * 1024 }
+ until (lc($mul[++$scale]) eq lc($2));
+ }
+ $num;
+ }
+}
+{
+ package Git::Config::boolean;
+ our @true = qw(true yes 1);
+ our @false = qw(false no 0);
+ our $true_re = qr/^${\( "(".join("|", @true).")" )}$/i;
+ our $false_re = qr/^${\( "(".join("|", @false).")" )}$/i;
+ sub freeze {
+ my $val = shift;
+ if (!!$val) {
+ $true[0];
+ }
+ else {
+ $false[0];
+ }
+ }
+ sub thaw {
+ my $val = shift;
+ if ($val =~ m{$true_re}) {
+ 1;
+ }
+ elsif ($val =~ m{$false_re}) {
+ 0;
+ }
+ else {
+ throw Error::Simple "Bad value for boolean: '$val'";
+ }
+ }
+}
+
+1;
+
+__END__
+
+=head1 SEE ALSO
+
+L<Git>
+
+=head1 AUTHOR AND LICENSE
+
+Copyright 2009, Sam Vilain, L<sam@vilain.net>. All Rights Reserved.
+This program is Free Software; you may use it under the terms of the
+Perl Artistic License 2.0 or later, or the GPL v2 or later.
+
+=cut
+
+# Local Variables:
+# mode: cperl
+# cperl-brace-offset: 0
+# cperl-continued-brace-offset: 0
+# cperl-indent-level: 8
+# cperl-label-offset: -8
+# cperl-merge-trailing-else: nil
+# cperl-continued-statement-offset: 8
+# cperl-indent-parens-as-block: t
+# cperl-indent-wrt-brace: nil
+# End:
+#
+# vim: vim:tw=78:sts=0:noet
diff --git a/perl/Makefile.PL b/perl/Makefile.PL
index 320253e..80df0b0 100644
--- a/perl/Makefile.PL
+++ b/perl/Makefile.PL
@@ -8,7 +8,10 @@ instlibdir:
MAKE_FRAG
}
-my %pm = ('Git.pm' => '$(INST_LIBDIR)/Git.pm');
+my %pm = (map {( "$_.pm" => "\$(INST_LIBDIR)/$_.pm" )}
+ 'Git',
+ 'Git/Config',
+ );
# We come with our own bundled Error.pm. It's not in the set of default
# Perl modules so install it if it's not available on the system yet.
diff --git a/t/t9700-perl-git.sh b/t/t9700-perl-git.sh
index b81d5df..2f53d00 100755
--- a/t/t9700-perl-git.sh
+++ b/t/t9700-perl-git.sh
@@ -37,8 +37,16 @@ test_expect_success \
git config --add test.int 2k
'
+export PERL5LIB="$TEST_DIRECTORY/t9700:$GITPERLLIB:$PERL5LIB"
+
test_external_without_stderr \
'Perl API' \
perl "$TEST_DIRECTORY"/t9700/test.pl
+rm -rf .git
+
+test_external_without_stderr \
+ 'config API' \
+ perl "$TEST_DIRECTORY"/t9700/config.t
+
test_done
diff --git a/t/t9700/TestUtils.pm b/t/t9700/TestUtils.pm
new file mode 100644
index 0000000..72a386d
--- /dev/null
+++ b/t/t9700/TestUtils.pm
@@ -0,0 +1,77 @@
+#!/usr/bin/perl
+
+package TestUtils;
+
+use Carp;
+use base qw(Exporter);
+BEGIN {
+ our @EXPORT = qw(mk_tmp_repo in_empty_repo tmp_git random_port_pair);
+ $SIG{__WARN__} = sub {
+ my $i = 0;
+ while (my $caller = caller($i)) {
+ if ($caller eq "Test::More") {
+ return 0;
+ }
+ $i++;
+ }
+ my $culprit = caller;
+ if ($culprit =~ m{Class::C3}) {
+ return 1;
+ }
+ else {
+ print STDERR "*** WARNING FROM $culprit FOLLOWS ***\n";
+ }
+ Carp::cluck(@_);
+ print STDERR "*** END OF STACK DUMP ***\n";
+ 1
+ }
+ unless $ENV{NO_WARNING_TRACES};
+}
+
+use Cwd qw(fast_abs_path);
+use File::Temp qw(tempdir);
+
+sub mk_tmp_repo {
+ my $temp_dir = tempdir( "tmpXXXXX", CLEANUP => 1 );
+ system("cd $temp_dir; git-init >/dev/null 2>&1") == 0
+ or die "git-init failed; rc=$?";
+ fast_abs_path($temp_dir);
+}
+
+use Cwd;
+
+sub in_empty_repo {
+ my $coderef = shift;
+ my $old_wd = getcwd;
+ my $path = mk_tmp_repo();
+ chdir($path);
+ $coderef->();
+ chdir($old_wd);
+}
+
+use Git;
+sub tmp_git {
+ Git->repository(mk_tmp_repo);
+}
+
+# return an array ref of two unprivileged ports
+sub random_port_pair {
+ my $port = int(rand(2**16-1024-1)+1024);
+ [ $port, $port + 1 ];
+}
+
+1;
+
+# Local Variables:
+# mode: cperl
+# cperl-brace-offset: 0
+# cperl-continued-brace-offset: 0
+# cperl-indent-level: 8
+# cperl-label-offset: -8
+# cperl-merge-trailing-else: nil
+# cperl-continued-statement-offset: 8
+# cperl-indent-parens-as-block: t
+# cperl-indent-wrt-brace: nil
+# End:
+#
+# vim: vim:tw=78:sts=0:noet
diff --git a/t/t9700/config.t b/t/t9700/config.t
new file mode 100644
index 0000000..395a5c9
--- /dev/null
+++ b/t/t9700/config.t
@@ -0,0 +1,127 @@
+#!/usr/bin/env perl -- -w
+
+use TestUtils;
+use Test::More no_plan;
+use strict;
+
+use Error qw(:try);
+
+use_ok("Git::Config");
+
+in_empty_repo sub {
+ my $git = Git->repository;
+ $git->command_oneline("config", "foo.bar", "baz");
+ $git->command_oneline("config", "list.value", "one");
+ $git->command_oneline("config", "--add", "list.value", "two");
+ $git->command_oneline("config", "foo.intval", "12g");
+ $git->command_oneline("config", "foo.false.val", "false");
+ $git->command_oneline("config", "foo.true.val", "yes");
+
+ my $conf = Git::Config->new();
+ ok($conf, "constructed a new Git::Config");
+ isa_ok($conf, "Git::Config", "Git::Config->new()");
+
+ is($conf->config("foo.bar"), "baz", "read single line");
+ $conf->config("foo.bar", "frop");
+ like($git->command_oneline("config", "foo.bar"), qr/frop/,
+ "->config() has immediate effect");
+ $conf->autoflush(0);
+ is_deeply(
+ [$conf->config("list.value")],
+ [qw(one two)],
+ "read multi-value item",
+ );
+
+ eval {
+ my $val = $conf->config("list.value");
+ };
+ like ($@, qr{multiple}i,
+ "produced an error reading a list into a scalar");
+
+ eval {
+ $conf->config("list.value" => "single");
+ };
+ like($@, qr{multiple}i,
+ "produced an error replacing a list with a scalar");
+
+ ok(eval { $conf->config("foo.bar", [ "baz", "frop"]); 1 },
+ "no error replacing a scalar with a list");
+
+ like($git->command_oneline("config", "foo.bar"), qr/frop/,
+ "->config() no immediate effect with autoflush = 0");
+
+ $conf->flush;
+
+ like($git->command("config", "--get-all", "foo.bar"),
+ qr/baz\s*frop/,
+ "->flush()");
+
+ $conf->config("x.y$$" => undef);
+ my $x = $conf->config("x.y$$");
+ ok(!defined $x, "unset items return undef in scalar context");
+ my @x = $conf->config("x.y$$");
+ ok(@x == 0, "unset items return empty list in list context");
+
+ $conf->config("foo.bar" => undef);
+ @x = $conf->config("foo.bar");
+ ok(@x == 0, "deleted items appear empty immediately");
+ $conf->flush;
+ ok(!eval {
+ $git->command("config", "--get-all", "foo.bar");
+ 1;
+ }, "deleted items are removed from the config file");
+
+ $conf->type("foo.intval", "integer");
+ is($conf->type("foo.intval"), "integer",
+ "->type()");
+ is($conf->config("foo.intval"), 12*1024*1024*1024,
+ "integer thaw");
+ $conf->config("foo.intval", 1025*1024);
+ $conf->type("foo.intval", "string");
+ is($conf->config("foo.intval"), "1025k",
+ "integer freeze");
+
+ $conf->type("foo.*.val", "boolean");
+ is($conf->type("foo.this.val"), "boolean",
+ "wildcard types");
+ is($conf->config("foo.true.val"), 1,
+ "boolean thaw - 'yes'");
+ is($conf->config("foo.false.val"), 0,
+ "boolean thaw - 'false'");
+ my $unset = $conf->config("foo.bar.val");
+ is($unset, undef,
+ "boolean thaw - not present");
+
+ $git->command_oneline("config", "foo.intval", "12g");
+ $git->command_oneline("config", "foo.falseval", "false");
+ $git->command_oneline("config", "foo.trueval", "on");
+
+ SKIP:{
+ if (eval {
+ $git->command(
+ "config", "--global",
+ "--get-all", "foo.bar"
+ );
+ 1;
+ }) {
+ skip "someone set foo.bar in global config", 1;
+ }
+ my @foo_bar = $conf->global("foo.bar");
+ is_deeply(\@foo_bar, [], "->global() reading only");
+ }
+};
+
+
+# Local Variables:
+# mode: cperl
+# cperl-brace-offset: 0
+# cperl-continued-brace-offset: 0
+# cperl-indent-level: 8
+# cperl-label-offset: -8
+# cperl-merge-trailing-else: nil
+# cperl-continued-statement-offset: 8
+# cperl-indent-parens-as-block: t
+# cperl-indent-wrt-brace: nil
+# End:
+#
+# vim: vim:tw=78:sts=0:noet
--
1.6.0
next reply other threads:[~2009-04-06 0:23 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2009-04-05 23:46 Sam Vilain [this message]
2009-04-05 23:46 ` [PATCH] perl: make Git.pm use new Git::Config module Sam Vilain
2009-04-06 9:29 ` [PATCH] perl: add new module Git::Config for cached 'git config' access Frank Lichtenheld
2009-04-06 22:50 ` Sam Vilain
2009-04-07 12:01 ` Jakub Narebski
2009-04-08 5:49 ` Sam Vilain
2009-04-08 10:18 ` Jakub Narebski
2009-04-08 10:44 ` Sam Vilain
2009-04-08 23:13 ` Jakub Narebski
2009-04-08 6:29 ` Junio C Hamano
2009-04-08 9:50 ` Sam Vilain
2009-04-08 18:51 ` Junio C Hamano
2009-04-08 8:12 ` Petr Baudis
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=1238975176-14354-1-git-send-email-sam.vilain@catalyst.net.nz \
--to=sam.vilain@catalyst.net.nz \
--cc=git@vger.kernel.org \
--cc=pasky@suse.cz \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).