view release on metacpan or search on metacpan
.claude/skills/git-native-core/SKILL.md view on Meta::CPAN
`Git::Libgit2` to call libgit2 directly from a wrapper.
## Memory ownership (CLAUDE.md â "Memory Ownership")
Each Moo wrapper holds exactly one opaque libgit2 handle. `DESTROY` calls the matching
`git_*_free`. **Child objects** (a `Tree` returned from a `Commit`, a `Reference` returned
from `Repository->head`, etc.) hold a **strong ref to their parent in `_owner`** so the
parent outlives the child â no use-after-free. Pattern to follow when adding a child:
```perl
has _owner => ( is => 'ro', weak_ref => 0 ); # strong â child keeps parent alive
```
`weak_ref => 1` is for the *parent* holding a child when lifetime is shared; the **child**
holding the parent must be a strong ref, otherwise the parent can be freed mid-method and
the child's `git_*_owner` calls segfault.
## Error handling (CLAUDE.md â "Error Handling")
Every FFI call with an `int` return code goes through `check_rc($rc)` from
`Git::Native::Error`. Negative rc â `Git::Libgit2::Error->last` â re-throw as
lib/Git/Native/Blob.pm view on Meta::CPAN
# ABSTRACT: A libgit2 blob object
package Git::Native::Blob;
use Moo;
use Git::Libgit2::FFI ();
use Git::Native::Oid ();
has _handle => ( is => 'ro', required => 1 );
has _owner => ( is => 'ro', required => 1 ); # Repository - keeps repo alive
has oid => ( is => 'lazy' );
sub _build_oid {
my $self = shift;
Git::Native::Oid->from_ptr(
Git::Libgit2::FFI::git_object_id( $self->_handle )
);
}
sub size {
lib/Git/Native/Blob.pm view on Meta::CPAN
my $blob = $repo->blob($oid);
say $blob->size;
say $blob->content;
=head1 DESCRIPTION
A libgit2 blob, exposing C<oid>, C<size>, C<content>. Freed when the
object goes out of scope. Obtained from
L<Git::Native::Repository/blob> or L<Git::Native::Repository/object>; the
blob keeps its repository alive for as long as it is itself in scope.
Blobs are created from a Perl scalar with
L<Git::Native::Repository/blob_create_frombuffer>, which returns the OID
rather than a Blob.
=head2 oid
say $blob->oid; # full hex
The blob's L<Git::Native::Oid>. Computed on first use from the object
lib/Git/Native/Branch.pm view on Meta::CPAN
say $b->name; # 'main'
say $b->refname; # 'refs/heads/main'
say $b->target->hex; # commit OID
$b->rename('trunk');
=head1 DESCRIPTION
Wraps a libgit2 branch (which is really a C<git_reference> under
C<refs/heads/*> or C<refs/remotes/*>). Constructed by
L<Git::Native::Repository/branch> and L<Git::Native::Repository/branches>.
A Branch keeps its repository alive for as long as it is in scope.
The same ref is reachable as a L<Git::Native::Reference> through
L<Git::Native::Repository/reference>; this class adds the branch-specific
calls (C<name>, C<is_head>, C<rename>) on top.
=head2 type
my $b = $repo->branch('origin/main', type => Git::Native::Branch::GIT_BRANCH_REMOTE);
Which namespace the branch was looked up in â C<GIT_BRANCH_LOCAL> (1, the
lib/Git/Native/Commit.pm view on Meta::CPAN
=head1 DESCRIPTION
A libgit2 commit object exposing C<oid>, C<message>, C<summary>,
C<time> (Unix epoch), C<time_offset> (minutes east of UTC), C<tree>,
C<tree_oid>, C<parent_count>, C<parent_oids>.
Obtained from L<Git::Native::Repository/commit> or
L<Git::Native::Repository/object>; created with
L<Git::Native::Repository/commit_create>, which returns the new OID rather
than a Commit. A Commit keeps its repository alive, and so does the
L<Git::Native::Tree> it hands out â the tree outlives the commit it came
from.
=head2 oid
say $commit->oid;
The commit's own L<Git::Native::Oid>. Computed on first use.
=head2 message
lib/Git/Native/Config.pm view on Meta::CPAN
# ABSTRACT: A libgit2 configuration handle
package Git::Native::Config;
use Moo;
use Git::Libgit2::FFI ();
use Git::Libgit2 qw( GIT_ENOTFOUND );
use Git::Native::Error qw( check_rc );
has _handle => ( is => 'ro', required => 1 );
has _owner => ( is => 'ro' ); # Repository (when repo-derived) - keeps it alive
# get_string($key): the value, or undef when the key is unset. Only
# GIT_ENOTFOUND maps to undef; any other libgit2 failure throws via check_rc
# (matching get_bool) - we don't silently swallow real errors as "unset".
# libgit2 only guarantees git_config_get_string on a *snapshot* config;
# use Repository->config_snapshot / config_string for reads.
sub get_string {
my ( $self, $key ) = @_;
my $rc = Git::Libgit2::FFI::git_config_get_string( \my $out, $self->_handle, $key );
return undef if $rc == GIT_ENOTFOUND; # unset
lib/Git/Native/Oid.pm view on Meta::CPAN
my $oid = Git::Native::Oid->from_hex('abcd...');
say $oid; # full hex
say $oid->short; # 7 chars
$oid->ptr; # C pointer for libgit2
=head1 DESCRIPTION
A SHA-1 OID. Holds the raw 20 bytes; everything else is derived.
The raw scalar is the anchor for any pointer libgit2 reads it through -
keep the Oid alive as long as the pointer is in use.
An Oid is a plain value object with no libgit2 handle behind it, so it
outlives the repository, reference or commit it came from. Methods all
over L<Git::Native> that take an OID accept either an Oid or a
40-character hex string.
Two operators are overloaded. Stringification (C<"">) gives the full hex
form, so an Oid interpolates and prints without an explicit C<< ->hex >>:
say "commit $oid"; # 35104eb6815e52f24b06c95cbc53e95943cb532b
lib/Git/Native/Oid.pm view on Meta::CPAN
say $oid->short(10); # 35104eb681
The first C<$n> hex characters, 7 by default. Purely a prefix of C<hex> â
no uniqueness check against the repository, unlike C<git rev-parse --short>.
=head2 ptr
Git::Libgit2::FFI::some_call( $oid->ptr );
A C pointer to the raw bytes, for passing into libgit2. It points into the
Oid's own scalar, so the Oid has to stay alive for as long as the pointer
is in use.
=head1 SEE ALSO
L<Git::Native::Reference>, L<Git::Native::Commit>
=head1 SUPPORT
=head2 Issues
lib/Git/Native/Reference.pm view on Meta::CPAN
A Git reference. Direct refs carry an C<oid> C<target>; symbolic refs
carry a C<symbolic_target> (a refname) and C<resolve> to a direct ref.
Read accessors: C<name>, C<shorthand>, C<target>, C<symbolic_target>,
C<is_symbolic>, C<is_branch>, C<is_remote>, C<is_tag>.
Mutators return a fresh Reference: C<set_target> (direct refs),
C<symbolic_set_target> (symbolic refs), plus C<delete>.
References are obtained from a L<Git::Native::Repository> and keep it
alive: the repository handle is not freed while any reference taken from
it is still in scope.
=head2 name
say $ref->name; # refs/heads/main
The full reference name.
=head2 shorthand
lib/Git/Native/Reference.pm view on Meta::CPAN
The counterpart for B<symbolic> references: repoint at another refname
(which may be one that does not exist yet) and return the updated
reference as a new object. C<message> goes into the reflog. Throws a
L<Git::Native::Error> on a direct reference.
=head2 delete
$repo->reference('refs/heads/stale')->delete;
Delete the reference from the repository and return the invocant. The Perl
object stays alive and its accessors keep answering out of the handle it
already holds, so what you have afterwards is a snapshot of a ref that is
no longer there.
=head1 SEE ALSO
L<Git::Native::Repository>, L<Git::Native::Branch>, L<Git::Native::Oid>
=head1 SUPPORT
=head2 Issues
lib/Git/Native/Remote.pm view on Meta::CPAN
);
my $pkt = pack 'J', $ptr_val;
my ($pkt_p) = scalar_to_buffer($pkt);
memcpy( $cb_ptr + CALLBACKS_CRED_OFFSET, $pkt_p, 8 );
CORE::push @keep, \$pkt;
}
_install_certcheck( $cb_ptr, 0, \@keep );
check_rc Git::Libgit2::FFI::git_remote_connect(
$self->_handle, $direction, $cb_ptr, 0, 0,
);
# Hold keepalive on $self so it survives until the next call frees it.
$self->{_connect_keep} = \@keep;
return $self;
}
# Compute delete refspecs for `--prune`: for each `[+]src:dst` with `*`,
# list remote refs matching the dst pattern, and emit a delete for each
# one whose local counterpart no longer exists.
sub _compute_prune_deletes {
my ( $self, $refspecs, $cred_cb ) = @_;
my $remote_names = $self->list_refs( credentials => $cred_cb );
lib/Git/Native/Remote.pm view on Meta::CPAN
$expanded_dst =~ s/\*/$cap/;
CORE::push @out, "${force}${name}:${expanded_dst}";
}
}
return \@out;
}
# ---------- internals ----------
# Build a git_strarray pointing into Perl-owned memory. Returns
# ($strarray_ptr, $keepalive_scalars_ref). Caller must hold
# $keepalive_scalars_ref alive across the C call.
sub _build_strarray {
my ($refspecs) = @_;
$refspecs //= [];
Carp::croak "_build_strarray: refspecs must be an arrayref"
if ref $refspecs ne 'ARRAY';
# Empty list â NULL strarray pointer, which libgit2 reads as
# "use configured refspecs from .git/config".
return ( 0, [] ) unless @$refspecs;
# Copy each string so we have stable storage we control.
lib/Git/Native/Remote.pm view on Meta::CPAN
my ($bp) = scalar_to_buffer($buf);
memcpy( $opts_ptr + PUSH_OPTS_CALLBACKS_OFFSET
+ CALLBACKS_PUSH_UPDATE_REF_OFFSET, $bp, 8 );
CORE::push @keep, \$buf;
}
return ( $opts_ptr, \@keep );
}
# Wrap a user coderef so it conforms to git_credential_acquire_cb.
# Returns ($closure, $keepalive). The closure must outlive the C call â
# the keepalive bundle is what the Remote method holds onto.
#
# NOTHING in here may die: the closure is called from libgit2's C frames,
# and a Perl exception unwinding across them is undefined behaviour. Every
# failure mode reports via warn and returns a negative rc, which libgit2
# propagates out of git_remote_fetch/push for check_rc to throw properly.
sub _make_credential_thunk {
my ($user_cb) = @_;
my $ffi = Git::Libgit2::FFI::ffi();
my $closure = $ffi->closure(sub {
lib/Git/Native/Remote.pm view on Meta::CPAN
# *out_ptr = cred_handle (write 8 bytes of pointer to the address
# the caller gave us)
my $pkt = pack 'J', $cred_handle;
my ($pkt_p) = scalar_to_buffer($pkt);
memcpy( $out_ptr, $pkt_p, 8 );
return 0;
});
# `sticky` would survive process-lifetime; we only need until the C
# call returns, so just hand the closure to the caller's keepalive.
return ( $closure, [ \$closure ] );
}
# Name a value for a diagnostic without ever dying on it â a blessed object
# may carry an overloaded (and throwing) stringifier, so report its class
# instead of interpolating it. Only used from inside FFI closures, where a
# die is not survivable.
sub _describe_value {
my ($v) = @_;
if ( my $class = Scalar::Util::blessed($v) ) { return "a $class object" }
lib/Git/Native/Remote.pm view on Meta::CPAN
my ( $self, $refname ) = @_;
my $oid = eval { $self->_owner->reference($refname)->resolve->target };
return undef unless $oid;
return $oid->hex;
}
# ---------- host-key verification (certificate_check callback) ----------
# Write a certificate_check closure into a callbacks struct. $cb_base is the
# offset of the embedded git_remote_callbacks within $struct_ptr (0 for a
# bare callbacks struct, 8 for fetch/push options). Pushes keepalives.
#
# libgit2 1.5.x + libssh2 has NO built-in known_hosts checking (that landed in
# 1.7). Without a certificate_check callback the ssh transport rejects every
# host with GIT_ECERTIFICATE (-17) "invalid or unknown remote ssh hostkey".
# So we always install one and verify the hostkey against ~/.ssh/known_hosts
# ourselves, mirroring what the `git` CLI does via OpenSSH.
sub _install_certcheck {
my ( $struct_ptr, $cb_base, $keep ) = @_;
my ( $thunk, $thunk_keep ) = _make_certcheck_thunk();
CORE::push @$keep, @$thunk_keep;
my $ptr_val = Git::Libgit2::FFI::ffi->cast(
'git_transport_certificate_check_cb' => 'opaque', $thunk,
);
my $buf = pack 'J', $ptr_val;
my ($bp) = scalar_to_buffer($buf);
memcpy( $struct_ptr + $cb_base + CALLBACKS_CERTCHECK_OFFSET, $bp, 8 );
CORE::push @$keep, \$buf;
return;
}
# Build the certificate_check closure. Returns ($closure, $keepalive).
#
# int cb(git_cert *cert, int valid, const char *host, void *payload)
#
# Return 0 to accept, <0 to reject (libgit2 aborts the connection with that
# code). For TLS (git_cert_x509) we honour libgit2's own `valid` flag so HTTPS
# remotes keep their normal CA validation. For SSH (git_cert_hostkey) we verify
# against known_hosts unless GIT_NATIVE_SSH_INSECURE is set (accept-all).
sub _make_certcheck_thunk {
my $ffi = Git::Libgit2::FFI::ffi();
my $closure = $ffi->closure(sub {
lib/Git/Native/Revwalker.pm view on Meta::CPAN
B<Seeding is mandatory.> A walker that has had no C<push_*> call has no
starting point and therefore yields nothing at all: C<next> returns
C<undef> straight away and C<all> gives an empty arrayref. There is no
implicit "walk HEAD" â say C<< $walker->push_head >> for that.
The walk goes from the pushed commits towards their ancestors, so a child
always comes out before its parents. Every C<push_*> and C<hide_*> returns
the walker, so seeding chains.
A walker keeps its repository alive for as long as it is in scope.
=head2 push_oid
$walker->push_oid($oid);
$walker->push_oid('35104eb6815e52f24b06c95cbc53e95943cb532b');
Add a commit as a starting point. C<$oid> is a L<Git::Native::Oid> or a
40-character hex string, and must resolve to something committish â a blob
OID throws a L<Git::Native::Error> ("object is not a committish").
lib/Git/Native/Tag.pm view on Meta::CPAN
Wraps a libgit2 annotated tag object. Lightweight tags are plain refs
under C<refs/tags/*> and don't get a Tag wrapper - look them up with
L<Git::Native::Repository/reference> instead.
Everything in this class is therefore B<annotated-tag only>: a lightweight
tag has no tag object to carry a name, a message or a tagger.
L<Git::Native::Repository/tag> returns C<undef> for one rather than dying,
so a C<undef> result means "no annotated tag under that name", not "no
such tag" â L<Git::Native::Repository/tag_names> lists both kinds. A Tag
keeps its repository alive for as long as it is in scope.
=head2 name
say $tag->name; # 'v1.0.0'
The tag's short name, without the C<refs/tags/> prefix.
=head2 message
print $tag->message;
t/10-roundtrip.t view on Meta::CPAN
use Test2::V0;
use lib 't/lib';
use TestRepo;
use Git::Native;
use Git::Native::Signature;
my ( $repo, $tmp ) = TestRepo::new_repo(); # keep $tmp alive â tempdir is auto-removed when its refcount hits 0
ok( $repo, 'init returned a repository' );
like( $repo->workdir, qr{/$}, 'workdir ends with slash' );
ok( ! $repo->is_bare, 'non-bare by default' );
# blob
my $blob_oid = $repo->blob_create_frombuffer("hello native git\n");
like( "$blob_oid", qr/\A[0-9a-f]{40}\z/, "blob OID: $blob_oid" );
my $blob = $repo->blob($blob_oid);
is( $blob->size, length("hello native git\n"), 'blob size matches' );
t/39-config.t view on Meta::CPAN
use Test2::V0;
use lib 't/lib';
use TestRepo;
use Git::Native;
use Git::Native::Config;
my ( $repo, $tmp ) = TestRepo::new_repo(); # keep $tmp alive
# Live config: write a couple of values.
my $cfg = $repo->config;
isa_ok( $cfg, ['Git::Native::Config'], 'config returns a Config' );
$cfg->set_string( 'user.name', 'Native Tester' );
$cfg->set_string( 'user.email', 'native@example.invalid' );
# config_string reads off a fresh snapshot.
is( $repo->config_string('user.name'), 'Native Tester', 'config_string user.name' );
is( $repo->config_string('user.email'), 'native@example.invalid', 'config_string user.email' );
t/49-remote-callbacks.t view on Meta::CPAN
{ ref => 'refs/heads/ok', from => undef, to => 'a' x 40, reason => '' },
{ ref => 'refs/heads/empty', from => undef, to => 'b' x 40, reason => '' },
{ ref => 'refs/heads/gone', from => undef, to => undef, reason => '' },
], 'NULL and "" are both "accepted", and each entry carries the four-key '
. 'shape with `to` taken from the refspec target map';
is \@rejected, [
{ ref => 'refs/heads/nope', reason => 'pre-receive hook declined' },
], 'a non-empty status is a rejection carrying the server message verbatim';
ok $keep, 'the thunk returns a keepalive alongside the closure';
};
# A ref the server names but the refspec map does not know (a push through
# a refspec whose source is not a resolvable local reference) must still
# produce the full key set â an unknown oid is undef, never a missing key.
subtest 'an unmapped refname still yields the full four-key shape' => sub {
my ( @rejected, @updated );
my ( $closure, $keep ) = Git::Native::Remote::_make_push_update_thunk(
\@rejected, \@updated,
);
t/49-remote-callbacks.t view on Meta::CPAN
# pattern; if Remote keeps the closure in @keep as it claims, the fetch
# completes cleanly. (The hidden contract test: prove by absence of
# segfault.)
sub fetch_in_nested_scope {
my ($url) = @_;
my $tmp = Path::Tiny->tempdir;
my $r = Git::Native->init("$tmp");
my $rmt = $r->remote_create( 'o', $url );
my $kept;
do {
# The fetch result itself holds the keepalive. Discard the
# intermediate; the returned Result's @updated list captures the
# outcome independently of the closure's lifetime.
$kept = $rmt->fetch( refspecs => ['+refs/karr/*:refs/karr/*'] );
};
return $kept;
}
my $nested = fetch_in_nested_scope( 'file://' . $tmp_bare );
ok $nested, 'fetch in nested scope completes without segfault';
ok scalar @{ $nested->updated } >= 1,
'nested-scope fetch still recorded updates';
t/52-credential-callback.t view on Meta::CPAN
# Network-free unit test for the credential-acquire thunk.
#
# Why this file exists: a file:// fetch never asks for credentials (libgit2
# calls the registered callback zero times - instrumented, see the note in
# t/20-remote-local.t), and t/40 / t/41 skip without operator-set env vars.
# So the whole credential path - the FFI closure, the PASSTHROUGH mapping,
# the disown-and-memcpy handoff - had no test that runs on a clean checkout.
#
# Git::Native::Remote::_make_credential_thunk is a pure function: it takes the
# user's coderef and returns ($closure, $keepalive). An FFI::Platypus::Closure
# is a blessed CODE ref, so the closure can be invoked straight from Perl with
# synthetic arguments - exactly the values libgit2 would pass for
# int cb(git_credential **out, const char *url,
# const char *username_from_url, unsigned int allowed_types,
# void *payload)
# The one thing we cannot fake is libgit2 taking ownership of the credential,
# so the success case frees the handed-off pointer itself.
init_lib();
# Synthetic call arguments, standing in for what libgit2 passes.
my $URL = 'https://example.invalid/repo.git';
my $USER_FROM_URL = 'git';
my $ALLOWED = 3; # USERPASS_PLAINTEXT | SSH_KEY bitmask
# Allocate a writable 8-byte cell for the `git_credential **out` out-param and
# return ( $address, \$scalar ) - the scalar must stay alive while the address
# is in use. Same scalar_to_buffer trick Remote.pm uses to write into C memory.
sub out_cell {
my $buf = "\0" x 8;
my ($addr) = scalar_to_buffer($buf);
return ( $addr, \$buf );
}
subtest 'user coderef returning undef maps to GIT_PASSTHROUGH' => sub {
my @calls;
my ( $closure, $keep ) = Git::Native::Remote::_make_credential_thunk(
t/52-credential-callback.t view on Meta::CPAN
is unpack( 'J', $$cell ), 0,
'the out-param is left NULL on PASSTHROUGH - libgit2 must not read a stale pointer';
is scalar(@calls), 1, 'the user callback was invoked exactly once';
is $calls[0], {
url => $URL,
username_from_url => $USER_FROM_URL,
allowed_types => $ALLOWED,
}, 'the user callback gets url / username_from_url / allowed_types as named args';
ok $keep, 'the thunk returns a keepalive alongside the closure';
};
subtest 'a Git::Native::Credential is handed to libgit2 through the out-param' => sub {
my $cred = Git::Native::Credential->userpass(
username => 'user', password => 's3cr3t',
);
my $handle = $cred->_handle;
ok $handle, 'the credential wrapper starts out owning a git_credential*';
my ( $closure, $keep ) = Git::Native::Remote::_make_credential_thunk(
t/56-commit-tree.t view on Meta::CPAN
subtest 'a child commit records its parent' => sub {
my $commit = $repo->commit($child);
is $commit->parent_count, 1, 'parent_count is 1';
is [ map { $_->hex } @{ $commit->parent_oids } ], [ $root->hex ],
'parent_oids lists the parent OID';
};
subtest 'a Tree outlives the Commit it came from' => sub {
# Memory ownership: Commit->tree passes _owner => the Repository, so the
# Tree does not depend on the Commit staying alive. If that ever changed to
# _owner => $commit-and-nothing-else, freeing the commit here would leave a
# dangling handle and this read would be a use-after-free.
my $tree;
{
my $commit = $repo->commit($child);
$tree = $commit->tree;
} # $commit demolished, git_commit_free called
is $tree->entrycount, 1, 'the Tree still reads after its Commit was freed';
is $tree->entry_by_name('f')->{oid}->hex, $blob->hex,
t/60-config-snapshot.t view on Meta::CPAN
isnt $repo->config_string('user.email'), 'nope@example.invalid',
'in particular the refused value did not land';
};
subtest 'set_string returns the config for chaining' => sub {
ref_is $live->set_string( 'a.b', 'c' ), $live, 'set_string returns $self';
is $repo->config_string('a.b'), 'c', 'the chained write took effect';
};
subtest 'a snapshot of a snapshot still reads' => sub {
# snapshot() passes _owner along, so the repository stays alive behind a
# nested snapshot; if that ownership were dropped this read would be a
# use-after-free rather than a value.
my $nested = $live->snapshot->snapshot;
is $nested->get_string('a.b'), 'c', 'the nested snapshot reads the value';
};
done_testing;
t/66-remote-strarray.t view on Meta::CPAN
# the layout is asserted by reading it back the same way libgit2 would.
my ( $repo, $tmp ) = TestRepo::new_repo();
subtest 'no refspecs means a NULL strarray' => sub {
# NULL is meaningful here: libgit2 falls back to the remote's configured
# refspecs. Returning a zero-length strarray instead would mean "transfer
# nothing", which is a different operation.
my ( $ptr, $keep ) = Git::Native::Remote::_build_strarray(undef);
is $ptr, 0, 'undef refspecs -> NULL strarray pointer';
is $keep, [], 'and nothing to keep alive';
my ( $ptr2, $keep2 ) = Git::Native::Remote::_build_strarray( [] );
is $ptr2, 0, 'an empty arrayref -> NULL strarray pointer too';
};
subtest 'refspecs must be an arrayref' => sub {
my $err = dies { Git::Native::Remote::_build_strarray('refs/heads/main') };
like $err, qr/_build_strarray: refspecs must be an arrayref/,
'a plain string is rejected';
ok !ref($err), 'and it is a croak, not a libgit2 error';
t/66-remote-strarray.t view on Meta::CPAN
my $remote = $repo->remote_anonymous('file:///nonexistent');
my $fetch_err = dies { $remote->fetch( refspecs => 'refs/heads/main' ) };
like $fetch_err, qr/must be an arrayref/,
'fetch rejects a non-arrayref refspecs before connecting';
};
subtest 'the packed git_strarray describes exactly the refspecs given' => sub {
my @specs = ( '+refs/karr/*:refs/karr/*', 'refs/heads/main:refs/heads/main' );
my ( $ptr, $keep ) = Git::Native::Remote::_build_strarray( \@specs );
ok $ptr, 'a non-empty list gets a real strarray pointer';
ok scalar(@$keep), 'and a keepalive holding the Perl-owned buffers';
# {char **strings; size_t count} - 8 bytes each on the LP64 platforms this
# distribution targets, which is the same assumption tag_names makes.
my ( $strings_ptr, $count ) =
unpack 'JJ', Git::Native::Remote::_peek_bytes( $ptr, 16 );
is $count, scalar(@specs), 'count matches the number of refspecs';
ok $strings_ptr, 'the strings pointer is not NULL';
my $ffi = Git::Libgit2::FFI::ffi();
my @read_back;
t/71-object-prefix.t view on Meta::CPAN
'an odd-length prefix resolves (length is hex characters, not bytes)';
# Degenerate ends of the accepted range.
is $repo->object_by_prefix("$blob_oid")->oid . "", "$blob_oid",
'a full 40-character hex string is accepted';
is $repo->object_by_prefix($blob_oid)->oid . "", "$blob_oid",
'a Git::Native::Oid is accepted and behaves like object()';
is $repo->object_by_prefix( uc( substr "$blob_oid", 0, 7 ) )->oid . "", "$blob_oid",
'prefix matching is case-insensitive';
# The child holds its parent alive (memory ownership contract).
ok $blob->_owner == $repo, 'the returned wrapper owns the repository';
}
# ---------------------------------------------------------------------------
# Boundary: GIT_OID_MINPREFIXLEN characters is enough.
#
# Its own repository, holding exactly one object, so "4 characters resolve"
# cannot be spoiled by a chance collision with a second object.
# ---------------------------------------------------------------------------
{
t/74-index.t view on Meta::CPAN
isa_ok $err, ['Git::Native::Error'], 'status on the same repo throws';
is $err->is_bare_repo, 1, 'with GIT_EBAREREPO - the refusal ->index does not make';
};
# ---------------------------------------------------------------------------
# Memory ownership.
# ---------------------------------------------------------------------------
subtest 'an Index outlives the Repository variable it came from' => sub {
# Repository->index passes _owner => $self, so the Index holds a strong ref
# to the Repository and git_repository_free cannot run while the Index is
# alive. Drop the only other reference and keep reading: if _owner were ever
# dropped, the git_index* below would be reading through a freed repository.
my $orphan;
{
my $r = Git::Native->open("$repo_dir");
$orphan = $r->index;
} # $r out of scope - git_repository_free must NOT have run
is $orphan->entrycount, scalar @TRACKED,
'the Index still reads after its Repository variable is gone';
is $orphan->is_tracked_under('tasks'), 1, 'and still answers the path question';