Git-Native

 view release on metacpan or  search on metacpan

lib/Git/Native.pm  view on Meta::CPAN

use Carp ();
use Git::Libgit2 qw( init_lib check_rc GIT_REPOSITORY_INIT_BARE );
use Git::Libgit2::FFI ();
use Git::Native::Repository ();
use FFI::Platypus::Buffer qw( scalar_to_buffer );

use constant {
  GIT_CLONE_OPTIONS_VERSION => 1,
  # git_clone_options on libgit2 1.5 is ~312 bytes; over-allocate for
  # forward compat with newer libgit2 versions.
  CLONE_OPTIONS_SIZE        => 512,
};

# Ensure libgit2 is initialised before first use.
my $_init_count = 0;
sub _ensure_init {
  return if $_init_count;
  $_init_count = init_lib();
}

sub open {
  my ( $class, $path ) = @_;
  Carp::croak "Git::Native->open requires a path" unless defined $path;
  _ensure_init();
  my $repo;
  check_rc Git::Libgit2::FFI::git_repository_open( \$repo, $path );
  return Git::Native::Repository->new( _handle => $repo );
}

sub open_ext {
  my ( $class, $start_path, %opts ) = @_;
  _ensure_init();
  my $repo;
  check_rc Git::Libgit2::FFI::git_repository_open_ext(
    \$repo, $start_path,
    $opts{flags} // 0,
    $opts{ceiling_dirs},
  );
  return Git::Native::Repository->new( _handle => $repo );
}

sub init {
  my ( $class, $path, %opts ) = @_;
  Carp::croak "Git::Native->init requires a path" unless defined $path;
  _ensure_init();
  my $repo;
  my $flags = $opts{bare} ? GIT_REPOSITORY_INIT_BARE : 0;
  check_rc Git::Libgit2::FFI::git_repository_init( \$repo, $path, $flags );
  my $r = Git::Native::Repository->new( _handle => $repo );
  # Pin HEAD at the requested branch regardless of the compiled-in default
  # 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};
  _ensure_init();

  my $buf = "\0" x CLONE_OPTIONS_SIZE;
  my ($buf_p) = scalar_to_buffer($buf);
  check_rc Git::Libgit2::FFI::git_clone_options_init( $buf_p, GIT_CLONE_OPTIONS_VERSION );

  my $repo;
  check_rc Git::Libgit2::FFI::git_clone( \$repo, $url, $local_path, $buf_p );
  return Git::Native::Repository->new( _handle => $repo );
}

# reference_name_is_valid($name) - does libgit2 accept this refname?
# No repository required. Returns 1 (valid) or 0 (invalid).
sub reference_name_is_valid {
  my ( $class, $name ) = @_;
  return 0 unless defined $name;
  _ensure_init();
  my $rc = Git::Libgit2::FFI::git_reference_name_is_valid( \my $valid, $name );
  return ( $rc == 0 && $valid ) ? 1 : 0;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Git::Native - Native Git for Perl via libgit2 (FFI, no fork/exec)

=head1 VERSION

version 0.003

=head1 SYNOPSIS

  use Git::Native;

  my $repo = Git::Native->open('/path/to/.git');
  my $main = $repo->reference('refs/heads/main');
  say $main->target;     # commit OID

  # Build a commit without forking git
  my $blob_oid = $repo->blob_create_frombuffer("hello\n");
  my $tb       = $repo->tree_builder;
  $tb->insert(name => 'hi.txt', oid => $blob_oid, mode => 0100644);
  my $tree_oid = $tb->write;



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