# ABSTRACT: Low-level FFI bindings to libgit2

package Git::Libgit2;
our $VERSION = '0.006';
use strict;
use warnings;
use Carp ();
use FFI::Platypus::Buffer qw( scalar_to_buffer );
use Git::Libgit2::FFI ();
use Git::Libgit2::Error ();
use Exporter 'import';

our @EXPORT_OK = qw(
  init_lib
  shutdown_lib
  version
  check_rc
  oid_from_hex
  oid_to_hex

  GIT_OBJECT_ANY
  GIT_OBJECT_INVALID
  GIT_OBJECT_COMMIT
  GIT_OBJECT_TREE
  GIT_OBJECT_BLOB
  GIT_OBJECT_TAG

  GIT_REPOSITORY_INIT_BARE

  GIT_OID_RAWSZ
  GIT_OID_HEXSZ
  GIT_OID_MINPREFIXLEN

  GIT_OPT_SET_SEARCH_PATH
  GIT_CONFIG_LEVEL_PROGRAMDATA
  GIT_CONFIG_LEVEL_SYSTEM
  GIT_CONFIG_LEVEL_XDG
  GIT_CONFIG_LEVEL_GLOBAL
  GIT_CONFIG_LEVEL_LOCAL
  GIT_CONFIG_LEVEL_APP
  GIT_CONFIG_HIGHEST_LEVEL

  GIT_SORT_NONE
  GIT_SORT_TOPOLOGICAL
  GIT_SORT_TIME
  GIT_SORT_REVERSE
  GIT_DIRECTION_FETCH
  GIT_DIRECTION_PUSH
  GIT_BRANCH_LOCAL
  GIT_BRANCH_REMOTE
  GIT_BRANCH_ALL
  GIT_FILEMODE_UNREADABLE
  GIT_FILEMODE_TREE
  GIT_FILEMODE_BLOB
  GIT_FILEMODE_BLOB_EXECUTABLE
  GIT_FILEMODE_LINK
  GIT_FILEMODE_COMMIT
  GIT_STATUS_CURRENT
  GIT_STATUS_INDEX_NEW
  GIT_STATUS_INDEX_MODIFIED
  GIT_STATUS_INDEX_DELETED
  GIT_STATUS_INDEX_RENAMED
  GIT_STATUS_INDEX_TYPECHANGE
  GIT_STATUS_WT_NEW
  GIT_STATUS_WT_MODIFIED
  GIT_STATUS_WT_DELETED
  GIT_STATUS_WT_TYPECHANGE
  GIT_STATUS_WT_RENAMED
  GIT_STATUS_WT_UNREADABLE
  GIT_STATUS_IGNORED
  GIT_STATUS_CONFLICTED

  GIT_OK
  GIT_ERROR
  GIT_ENOTFOUND
  GIT_EEXISTS
  GIT_EAMBIGUOUS
  GIT_EBUFS
  GIT_EUSER
  GIT_EBAREREPO
  GIT_EUNBORNBRANCH
  GIT_EUNMERGED
  GIT_ENONFASTFORWARD
  GIT_EINVALIDSPEC
  GIT_ECONFLICT
  GIT_ELOCKED
  GIT_EMODIFIED
  GIT_EAUTH
  GIT_ECERTIFICATE
  GIT_EAPPLIED
  GIT_EPEEL
  GIT_EEOF
  GIT_EINVALID
  GIT_EUNCOMMITTED
  GIT_EDIRECTORY
  GIT_EMERGECONFLICT
  GIT_PASSTHROUGH
  GIT_ITEROVER
  GIT_RETRY
  GIT_EMISMATCH
  GIT_EINDEXDIRTY
  GIT_EAPPLYFAIL
  GIT_EOWNER
  GIT_TIMEOUT
);
our %EXPORT_TAGS = ( all => \@EXPORT_OK );

