Git.pm: Handle failed commands' output

Currently if an external command returns error exit code, a generic exception
is thrown and there is no chance for the caller to retrieve the command's
output.

This patch introduces a Git::Error::Command exception class which is thrown
in this case and contains both the error code and the captured command output.
You can use the new git_cmd_try statement to fatally catch the exception
while producing a user-friendly message.

It also adds command_close_pipe() for easier checking of exit status of
a command we have just a pipe handle of. It has partial forward dependency
on the next patch, but basically only in the area of documentation.

Signed-off-by: Petr Baudis <pasky@suse.cz>
Signed-off-by: Junio C Hamano <junkio@cox.net>
This commit is contained in:
Petr Baudis 2006-06-24 04:34:44 +02:00 committed by Junio C Hamano
parent 97b16c0674
commit 8b9150e3e3
2 changed files with 183 additions and 22 deletions

View file

@ -7,6 +7,7 @@
use strict;
use Git;
use Error qw(:try);
my $repo = Git->repository();
@ -31,7 +32,17 @@ sub andjoin {
}
sub repoconfig {
my ($val) = $repo->command_oneline('repo-config', '--get', 'merge.summary');
my $val;
try {
$val = $repo->command_oneline('repo-config', '--get', 'merge.summary');
} catch Git::Error::Command with {
my ($E) = shift;
if ($E->value() == 1) {
return undef;
} else {
throw $E;
}
};
return $val;
}

View file

