Git-Native

 view release on metacpan or  search on metacpan

.gitignore  view on Meta::CPAN

# 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/

CLAUDE.md  view on Meta::CPAN

                          ->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) {



( run in 1.553 second using v1.01-cache-2.11-cpan-140bd7fdf52 )