use constant {
  GIT_OBJECT_ANY        => -2,
  GIT_OBJECT_INVALID    => -1,
  GIT_OBJECT_COMMIT     =>  1,
  GIT_OBJECT_TREE       =>  2,
  GIT_OBJECT_BLOB       =>  3,
  GIT_OBJECT_TAG        =>  4,

  GIT_REPOSITORY_INIT_BARE => 1 << 0,

  GIT_OID_RAWSZ        => 20,
  GIT_OID_HEXSZ        => 40,
  GIT_OID_MINPREFIXLEN => 4,

  # git_libgit2_opt_t (include/git2/common.h). Only SET_SEARCH_PATH is
  # exported for now — the variadic git_libgit2_opts binding covers just the
  # (int, string) form. Add siblings on demand.
  GIT_OPT_SET_SEARCH_PATH => 5,

  # git_config_level_t (include/git2/config.h).
  GIT_CONFIG_LEVEL_PROGRAMDATA => 1,
  GIT_CONFIG_LEVEL_SYSTEM      => 2,
  GIT_CONFIG_LEVEL_XDG         => 3,
  GIT_CONFIG_LEVEL_GLOBAL      => 4,
  GIT_CONFIG_LEVEL_LOCAL       => 5,
  GIT_CONFIG_LEVEL_APP         => 6,
  GIT_CONFIG_HIGHEST_LEVEL     => -1,

  # git_sort_t (include/git2/revwalk.h).
  GIT_SORT_NONE        => 0,
  GIT_SORT_TOPOLOGICAL => 1 << 0,
  GIT_SORT_TIME        => 1 << 1,
  GIT_SORT_REVERSE     => 1 << 2,

  # git_direction (include/git2/net.h).
  GIT_DIRECTION_FETCH => 0,
  GIT_DIRECTION_PUSH  => 1,

  # git_branch_t (include/git2/types.h). ALL = LOCAL | REMOTE.
  GIT_BRANCH_LOCAL  => 1,
  GIT_BRANCH_REMOTE => 2,
  GIT_BRANCH_ALL    => 3,

  # git_filemode_t (include/git2/types.h). Octal literals — Perl octal
  # matches the C enum values (e.g. 0100644 == 33188 decimal).
  GIT_FILEMODE_UNREADABLE      => 0,
  GIT_FILEMODE_TREE            => 0040000,
  GIT_FILEMODE_BLOB            => 0100644,
  GIT_FILEMODE_BLOB_EXECUTABLE => 0100755,
  GIT_FILEMODE_LINK            => 0120000,
  GIT_FILEMODE_COMMIT          => 0160000,

  # git_status_t (include/git2/status.h).
  GIT_STATUS_CURRENT          => 0,
  GIT_STATUS_INDEX_NEW        => 1 << 0,
  GIT_STATUS_INDEX_MODIFIED   => 1 << 1,
  GIT_STATUS_INDEX_DELETED    => 1 << 2,
  GIT_STATUS_INDEX_RENAMED    => 1 << 3,
  GIT_STATUS_INDEX_TYPECHANGE => 1 << 4,
  GIT_STATUS_WT_NEW           => 1 << 7,
  GIT_STATUS_WT_MODIFIED      => 1 << 8,
  GIT_STATUS_WT_DELETED       => 1 << 9,
  GIT_STATUS_WT_TYPECHANGE    => 1 << 10,
  GIT_STATUS_WT_RENAMED       => 1 << 11,
  GIT_STATUS_WT_UNREADABLE    => 1 << 12,
  GIT_STATUS_IGNORED          => 1 << 14,
  GIT_STATUS_CONFLICTED       => 1 << 15,

  # git_error_code enum (include/git2/errors.h). Stable values across
  # libgit2 1.x. GIT_PASSTHROUGH / GIT_ITEROVER are control codes, not
  # failures, but live in the same enum.
  GIT_OK              =>   0,
  GIT_ERROR           =>  -1,
  GIT_ENOTFOUND       =>  -3,
  GIT_EEXISTS         =>  -4,
  GIT_EAMBIGUOUS      =>  -5,
  GIT_EBUFS           =>  -6,
  GIT_EUSER           =>  -7,
  GIT_EBAREREPO       =>  -8,
  GIT_EUNBORNBRANCH   =>  -9,
  GIT_EUNMERGED       => -10,
  GIT_ENONFASTFORWARD => -11,
  GIT_EINVALIDSPEC    => -12,
  GIT_ECONFLICT       => -13,
  GIT_ELOCKED         => -14,
  GIT_EMODIFIED       => -15,
  GIT_EAUTH           => -16,
  GIT_ECERTIFICATE    => -17,
  GIT_EAPPLIED        => -18,
  GIT_EPEEL           => -19,
  GIT_EEOF            => -20,
  GIT_EINVALID        => -21,
  GIT_EUNCOMMITTED    => -22,
  GIT_EDIRECTORY      => -23,
  GIT_EMERGECONFLICT  => -24,
  GIT_PASSTHROUGH     => -30,
  GIT_ITEROVER        => -31,
  GIT_RETRY           => -32,
  GIT_EMISMATCH       => -33,
  GIT_EINDEXDIRTY     => -34,
  GIT_EAPPLYFAIL      => -35,
  GIT_EOWNER          => -36,
  GIT_TIMEOUT         => -37,
};

my $initialised = 0;

sub init_lib {
  Git::Libgit2::FFI::ffi();
  my $rc = Git::Libgit2::FFI::git_libgit2_init();
  Carp::croak "git_libgit2_init failed (rc=$rc)" if $rc < 1;
  $initialised = $rc;
  return $rc;
}


sub shutdown_lib {
  return 0 unless $initialised;
  my $rc = Git::Libgit2::FFI::git_libgit2_shutdown();
  $initialised = $rc;
  return $rc;
}


sub version {
  Git::Libgit2::FFI::ffi();
  my ( $maj, $min, $rev );
  Git::Libgit2::FFI::git_libgit2_version( \$maj, \$min, \$rev );
  return wantarray ? ( $maj, $min, $rev ) : "$maj.$min.$rev";
}


sub check_rc {
  my ($rc) = @_;
  return $rc if $rc >= 0;
  die Git::Libgit2::Error->last($rc);
}


