view release on metacpan or search on metacpan
# Claude Code â commit: skills/, agents/, hooks/, settings.json
# Ignore: local overrides, credentials, session data
# Tracked: shared config & extensibility
!/.claude/
.claude/*
!.claude/settings.json
!.claude/settings.local.json
!.claude/agents/
!.claude/agents/**
!.claude/skills/
!.claude/skills/**
!.claude/hooks/
!.claude/hooks/**
# Local overrides (machine-specific)
.claude/*.local.*
.claude/local/
# Credentials & session state (never track)
.claude/.credentials.json
.claude/statsig/
.claude/todos/
.claude/projects/
# Perl / Dist::Zilla distribution
# Build artifacts
.build/
_build/
blib/
->delete
Git::Native::Config ->get_string / ->get_bool / ->set_string / ->snapshot
Git::Native::Blob ->content, ->size, ->oid
Git::Native::Tree ->entries, ->entry_by_name
Git::Native::TreeBuilder ->insert(name =>, oid =>, mode => 0100644) / ->write
Git::Native::Commit ->oid, ->message, ->summary, ->time (epoch), ->time_offset (min)
->tree, ->tree_oid, ->parent_count, ->parent_oids
Git::Native::Remote ->url, ->name
->fetch(refspecs =>, credentials =>, prune =>)
->push(refspecs =>, credentials =>, prune =>)
->list_refs(credentials =>)
Git::Native::Credential ->userpass / ->ssh_key / ->ssh_agent / ->default / ->username
Git::Native::Revwalker ->push_head / ->push_ref / ->push_oid / ->push_glob / ->push_range
->hide_head / ->hide_ref / ->hide_oid / ->hide_glob
->sorting / ->reset / ->simplify_first_parent
->next -> Oid | undef ->all -> [Oid, ...]
Git::Native::Branch ->name / ->refname / ->target / ->is_head / ->is_local / ->is_remote
->rename($new) / ->delete
Git::Native::Tag ->name / ->message / ->target_id (annotated only)
Git::Native::Signature name, email, when
lib/Git/Native.pm view on Meta::CPAN
# or ambient init.defaultBranch (sterile CI containers default to
# 'master'). The branch may be unborn at this point - that's fine.
if ( defined( my $branch = $opts{initial_branch} ) ) {
$branch = "refs/heads/$branch" unless $branch =~ m{^refs/};
$r->set_head($branch);
}
return $r;
}
# clone($url, $local_path) - non-bare only for now.
# Auth via credentials => sub {...} not yet plumbed; the clone_options
# struct embeds a fetch_options whose callback offset we'd need to probe
# per libgit2 version. Bare clones go through init+fetch+HEAD instead -
# the offset of `bare` is past two large embedded structs and isn't
# stable across libgit2 versions worth pinning here.
sub clone {
my ( $class, $url, $local_path, %opts ) = @_;
Carp::croak "Git::Native->clone requires url and local_path"
unless defined $url && defined $local_path;
Carp::croak "bare clones not yet supported by Git::Native->clone - use init(bare=>1) + remote + fetch"
if $opts{bare};
lib/Git/Native/Credential.pm view on Meta::CPAN
# explicit key file
my $cred = Git::Native::Credential->ssh_key(
username => 'git',
public_key => "$ENV{HOME}/.ssh/id_ed25519.pub",
private_key => "$ENV{HOME}/.ssh/id_ed25519",
passphrase => '',
);
=head1 DESCRIPTION
Returned from the C<credentials> callback you pass to
L<Git::Native::Remote>'s C<fetch>/C<push>. libgit2 takes ownership of
the credential once the callback returns successfully â the Perl wrapper
is disowned automatically so it won't double-free.
If you construct one without passing it to libgit2, DEMOLISH calls
C<git_credential_free> for you.
=head1 SUPPORT
=head2 Issues
lib/Git/Native/Remote.pm view on Meta::CPAN
# Allocate buffers a bit larger than the C struct for forward-compat.
use constant {
GIT_REMOTE_CALLBACKS_VERSION => 1,
GIT_FETCH_OPTIONS_VERSION => 1,
GIT_PUSH_OPTIONS_VERSION => 1,
CALLBACKS_SIZE => 256, # actual 1.5: 120; 1.9: ~152
FETCH_OPTIONS_SIZE => 384, # actual 1.5: 208
PUSH_OPTIONS_SIZE => 384, # actual 1.5: 192
CALLBACKS_CRED_OFFSET => 24, # credentials cb pointer
CALLBACKS_PAYLOAD_OFFSET => 104, # payload void*
FETCH_OPTS_CALLBACKS_OFFSET => 8, # callbacks struct (embedded)
FETCH_OPTS_PRUNE_OFFSET => 128, # int (8 + 120)
PUSH_OPTS_CALLBACKS_OFFSET => 8,
GIT_PASSTHROUGH => -30,
GIT_DIRECTION_FETCH => 0,
lib/Git/Native/Remote.pm view on Meta::CPAN
};
has _handle => ( is => 'rw', required => 1 );
has _owner => ( is => 'ro', required => 1 ); # Repository
sub url { Git::Libgit2::FFI::git_remote_url( $_[0]->_handle ) }
sub name { Git::Libgit2::FFI::git_remote_name( $_[0]->_handle ) }
# ---------- fetch / push ----------
# fetch(refspecs => [...], credentials => sub { ... }, prune => 0|1,
# reflog_message => '...')
sub fetch {
my ( $self, %args ) = @_;
my $refspecs_ref = $args{refspecs};
my ( $sa_ptr, $sa_keep ) = _build_strarray( $refspecs_ref );
my ( $opts_ptr, $opts_keep )
= _build_fetch_options( $args{credentials}, $args{prune} );
my $rc = Git::Libgit2::FFI::git_remote_fetch(
$self->_handle, $sa_ptr, $opts_ptr,
$args{reflog_message} // 'fetch',
);
check_rc $rc;
return $self;
}
# push(refspecs => [...], credentials => sub { ... }, prune => 0|1)
sub push {
my ( $self, %args ) = @_;
my $original_refspecs = $args{refspecs} // [];
my $refspecs_ref = $self->_expand_push_refspecs($original_refspecs);
# --prune: connect, list remote refs in our refspec's destination
# namespace, emit delete refspecs for the ones we don't have locally.
# Pass ORIGINAL refspecs (still containing wildcards) so we can
# recover the namespace pattern.
if ( $args{prune} && @$original_refspecs ) {
my @delete = $self->_compute_prune_deletes(
$original_refspecs, $args{credentials},
);
CORE::push @$refspecs_ref, @delete;
}
my ( $sa_ptr, $sa_keep ) = _build_strarray( $refspecs_ref );
my ( $opts_ptr, $opts_keep )
= _build_push_options( $args{credentials} );
my $rc = Git::Libgit2::FFI::git_remote_push(
$self->_handle, $sa_ptr, $opts_ptr,
);
check_rc $rc;
return $self;
}
# List the remote-side refs (requires connecting first). Returns an
# arrayref of names. Caller passes credentials cb so private remotes work.
sub list_refs {
my ( $self, %args ) = @_;
$self->_connect( GIT_DIRECTION_FETCH, $args{credentials} );
my @names;
eval {
check_rc Git::Libgit2::FFI::git_remote_ls(
\my $heads_arr, \my $count, $self->_handle,
);
# heads_arr is git_remote_head**: an array of $count pointers,
# each pointing to a git_remote_head whose .name (char*) lives at
# offset REMOTE_HEAD_NAME_OFFSET.
my $ffi = Git::Libgit2::FFI::ffi();
for ( my $i = 0; $i < $count; $i++ ) {
lib/Git/Native/Remote.pm view on Meta::CPAN
# 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 );
my %local;
$local{$_} = 1 for @{ $self->_owner->reference_names };
my @deletes;
my %seen;
# Walk *original* user refspecs to figure out the dst-pattern namespace.
# We can't recover the dst-pattern from already-expanded specs.
for my $rs (@$refspecs) {
my ( $force, $src, $dst ) = $rs =~ /\A(\+?)([^:]+):(.+)\z/;
next unless defined $src && $dst =~ /\*/;
lib/Git/Native/Remote.pm view on Meta::CPAN
check_rc Git::Libgit2::FFI::git_fetch_options_init(
$opts_ptr, GIT_FETCH_OPTIONS_VERSION,
);
my @keep = ( \$opts );
if ($cred_cb) {
my ( $cb_thunk, $cb_keep ) = _make_credential_thunk($cred_cb);
CORE::push @keep, $cb_keep;
# Write the closure's C pointer into callbacks.credentials.
my $cb_ptr_val = Git::Libgit2::FFI::ffi->cast(
'git_credential_acquire_cb' => 'opaque', $cb_thunk,
);
my $cb_buf = pack 'J', $cb_ptr_val;
my ($cb_buf_ptr) = scalar_to_buffer($cb_buf);
memcpy( $opts_ptr + FETCH_OPTS_CALLBACKS_OFFSET + CALLBACKS_CRED_OFFSET,
$cb_buf_ptr, 8 );
CORE::push @keep, \$cb_buf;
}
lib/Git/Native/Remote.pm view on Meta::CPAN
url => $url,
username_from_url => $username_from_url,
allowed_types => $allowed_types,
);
};
if ($@) {
warn "credential callback died: $@";
return -1;
}
return GIT_PASSTHROUGH unless defined $cred;
Carp::croak "credentials callback must return a Git::Native::Credential"
unless ref $cred && $cred->isa('Git::Native::Credential');
# Disown the wrapper â libgit2 takes ownership on return 0.
my $cred_handle = $cred->_disown;
# *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 );
lib/Git/Native/Remote.pm view on Meta::CPAN
version 0.003
=head1 SYNOPSIS
my $remote = $repo->remote('origin');
say $remote->url;
$remote->fetch(
refspecs => ['+refs/heads/*:refs/remotes/origin/*'],
credentials => sub {
my (%args) = @_;
Git::Native::Credential->ssh_agent(
username => $args{username_from_url} // 'git',
);
},
prune => 1,
);
$remote->push(
refspecs => ['+refs/karr/*:refs/karr/*'],
credentials => sub {
Git::Native::Credential->userpass(
username => 'git',
password => $ENV{GITHUB_TOKEN},
);
},
);
=head1 DESCRIPTION
Wraps C<git_remote*>. Supports the libgit2 credential acquire callback,
so SSH-agent / SSH-key / HTTPS-token auth all work without shelling out
to the C<git> binary.
The C<credentials> coderef is invoked by libgit2 each time an auth
attempt is needed. It receives C<url>, C<username_from_url>, and
C<allowed_types> as named args, and must return either a
L<Git::Native::Credential> or C<undef> (to fall through to the next
auth type).
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
t/20-remote-local.t view on Meta::CPAN
ok 1, 'fetch completed without die';
my $b_ref = $b->reference('refs/karr/test/data');
is $b_ref->target->hex, $commit_oid->hex, 'repo B has the fetched ref';
# Credentials callback exercises the closure path even when not strictly
# needed (file:// requires no auth â libgit2 still asks if registered).
my $remote_b2 = $b->remote_anonymous($url);
$remote_b2->fetch(
refspecs => ['+refs/karr/*:refs/karr/*'],
credentials => sub { undef }, # PASSTHROUGH â falls through
);
ok 1, 'fetch with credentials callback (PASSTHROUGH path)';
# --- prune semantics ---
# Add a second ref to A, push it; remote now has both.
my $extra_blob = $a->blob_create_frombuffer("extra\n");
my $tb2 = $a->tree_builder;
$tb2->insert( name => 'data', oid => $extra_blob, mode => 0100644 );
my $tree2 = $tb2->write;
my $commit2 = $a->commit_create( tree => $tree2, parents => [], message => 'extra' );
$a->reference_create( 'refs/karr/extra/data', $commit2, force => 1 );
$remote_a->push( refspecs => ['+refs/karr/*:refs/karr/*'] );
t/40-remote-ssh.t view on Meta::CPAN
# ssh-agent path:
# TEST_GIT_NATIVE_SSH_URL=git@github.com:getty/p5-git-native.git
# (no other vars â uses the running ssh-agent)
#
# explicit key path:
# TEST_GIT_NATIVE_SSH_URL=git@host:owner/repo.git
# TEST_GIT_NATIVE_SSH_KEY=/path/to/id_ed25519
# [TEST_GIT_NATIVE_SSH_PASSPHRASE=...]
#
# What gets exercised either way: remote_create â fetch with a
# credentials callback, ref-walk to confirm something came across.
my $url = $ENV{TEST_GIT_NATIVE_SSH_URL};
plan skip_all => 'TEST_GIT_NATIVE_SSH_URL not set â skipping live SSH auth test'
unless $url;
my $key_path = $ENV{TEST_GIT_NATIVE_SSH_KEY};
if ($key_path) {
plan skip_all => "SSH key not readable at $key_path" unless -r $key_path;
}
t/40-remote-ssh.t view on Meta::CPAN
public_key => -r "${key_path}.pub" ? "${key_path}.pub" : undef,
passphrase => $ENV{TEST_GIT_NATIVE_SSH_PASSPHRASE} // '',
);
}
return Git::Native::Credential->ssh_agent( username => $user );
};
eval {
$remote->fetch(
refspecs => ['+refs/heads/*:refs/remotes/origin/*'],
credentials => $cred_cb,
);
1;
} or do {
my $err = $@;
fail "fetch died: $err";
done_testing;
exit 0;
};
ok $cb_calls >= 1, "credential callback was invoked at least once ($cb_calls)";
t/41-remote-https.t view on Meta::CPAN
$cb_calls++;
# Public repo path â libgit2 may still call the callback once if
# the server probes; returning undef â GIT_PASSTHROUGH lets it
# fall back to unauthenticated.
return undef;
};
eval {
$remote->fetch(
refspecs => ['+refs/heads/*:refs/remotes/origin/*'],
credentials => $cred_cb,
);
1;
} or do {
my $err = $@;
fail "fetch died: $err";
done_testing;
exit 0;
};
if ($token) {