@EXPORT_OK = qw(command command_oneline command_noisy
command_output_pipe command_input_pipe command_close_pipe
command_bidi_pipe command_close_bidi_pipe
- version exec_path hash_object git_cmd_try);
+ version exec_path html_path hash_object git_cmd_try
+ remote_refs
+ temp_acquire temp_release temp_reset temp_path);
=head1 DESCRIPTION
Currently, the module merely wraps calls to external Git tools. In the future,
it will provide a much faster way to interact with Git by linking directly
to libgit. This should be completely opaque to the user, though (performance
-increate nonwithstanding).
+increase notwithstanding).
=cut
use Error qw(:try);
use Cwd qw(abs_path);
use IPC::Open2 qw(open2);
-
+use Fcntl qw(SEEK_SET SEEK_CUR);
}
}
}
- if (not defined $opts{Repository} and not defined $opts{WorkingCopy}) {
- $opts{Directory} ||= '.';
+ if (not defined $opts{Repository} and not defined $opts{WorkingCopy}
+ and not defined $opts{Directory}) {
+ $opts{Directory} = '.';
}
- if ($opts{Directory}) {
+ if (defined $opts{Directory}) {
-d $opts{Directory} or throw Error::Simple("Directory not found: $!");
my $search = Git->repository(WorkingCopy => $opts{Directory});
unless (-d "$dir/refs" and -d "$dir/objects" and -e "$dir/HEAD") {
# Mimick git-rev-parse --git-dir error message:
- throw Error::Simple('fatal: Not a git repository');
+ throw Error::Simple("fatal: Not a git repository: $dir");
}
my $search = Git->repository(Repository => $dir);
try {
$search->command('symbolic-ref', 'HEAD');
} catch Git::Error::Command with {
# Mimick git-rev-parse --git-dir error message:
- throw Error::Simple('fatal: Not a git repository');
+ throw Error::Simple("fatal: Not a git repository: $dir");
}
$opts{Repository} = abs_path($dir);
=cut
sub command_close_bidi_pipe {
+ local $?;
my ($pid, $in, $out, $ctx) = @_;
foreach my $fh ($in, $out) {
unless (close $fh) {
sub exec_path { command_oneline('--exec-path') }
+=item html_path ()
+
+Return path to the Git html documentation (the same as
+C<git --html-path>). Useful mostly only internally.
+
+=cut
+
+sub html_path { command_oneline('--html-path') }
+
+
=item repo_path ()
Return path to the git repository. Must be called on a repository instance.
return $color;
}
+=item remote_refs ( REPOSITORY [, GROUPS [, REFGLOBS ] ] )
+
+This function returns a hashref of refs stored in a given remote repository.
+The hash is in the format C<refname =\> hash>. For tags, the C<refname> entry
+contains the tag object while a C<refname^{}> entry gives the tagged objects.
+
+C<REPOSITORY> has the same meaning as the appropriate C<git-ls-remote>
+argument; either an URL or a remote name (if called on a repository instance).
+C<GROUPS> is an optional arrayref that can contain 'tags' to return all the
+tags and/or 'heads' to return all the heads. C<REFGLOB> is an optional array
+of strings containing a shell-like glob to further limit the refs returned in
+the hash; the meaning is again the same as the appropriate C<git-ls-remote>
+argument.
+
+This function may or may not be called on a repository instance. In the former
+case, remote names as defined in the repository are recognized as repository
+specifiers.
+
+=cut
+
+sub remote_refs {
+ my ($self, $repo, $groups, $refglobs) = _maybe_self(@_);
+ my @args;
+ if (ref $groups eq 'ARRAY') {
+ foreach (@$groups) {
+ if ($_ eq 'heads') {
+ push (@args, '--heads');
+ } elsif ($_ eq 'tags') {
+ push (@args, '--tags');
+ } else {
+ # Ignore unknown groups for future
+ # compatibility
+ }
+ }
+ }
+ push (@args, $repo);
+ if (ref $refglobs eq 'ARRAY') {
+ push (@args, @$refglobs);
+ }
+
+ my @self = $self ? ($self) : (); # Ultra trickery
+ my ($fh, $ctx) = Git::command_output_pipe(@self, 'ls-remote', @args);
+ my %refs;
+ while (<$fh>) {
+ chomp;
+ my ($hash, $ref) = split(/\t/, $_, 2);
+ $refs{$ref} = $hash;
+ }
+ Git::command_close_pipe(@self, $fh, $ctx);
+ return \%refs;
+}
+
+
=item ident ( TYPE | IDENTSTR )
=item ident_person ( TYPE | IDENTSTR | IDENTARRAY )
in the commit and tag objects or produced by C<var GIT_type_IDENT> (thus
C<TYPE> can be either I<author> or I<committer>; case is insignificant).
-The C<ident> method retrieves the ident information from C<git-var>
+The C<ident> method retrieves the ident information from C<git var>
and either returns it as a scalar string or as an array with the fields parsed.
Alternatively, it can take a prepared ident string (e.g. from the commit
object) and just parse it.
my @vars = map { 'hash_object_' . $_ } qw(pid in out ctx);
- command_close_bidi_pipe($self->{@vars});
- delete $self->{@vars};
+ command_close_bidi_pipe(@$self{@vars});
+ delete @$self{@vars};
}
=item cat_blob ( SHA1, FILEHANDLE )
my @vars = map { 'cat_blob_' . $_ } qw(pid in out ctx);
- command_close_bidi_pipe($self->{@vars});
- delete $self->{@vars};
+ command_close_bidi_pipe(@$self{@vars});
+ delete @$self{@vars};
+}
+
+
+{ # %TEMP_* Lexical Context
+
+my (%TEMP_FILEMAP, %TEMP_FILES);
+
+=item temp_acquire ( NAME )
+
+Attempts to retreive the temporary file mapped to the string C<NAME>. If an
+associated temp file has not been created this session or was closed, it is
+created, cached, and set for autoflush and binmode.
+
+Internally locks the file mapped to C<NAME>. This lock must be released with
+C<temp_release()> when the temp file is no longer needed. Subsequent attempts
+to retrieve temporary files mapped to the same C<NAME> while still locked will
+cause an error. This locking mechanism provides a weak guarantee and is not
+threadsafe. It does provide some error checking to help prevent temp file refs
+writing over one another.
+
+In general, the L<File::Handle> returned should not be closed by consumers as
+it defeats the purpose of this caching mechanism. If you need to close the temp
+file handle, then you should use L<File::Temp> or another temp file faculty
+directly. If a handle is closed and then requested again, then a warning will
+issue.
+
+=cut
+
+sub temp_acquire {
+ my $temp_fd = _temp_cache(@_);
+
+ $TEMP_FILES{$temp_fd}{locked} = 1;
+ $temp_fd;
+}
+
+=item temp_release ( NAME )
+
+=item temp_release ( FILEHANDLE )
+
+Releases a lock acquired through C<temp_acquire()>. Can be called either with
+the C<NAME> mapping used when acquiring the temp file or with the C<FILEHANDLE>
+referencing a locked temp file.
+
+Warns if an attempt is made to release a file that is not locked.
+
+The temp file will be truncated before being released. This can help to reduce
+disk I/O where the system is smart enough to detect the truncation while data
+is in the output buffers. Beware that after the temp file is released and
+truncated, any operations on that file may fail miserably until it is
+re-acquired. All contents are lost between each release and acquire mapped to
+the same string.
+
+=cut
+
+sub temp_release {
+ my ($self, $temp_fd, $trunc) = _maybe_self(@_);
+
+ if (exists $TEMP_FILEMAP{$temp_fd}) {
+ $temp_fd = $TEMP_FILES{$temp_fd};
+ }
+ unless ($TEMP_FILES{$temp_fd}{locked}) {
+ carp "Attempt to release temp file '",
+ $temp_fd, "' that has not been locked";
+ }
+ temp_reset($temp_fd) if $trunc and $temp_fd->opened;
+
+ $TEMP_FILES{$temp_fd}{locked} = 0;
+ undef;
+}
+
+sub _temp_cache {
+ my ($self, $name) = _maybe_self(@_);
+
+ _verify_require();
+
+ my $temp_fd = \$TEMP_FILEMAP{$name};
+ if (defined $$temp_fd and $$temp_fd->opened) {
+ if ($TEMP_FILES{$$temp_fd}{locked}) {
+ throw Error::Simple("Temp file with moniker '" .
+ $name . "' already in use");
+ }
+ } else {
+ if (defined $$temp_fd) {
+ # then we're here because of a closed handle.
+ carp "Temp file '", $name,
+ "' was closed. Opening replacement.";
+ }
+ my $fname;
+
+ my $tmpdir;
+ if (defined $self) {
+ $tmpdir = $self->repo_path();
+ }
+
+ ($$temp_fd, $fname) = File::Temp->tempfile(
+ 'Git_XXXXXX', UNLINK => 1, DIR => $tmpdir,
+ ) or throw Error::Simple("couldn't open new temp file");
+
+ $$temp_fd->autoflush;
+ binmode $$temp_fd;
+ $TEMP_FILES{$$temp_fd}{fname} = $fname;
+ }
+ $$temp_fd;
+}
+
+sub _verify_require {
+ eval { require File::Temp; require File::Spec; };
+ $@ and throw Error::Simple($@);
+}
+
+=item temp_reset ( FILEHANDLE )
+
+Truncates and resets the position of the C<FILEHANDLE>.
+
+=cut
+
+sub temp_reset {
+ my ($self, $temp_fd) = _maybe_self(@_);
+
+ truncate $temp_fd, 0
+ or throw Error::Simple("couldn't truncate file");
+ sysseek($temp_fd, 0, SEEK_SET) and seek($temp_fd, 0, SEEK_SET)
+ or throw Error::Simple("couldn't seek to beginning of file");
+ sysseek($temp_fd, 0, SEEK_CUR) == 0 and tell($temp_fd) == 0
+ or throw Error::Simple("expected file position to be reset");
+}
+
+=item temp_path ( NAME )
+
+=item temp_path ( FILEHANDLE )
+
+Returns the filename associated with the given tempfile.
+
+=cut
+
+sub temp_path {
+ my ($self, $temp_fd) = _maybe_self(@_);
+
+ if (exists $TEMP_FILEMAP{$temp_fd}) {
+ $temp_fd = $TEMP_FILEMAP{$temp_fd};
+ }
+ $TEMP_FILES{$temp_fd}{fname};
}
+sub END {
+ unlink values %TEMP_FILEMAP if %TEMP_FILEMAP;
+}
+
+} # %TEMP_* Lexical Context
+
=back
=head1 ERROR HANDLING
# the method was called upon an instance and (undef, @args) if
# it was called directly.
sub _maybe_self {
- # This breaks inheritance. Oh well.
- ref $_[0] eq 'Git' ? @_ : (undef, @_);
+ UNIVERSAL::isa($_[0], 'Git') ? @_ : (undef, @_);
}
# Check if the command id is something reasonable.