sub oid_from_hex {
  my ($hex) = @_;
  Carp::croak "oid_from_hex: expected 40-char hex, got '$hex'"
    unless defined $hex && length($hex) == GIT_OID_HEXSZ && $hex =~ /\A[0-9a-fA-F]{40}\z/;
  my $raw = "\0" x GIT_OID_RAWSZ;
  my ($ptr) = scalar_to_buffer($raw);
  check_rc Git::Libgit2::FFI::git_oid_fromstr( $ptr, $hex );
  return $raw;
}


sub oid_to_hex {
  my ($oid_ptr) = @_;
  my $buf = "\0" x ( GIT_OID_HEXSZ + 1 );
  my ($bufp) = scalar_to_buffer($buf);
  Git::Libgit2::FFI::git_oid_tostr( $bufp, GIT_OID_HEXSZ + 1, $oid_ptr );
  $buf =~ s/\0.*//s;
  return $buf;
}


# Get a raw pointer to a Perl scalar's bytes (for passing as 'opaque').
sub _scalar_ptr {
  my ($p) = scalar_to_buffer($_[0]);
  return $p;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Git::Libgit2 - Low-level FFI bindings to libgit2

=head1 VERSION

version 0.006

=head1 SYNOPSIS

  use Git::Libgit2 qw( init_lib version check_rc );

  init_lib();
  printf "libgit2 %s\n", version();

  # Direct FFI calls live in Git::Libgit2::FFI
  use Git::Libgit2::FFI;
  my $rc = Git::Libgit2::FFI::git_repository_open(\my $repo, '/path/to/.git');
  check_rc $rc;

=head1 DESCRIPTION

Low-level L<FFI::Platypus> bindings to the C<libgit2> C library, via
L<Alien::Libgit2>.

This module is intentionally close to the C surface. Use L<Git::Native>
for an idiomatic Moo wrapper with RAII handle management.

=head2 init_lib

    init_lib();

Initialise the libgit2 library (wraps C<git_libgit2_init>). Safe to call
repeatedly — libgit2 reference-counts initialisations and returns the new
count, which this returns too. Croaks if the count comes back below C<1>.

=head2 shutdown_lib

    shutdown_lib();

Decrement libgit2's initialisation count (wraps C<git_libgit2_shutdown>) and
return the remaining count. A no-op returning C<0> if L</init_lib> was never
called. Call once per matching L</init_lib>.

=head2 version

    my $string            = version();   # "1.9.0"
    my ($maj, $min, $rev) = version();    # (1, 9, 0)

Return the libgit2 library version (wraps C<git_libgit2_version>). In scalar
context returns a dotted C<"major.minor.revision"> string; in list context
returns the three numeric components.

=head2 check_rc

    my $rc = check_rc( some_libgit2_call(...) );

Pass a libgit2 return code straight through when it is non-negative. On a
negative code, throw the corresponding L<Git::Libgit2::Error> (built from
C<git_error_last>); the exception stringifies to the libgit2 error message.

=head2 oid_from_hex

    my $raw = oid_from_hex('39a3c8...');   # 40 hex chars

Convert a 40-character hex OID into a Perl scalar holding the raw 20 bytes
(wraps C<git_oid_fromstr>). Croaks unless the input is exactly 40 hex digits.

B<Lifetime:> the returned scalar I<is> the OID buffer — libgit2 is handed a
pointer into its PV. Keep the scalar alive for as long as any libgit2 call
still needs the OID.

=head2 oid_to_hex

    my $hex = oid_to_hex($oid_ptr);

Convert a C<git_oid> pointer into its 40-character hex string (wraps
C<git_oid_tostr>).

=head1 EXPORTS

Nothing is exported by default. Functions available on request:
C<init_lib>, C<shutdown_lib>, C<version>, C<check_rc>, C<oid_from_hex>,
C<oid_to_hex>.

Constants, by group: object type (C<GIT_OBJECT_*>), repository init
(C<GIT_REPOSITORY_INIT_BARE>), OID sizes and the abbreviated-OID minimum
(C<GIT_OID_RAWSZ>, C<GIT_OID_HEXSZ>, C<GIT_OID_MINPREFIXLEN>), error codes
(C<GIT_OK>, C<GIT_ERROR>, the
C<GIT_E*> family plus C<GIT_PASSTHROUGH>, C<GIT_ITEROVER>, C<GIT_RETRY>,
C<GIT_TIMEOUT>), the C<git_libgit2_opts> option and config levels
(C<GIT_OPT_SET_SEARCH_PATH>, C<GIT_CONFIG_LEVEL_*>,
C<GIT_CONFIG_HIGHEST_LEVEL>), revwalk sort (C<GIT_SORT_*>), remote
direction (C<GIT_DIRECTION_*>), branch type (C<GIT_BRANCH_*>), tree entry
filemode (C<GIT_FILEMODE_*>) and status flags (C<GIT_STATUS_*>).

The C<:all> tag imports every name listed above.

=head1 SEE ALSO

L<Alien::Libgit2>, L<Git::Native>, L<FFI::Platypus>, L<libgit2|https://libgit2.org/>

=head1 SUPPORT

=head2 Issues

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

=head1 CONTRIBUTING

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

=head1 AUTHOR

Torsten Raudssus <getty@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> L<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.

=cut
