NAME

Git::Native::Remote - A libgit2 remote (fetch / push)

VERSION

version 0.005

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},
    );
  },
);

DESCRIPTION

Wraps git_remote*. Supports the libgit2 credential acquire callback, so SSH-agent / SSH-key / HTTPS-token auth all work without shelling out to the git binary.

The credentials coderef accepted by fetch, push and list_refs is libgit2's credential-acquire callback. libgit2 invokes it only when the transport actually issues an auth challenge — over file:// it is never called at all — and it may invoke it more than once, once per auth type it is prepared to try:

credentials => sub {
  my (%args) = @_;
  # $args{url}                the URL libgit2 is authenticating against
  # $args{username_from_url}  user part of the URL, or undef
  # $args{allowed_types}      git_credential_t bitmask of the credential
  #                           kinds this transport will accept
  Git::Native::Credential->ssh_agent(
    username => $args{username_from_url} // 'git',
  );
};

Return a Git::Native::Credential to authenticate with it — libgit2 takes ownership of it from that moment on. Return undef to decline: that maps to libgit2's GIT_PASSTHROUGH, which makes it move on to the next auth type instead of failing the operation outright. The credential you return has to be of a kind listed in allowed_types; libgit2 checks. The bitmask values are libgit2's own (1 userpass-plaintext, 2 ssh-key, 8 default, 32 username, ...) — Git::Libgit2 exports no names for them yet.

Nothing may escape the callback: it runs inside libgit2's C frames, where a Perl exception is undefined behaviour. The wrapper therefore contains every failure — if your coderef dies, or returns anything other than a credential or undef, the reason is reported with warn and an error code is returned to libgit2, so the surrounding fetch / push fails with a Git::Native::Error. Catch what you can inside the callback if you want a better message than that.

SSH host keys are verified against known_hosts by this module (libgit2 1.5 has no built-in check and would otherwise reject every host). A host that is unknown, or whose cached key does not match, fails the connection with a warning naming the ssh-keyscan command that would fix it; setting GIT_NATIVE_SSH_INSECURE=1 accepts any host key instead. HTTPS keeps libgit2's own CA validation.

url / name

say $remote->url;    # file:///srv/git/karr.git
say $remote->name;   # origin

url is the remote's fetch URL (a separately configured remote.<name>.pushurl is not exposed). name is undef for a remote built with $repo->remote_anonymous($url), which exists only in memory and is never written to the config.

fetch

my $result = $remote->fetch(
  refspecs => ['+refs/heads/*:refs/remotes/origin/*'],
  prune    => 1,
);
say "$_->{ref}: ", $_->{from} // '(new)', ' -> ', $_->{to} // '(deleted)'
  for @{ $result->updated };

Fetch from the remote. Returns a Git::Native::Remote::Result, not a truth value: libgit2 returns 0 from git_remote_fetch even when it skipped individual refs (a non-fast-forward update on a refspec without +), so the list of refs that actually moved is the only trustworthy report. $result->updated holds one { ref => $refname, from => $oid_hex|undef, to => $oid_hex|undef, reason => '' } per ref that changed locally — the same four keys a push reports, see "updated" in Git::Native::Remote::Result.

ref is the local destination the refspec mapped to, not the remote's name for it. from is undef for a ref that did not exist locally before. to is undef when the ref was deleted rather than moved, which is how a prune => 1 fetch reports the stale refs it dropped. reason is always the empty string on a fetch; it exists so fetch and push entries have the same shape. Refs that were already up to date produce no entry at all. $result->rejected is always empty for a fetch — libgit2's skip path does not route through the callback we install, so a skipped ref shows up as a missing entry in updated, not as a rejection.

Named arguments:

refspecs — arrayref of refspecs. Omitted or empty means "use the refspecs configured for this remote", the same as a bare git fetch.

prune1 deletes local refs whose upstream counterpart is gone, 0 explicitly disables pruning even when the configuration asks for it, and omitting it leaves the decision to remote.<name>.prune / fetch.prune. This is libgit2's own git_fetch_options.prune, unlike the reimplemented push side.

credentials — coderef, see above.

reflog_message — reflog message written for the updated refs, default fetch.

push

my $result = $remote->push(
  refspecs    => ['+refs/karr/*:refs/karr/*'],
  credentials => \&creds,
);
die join ', ', map { "$_->{ref}: $_->{reason}" } @{ $result->rejected }
  if @{ $result->rejected };

Push to the remote. Returns a Git::Native::Remote::Result, and $result->rejected is the part that matters: libgit2 returns 0 from git_remote_push as long as the transfer itself worked, even if the server refused every single ref (pre-receive hook declined, protected branch, non-fast-forward on a refspec without +). Code that only checks that push did not throw will report a wholly rejected push as a success.

Rejected entries are { ref => $remote_refname, reason => $message } with the status string the server sent (non-fast-forward, pre-receive hook declined, ...). Accepted refs land in $result->updated with the same four keys a fetch reports, { ref => $remote_refname, from => undef, to => $oid_hex|undef, reason => '' }.

ref is the ref's name on the remote side here, not a local one — the server reports its own names, and no attempt is made to map them back through the refspec. to is the OID the push put there, recovered from the local source side of the refspec rather than from the server, which only sends a per-ref verdict; it is undef for a ref the push deleted (the delete refspecs prune => 1 generates, or an explicit :refs/...), and also undef when the source is not a local reference we can resolve. from is always undef on a push: the previous remote-side OID is only knowable from a ref listing taken before the push, and push will not spend an extra network round trip on it — use list_refs beforehand if you need it.

Both lists come from the server's report-status, so they are only as detailed as the transport. libgit2's local (file://) transport neither runs receive hooks nor reports status, so against a local bare repository every pushed ref is reported as accepted.

Wildcard refspecs are expanded by this module, not by libgit2: git_remote_push rejects +refs/karr/*:refs/karr/* outright as "not a valid reference". push enumerates the local refs matching the source pattern and emits one concrete refspec per match, splicing the captured tail into the destination, so refs/karr/*:refs/backup/* works too. The consequence for callers: a push wildcard covers exactly the refs that exist locally at that moment, and a pattern matching nothing contributes nothing to the push rather than being an error. Fetch is unaffected — there the server side enumerates.

prune => 1 is likewise reimplemented; libgit2 has no equivalent of git push --prune. It connects to the remote, lists its refs, and for every remote ref in the destination namespace whose local counterpart no longer exists adds a :refs/... delete refspec to the push. It therefore needs an explicit refspecs list and only takes effect for wildcard refspecs — a concrete a:b refspec spans no namespace to diff. The extra connection uses the same credentials callback, and the deleted refs are reported in $result->updated alongside the refs that were written, distinguishable by their to => undef.

list_refs

my $names = $remote->list_refs;
# [ 'HEAD', 'refs/heads/main', 'refs/tags/v1.0' ]

Connect to the remote, read its ref advertisement, disconnect. Returns an arrayref of the refnames as the remote names them, HEAD included: no refspec mapping is applied and nothing is written to the local repository. Takes an optional credentials callback with the same contract as fetch. The connection is closed again even when the listing throws.

SEE ALSO

Git::Native::Credential, Git::Native::Remote::Result

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-git-native/issues.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.