@ -24,16 +24,17 @@ =head1 SYNOPSIS
my $version = Git::command_oneline('version');
Git::command_noisy('update-server-info');
git_cmd_try { Git::command_noisy('update-server-info') }
'%s failed w/ code %d';
my $repo = Git->repository (Directory => '/srv/git/cogito.git');
my @revs = $repo->command('rev-list', '--since=last monday', '--all');
my $fh = $repo->command_pipe('rev-list', '--since=last monday', '--all');
my ($fh, $c) = $repo->command_pipe('rev-list', '--since=last monday', '--all');
my $lastrev = <$fh>; chomp $lastrev;
close $fh; # You may want to test rev-list exit status here
$repo->command_close_pipe($fh, $c);
my $lastrev = $repo->command_oneline('rev-list', '--all');
@ -44,11 +45,11 @@ =head1 SYNOPSIS
@ISA = qw(Exporter);
@EXPORT = qw();
@EXPORT = qw(git_cmd_try);
# Methods which can be called as standalone functions as well:
@EXPORT_OK = qw(command command_oneline command_pipe command_noisy
version exec_path hash_object);
version exec_path hash_object git_cmd_try);
=head1 DESCRIPTION
@ -88,7 +89,7 @@ =head1 DESCRIPTION
=cut
use Carp qw(carp); # croak is bad - throw instead
use Carp qw(carp croak); # but croak is bad - throw instead
use Error qw(:try);
require XSLoader;
@ -193,21 +194,35 @@ =head1 METHODS
=cut
sub command {
my $fh = command_pipe(@_);
my ($fh, $ctx) = command_pipe(@_);
if (not defined wantarray) {
_cmd_close($fh);
# Nothing to pepper the possible exception with.
_cmd_close($fh, $ctx);
} elsif (not wantarray) {
local $/;
my $text = <$fh>;
_cmd_close($fh);
try {
_cmd_close($fh, $ctx);
} catch Git::Error::Command with {
# Pepper with the output:
my $E = shift;
$E->{'-outputref'} = \$text;
throw $E;
};
return $text;
} else {
my @lines = <$fh>;
_cmd_close($fh);
chomp @lines;
try {
_cmd_close($fh, $ctx);
} catch Git::Error::Command with {
my $E = shift;
$E->{'-outputref'} = \@lines;
throw $E;
};
return @lines;
}
}
@ -222,12 +237,18 @@ sub command {
=cut
sub command_oneline {
my $fh = command_pipe(@_);
my ($fh, $ctx) = command_pipe(@_);
my $line = <$fh>;
_cmd_close($fh);
chomp $line;
try {
_cmd_close($fh, $ctx);
} catch Git::Error::Command with {
# Pepper with the output:
my $E = shift;
$E->{'-outputref'} = \$line;
throw $E;
};
return $line;
}
@ -251,7 +272,32 @@ sub command_pipe {
} elsif ($pid == 0) {
_cmd_exec($self, $cmd, @args);
}
return $fh;
return wantarray ? ($fh, join(' ', $cmd, @args)) : $fh;
}
=item command_close_pipe ( PIPE [, CTX ] )
Close the C<PIPE> as returned from C<command_pipe()>, checking
whether the command finished successfuly. The optional C<CTX> argument
is required if you want to see the command name in the error message,
and it is the second value returned by C<command_pipe()> when
called in array context. The call idiom is:
my ($fh, $ctx) = $r->command_pipe('status');
while (<$fh>) { ... }
$r->command_close_pipe($fh, $ctx);
Note that you should not rely on whatever actually is in C<CTX>;
currently it is simply the command name but in future the context might
have more complicated structure.
=cut
sub command_close_pipe {
my ($self, $fh, $ctx) = _maybe_self(@_);
$ctx ||= '<unknown>';
_cmd_close($fh, $ctx);
}
@ -280,9 +326,8 @@ sub command_noisy {
} elsif ($pid == 0) {
_cmd_exec($self, $cmd, @args);
}
if (waitpid($pid, 0) > 0 and $? != 0) {
# This is the best candidate for a custom exception class.
throw Error::Simple("exit status: $?");
if (waitpid($pid, 0) > 0 and $?>>8 != 0) {
throw Git::Error::Command(join(' ', $cmd, @args), $? >> 8);
}
}
@ -340,12 +385,117 @@ sub command_noisy {
# Implemented in Git.xs.
=back
=head1 ERROR HANDLING
All functions are supposed to throw Perl exceptions in case of errors.
See L<Error>.
See the L<Error> module on how to catch those. Most exceptions are mere
L<Error::Simple> instances.
However, the C<command()>, C<command_oneline()> and C<command_noisy()>
functions suite can throw C<Git::Error::Command> exceptions as well: those are
thrown when the external command returns an error code and contain the error
code as well as access to the captured command's output. The exception class
provides the usual C<stringify> and C<value> (command's exit code) methods and
in addition also a C<cmd_output> method that returns either an array or a
string with the captured command output (depending on the original function
call context; C<command_noisy()> returns C<undef>) and $<cmdline> which
returns the command and its arguments (but without proper quoting).
Note that the C<command_pipe()> function cannot throw this exception since
it has no idea whether the command failed or not. You will only find out
at the time you C<close> the pipe; if you want to have that automated,
use C<command_close_pipe()>, which can throw the exception.
=cut
{
package Git::Error::Command;
@Git::Error::Command::ISA = qw(Error);
sub new {
my $self = shift;
my $cmdline = '' . shift;
my $value = 0 + shift;
my $outputref = shift;
my(@args) = ();
local $Error::Depth = $Error::Depth + 1;
push(@args, '-cmdline', $cmdline);
push(@args, '-value', $value);
push(@args, '-outputref', $outputref);
$self->SUPER::new(-text => 'command returned error', @args);
}
sub stringify {
my $self = shift;
my $text = $self->SUPER::stringify;
$self->cmdline() . ': ' . $text . ': ' . $self->value() . "\n";
}
sub cmdline {
my $self = shift;
$self->{'-cmdline'};
}
sub cmd_output {
my $self = shift;
my $ref = $self->{'-outputref'};
defined $ref or undef;
if (ref $ref eq 'ARRAY') {
return @$ref;
} else { # SCALAR
return $$ref;
}
}
}
=over 4
=item git_cmd_try { CODE } ERRMSG
This magical statement will automatically catch any C<Git::Error::Command>
exceptions thrown by C<CODE> and make your program die with C<ERRMSG>
on its lips; the message will have %s substituted for the command line
and %d for the exit status. This statement is useful mostly for producing
more user-friendly error messages.
In case of no exception caught the statement returns C<CODE>'s return value.
Note that this is the only auto-exported function.
=cut
sub git_cmd_try(&$) {
my ($code, $errmsg) = @_;
my @result;
my $err;
my $array = wantarray;
try {
if ($array) {
@result = &$code;
} else {
$result[0] = &$code;
}
} catch Git::Error::Command with {
my $E = shift;
$err = $errmsg;
$err =~ s/\%s/$E->cmdline()/ge;
$err =~ s/\%d/$E->value()/ge;
# We can't croak here since Error.pm would mangle
# that to Error::Simple.
};
$err and croak $err;
return $array ? @result : $result[0];
}
=back
=head1 COPYRIGHT
@ -384,14 +534,14 @@ sub _cmd_exec {
# Close pipe to a subprocess.
sub _cmd_close {
my ($fh) = @_;
my ($fh, $ctx) = @_;
if (not close $fh) {
if ($!) {
# It's just close, no point in fatalities
carp "error closing pipe: $!";
} elsif ($? >> 8) {
# This is the best candidate for a custom exception class.
throw Error::Simple("exit status: ".($? >> 8));
# The caller should pepper this.
throw Git::Error::Command($ctx, $? >> 8);
}
# else we might e.g. closed a live stream; the command
# dying of SIGPIPE would drive us here.