Revision history for Git-Native

0.006     2026-08-30 14:35:09Z
  - Remote: read the git_cert_hostkey field offsets from
    Git::Libgit2::cert_hostkey_offsets (new in 0.007) instead of
    compiling in the 1.5.x numbers. The values are identical on every
    supported ABI; the difference is one source of truth next to the
    binding, derived from the ABI, with t/75-cert-hostkey-layout.t still
    cross-checking against offsetof() wherever a compiler is available.
  - Git::Native: drop `use Moo` from the entry-point package, which takes
    the constructor away with it. The package declares no attributes and
    every sub in it is a class method, so Git::Native->new returned an
    object no method ever read - indistinguishable from passing the class
    name, which is what every caller did anyway. Nothing in this
    distribution or in its only known consumer ever called it. The
    wrapper classes under Git::Native:: are untouched and still Moo.
  - All modules now carry their own $VERSION. Only the main module had
    one, so Git::Native::Repository->VERSION and its siblings answered
    undef while META provides and the generated POD both claimed the
    dist version. dist.ini drops version_finder = :MainModule, which had
    confined the version rewrite to the main module.
  - Remote: take the git_fetch_options.prune offset from
    Git::Libgit2::fetch_options_prune_offset instead of a compiled-in
    constant. That constant was derived from libgit2 1.5, where the
    embedded callbacks struct is 120 bytes; against 1.9 (128 bytes) the
    prune value landed 8 bytes early, on the `update_refs` callback
    pointer that 1.9 prefers over `update_tips`. Fetch therefore never
    pruned, and a fetch that did report ref updates would have jumped to
    address 1 or 2. Requires Git::Libgit2 0.007.
  - t/60-config-snapshot.t: assert GIT_EREADONLY instead of matching
    libgit2 prose, whose wording moved from "readonly" to "read-only".
    The constant is likewise new in Git::Libgit2 0.007.
  - t/75-cert-hostkey-layout.t: new network-free probe pinning the
    git_cert_hostkey field offsets Remote compiles in. They are read only
    from the SSH certificate_check callback, so t/40-remote-ssh.t was
    their sole coverage and it skips without TEST_GIT_NATIVE_SSH_URL -
    the same blind spot the git_fetch_options.prune offset had. The test
    compiles offsetof() against the git2.h Alien::Libgit2 installs and
    compares the measurement with the constants, along with the
    git_cert_t and git_cert_ssh_t values the callback branches on. It
    skips, with the reason, when there is no C compiler, when git2.h is
    not reachable, or when the header's LIBGIT2_VERSION disagrees with
    the loaded library. Adds Alien::Libgit2 and ExtUtils::CBuilder as
    test prerequisites.

  - dist.ini: exclude .claude/ and CLAUDE.md from the gathered dist. The
    [@Author::GETTY] bundle runs Git::GatherDir with include_dotfiles, so
    the agent instructions - five agent files, seven skills, the house
    rules - shipped in the CPAN tarball alongside the code. They are
    working material for this repository, not documentation for
    consumers; nothing in lib/ or t/ reads them at runtime.

0.005     2026-08-11 03:39:12Z
  - Repository: new index accessor returning a Git::Native::Index, the
    first native access to the staging area in this distribution. It
    answers "is anything tracked at or below this path?" - the question
    status_for_path cannot: being a working-tree comparison it misses a
    path git tracks but that is gone from disk, and on a directory it
    fails GIT_EAMBIGUOUS. Consumers had to shell out to `git ls-files`
    for this.
  - Index: new read-only wrapper - entrycount, find, find_prefix,
    has_path, has_prefix, is_tracked_under, reload. No add / remove /
    write; nothing here can stage or unstage anything. find_prefix and
    has_prefix match a raw STRING prefix ('tasks' also matches
    'tasksfoo.txt'); is_tracked_under is the path-aware question and
    answers exactly what `git ls-files -- $path` does.
  - Index: Repository->index re-reads the index file on every call.
    libgit2 caches the git_index* inside the repository and hands the
    same object to every git_repository_index call, so without the read
    a fresh accessor would still report a stale index - a file another
    process staged would be invisible. A held Index is consequently not
    a snapshot either; reload is the explicit refresh.
  - Error: new is_locked predicate (GIT_ELOCKED). libgit2 takes a
    refs/<name>.lock for every reference write, so a concurrent writer
    fails with GIT_ELOCKED (-14), not GIT_EMODIFIED. A compare-and-swap
    retry loop that only retries on is_not_matched drops updates under
    contention; reference_create / reference_set_target now document that
    both codes are normal and retryable.
  - Error: new is_bare_repo predicate (GIT_EBAREREPO). A bare repository
    has no worktree, so status / status_for_path fail with GIT_EBAREREPO
    (-8) rather than returning an empty result. Code walking a mixed set
    of repositories had to compare the raw -8 to tell "not applicable
    here" from a real error.
  - Repository: replace the remaining hardcoded libgit2 return codes with
    the GIT_E* / GIT_ITEROVER / GIT_OBJECT_ANY constants from
    Git::Libgit2. No behaviour change - head() already treated -9 / -3 as
    unborn / missing HEAD, and the iterators already stopped on -31.
  - Tests: new edge-case layer t/51 - t/66, lifting branch coverage from
    59% to 81% and condition coverage from 48% to 76%. Covers the failure
    and boundary paths: every Error predicate against every other
    predicate's code, the credential-callback contract network-free
    (t/52), binary blob content with embedded NULs, open_ext and the init
    argument guards, _known_hosts_match including @revoked and
    @cert-authority, and _build_strarray's NULL-on-empty semantics.
  - Repository: signature_default returned a Signature whose name and
    email were the literal string '<from-config>'. The real values sat in
    the C handle and were never read, so anything asking who would commit
    got a placeholder. New Signature->from_handle reads name / email /
    when / offset out of the git_signature struct and copies them into
    Perl, so they survive the handle being freed.
  - Remote: nothing can die out of an FFI closure any more. The credential
    callback's type check sat outside its eval, so a callback returning
    something that is not a Git::Native::Credential died across libgit2's
    C frames; the check also used `ref` before calling ->isa, which died
    one line earlier on an unblessed reference. The update_tips callback
    had a bare die with no eval at all. All three now warn and return a
    negative rc, which libgit2 propagates for check_rc to throw.
  - Remote, Revwalker: use the GIT_PASSTHROUGH / GIT_ITEROVER constants
    from Git::Libgit2 instead of re-declaring them locally.
  - Tests: t/lib/TestRepo.pm now really isolates the developer's git
    config. GIT_CONFIG_GLOBAL / GIT_CONFIG_SYSTEM only reach the git CLI -
    libgit2 does not know them and guesses its config search path from
    HOME once, during git_libgit2_init. A BEGIN block redirects HOME and
    XDG_CONFIG_HOME before Git::Native is loaded, and TestRepo refuses to
    load after it. Until now every test commit was signed with the
    developer's identity and results depended on their ~/.gitconfig.
    Regression test in t/69-config-isolation.t. The system level is
    covered too, see the set_config_search_path entry below.
  - Tests: t/20-remote-local.t no longer claims to cover the PASSTHROUGH
    credential path. libgit2 invokes the credential callback only when
    the transport raises an auth challenge, which file:// never does, so
    the old assertion could not fail. The callback is now pinned in
    t/52-credential-callback.t instead.
  - Requires Git::Libgit2 0.006, up from 0.005. The new bindings it
    carries - git_object_lookup_prefix, git_libgit2_opts - and its
    constant-group exports are what the next few entries are built on.
  - Git::Native: new set_config_search_path(system|global|xdg|programdata
    => $dir) class method, wrapping git_libgit2_opts with
    GIT_OPT_SET_SEARCH_PATH. This is the only supported way to move
    libgit2's config search: /etc/gitconfig is compiled in and no
    environment variable reaches it - not GIT_CONFIG_SYSTEM, not
    GIT_CONFIG_NOSYSTEM. Deliberately a class method and not a repository
    method: the option mutates a process-global table in libgit2, and a
    repository that is already open keeps the config it resolved when it
    was opened, so a per-repository method would provably do nothing to
    its own object. Passing undef restores libgit2's default, "" blanks
    the level.
  - Repository: new object_by_prefix($short_hex), the equivalent of
    git rev-parse abc1234. Until now Oid->from_hex demanded all 40
    characters, so there was no way to resolve an abbreviated OID. Note
    that git_object_lookup_prefix counts the prefix in hex characters,
    not bytes. A prefix shorter than 4 croaks rather than reaching
    libgit2, which answers it with GIT_EAMBIGUOUS - the same code a
    genuine collision returns - so is_ambiguous keeps exactly one
    meaning.
  - Error: new is_ambiguous (GIT_EAMBIGUOUS) and is_owner_mismatch
    (GIT_EOWNER) predicates. On the second one, worth knowing before you
    reach for it: libgit2 1.5.1 only reports GIT_EOWNER when a
    safe.directory entry exists and does not match. With no entry at all
    - the normal state - it asks the config for the multivar, gets
    GIT_ENOTFOUND and returns that instead, so a foreign-owned repository
    reports a not-found naming a config key nobody set, and is_not_found
    is what answers. safe.directory = * is not honoured on 1.5.1 either.
    Both quirks are pinned in t/72-owner-mismatch.t.
  - Remote::Result: updated entries now always carry the same four keys,
    { ref, from, to, reason }. They used to differ by operation - fetch
    produced { ref, from, to } from update_tips, push produced
    { ref, reason } from push_update_reference - so caller code that
    handled both got undef on push without a warning, and the old POD
    claimed push returned from/to, which it never did. An all-zero OID in
    either tip now maps to undef, meaning "did not exist" resp. "ref
    deleted", instead of 40 zeroes.
  - Repository: commit_create validates its required arguments in Perl.
    Omitting message used to die inside libgit2 with "invalid argument:
    'string'", naming neither the method nor the argument. message => ''
    stays valid, parents => undef stays equivalent to omitting it.
  - Oid: from_hex and from_raw croak with their own message naming
    Git::Native::Oid, instead of passing through a croak from
    Git::Libgit2's helper that named a function the caller never called.
    @CARP_NOT makes the blame land on the calling line rather than inside
    the distribution. These stay croaks rather than becoming
    Git::Native::Error on purpose: the input never reaches libgit2, so
    there is no return code to report, and borrowing GIT_EINVALIDSPEC
    would make is_invalid_spec ambiguous between a bad refname and a bad
    OID inside a single reference_create call.
  - Branch, Revwalker, Remote: take GIT_BRANCH_*, GIT_SORT_* and
    GIT_DIRECTION_* from Git::Libgit2 0.006 instead of re-declaring them
    locally. Values verified identical against the installed module, not
    just the header. The struct-version constants stay local - Git::Libgit2
    does not export those.
  - POD for the whole public surface. Until now only reference_create and
    reference_set_target had one, and the class list in CLAUDE.md was the
    de-facto documentation - which no CPAN user sees. Every public method
    and attribute across all 17 modules is documented, with emphasis on
    the semantics you cannot guess: head() returning undef on an unborn
    or missing HEAD, tag() returning undef for lightweight tags, status
    on a bare repository failing GIT_EBAREREPO, Revwalker::next returning
    undef at ITEROVER.
  - Tests: t/70 - t/73 pin the argument guards on commit_create, the
    abbreviated-OID lookup including a real SHA1 prefix collision, the
    ownership check, and the croak-not-throw contract across all 14
    entry points that accept an OID or hex string. The predicate matrix
    in t/51 now covers all 13 curated predicates and has a symbol-table
    check that fails if a new predicate is added to Error.pm and not
    listed there.

0.004     2026-08-09 20:34:42Z
  - Repository: add compare-and-swap direct reference updates.
    reference_create(..., expected_old => $oid) uses
    git_reference_create_matching, and reference_set_target($name, $new_oid,
    expected_old => $old_oid) uses lookup plus git_reference_set_target.
    Stale expected OIDs throw Git::Native::Error with is_not_matched true.
    expected_old => undef atomically requires an absent ref when creating.
    The create path needs Git::Libgit2 to bind
    git_reference_create_matching; until that parallel binding lands it
    throws a clear function-not-bound error. libgit2 has no
    git_reference_set_target_matching function: git_reference_set_target
    already performs the atomic write by calling git_reference_create_matching
    with the looked-up ref's OID.
  - Remote: fetch and push now return a Git::Native::Remote::Result that
    carries per-ref outcomes. libgit2 returns 0 even when individual refs
    were skipped (non-fast-forward on fetch) or rejected (pre-receive
    hook, protected branch, server-side non-ff on push) — the only way to
    see those outcomes was through the per-ref callbacks. Git::Native now
    installs them and surfaces the verdicts:
      $r = $remote->fetch(refspecs => [...]);
      for my $u (@{$r->updated})  { ... }   # accepted moves, {ref, from, to}
      for my $r2 (@{$r->rejected}) { ... }  # push rejections, {ref, reason}
    `from` is undef for refs that did not exist locally before; `reason`
    is the libgit2 / server message ("" on a clean success).
  - Remote: install a certificate_check callback so SSH remotes work on
    libgit2 < 1.7 (which has no built-in known_hosts checking). Without it
    every git+ssh fetch/push/list_refs failed with GIT_ECERTIFICATE (-17)
    "invalid or unknown remote ssh hostkey". The hostkey is verified
    against ~/.ssh/known_hosts (hashed, plain, [host]:port and wildcard
    entries) by SHA256/SHA1 fingerprint, mirroring the git CLI. Set
    GIT_NATIVE_SSH_INSECURE=1 to accept any hostkey. HTTPS remotes keep
    libgit2's own TLS validation.
  - Oid: fix the `eq` overload so an Oid compares equal to its hex string
    (e.g. `$ref->target eq $known_sha`). It previously compared the 20 raw
    bytes against the right-hand side verbatim, so Oid-vs-hex-string never
    matched. Oid-vs-Oid is unaffected.
  - Error: libgit2 failures now surface as a Throwable Git::Native::Error
    (with code/klass/message) as documented, instead of leaking the
    low-level Git::Libgit2::Error. check_rc is re-homed in Git::Native::Error
    and used by every wrapper. klass now carries a real decoded category
    (Git::Libgit2 0.005), no longer always 0.
  - Error: predicates over the libgit2 code - is_not_found, is_exists,
    is_auth, is_certificate, is_conflict, is_not_fast_forward,
    is_unborn_branch, is_invalid_spec. The long tail is reachable via
    ->code and the GIT_E* constants now exported by Git::Libgit2.
  - Repository: new object($oid) - look up an object of unknown kind and
    return the matching typed wrapper (Blob / Tree / Commit / Tag).
  - Config: new get_bool($key) - read a git-style boolean via libgit2's
    git_config_get_bool (true/yes/on/1, false/no/off/0, integers by
    non-zero); undef when unset, Git::Native::Error on a non-boolean value.
    Repository->config_bool($key) is the snapshot-backed convenience, like
    config_string. Requires Git::Libgit2 0.005.
  - Config: get_string no longer swallows every error as undef - only
    GIT_ENOTFOUND maps to undef now, other failures throw (matching
    get_bool).

0.003     2026-05-27 18:55:00Z
  - New Git::Native::Config class - get_string / set_string / snapshot
  - Repository: config (live), config_snapshot (read-only), and a
    config_string($key) convenience that reads off a fresh snapshot
  - Git::Native->reference_name_is_valid($name) - static refname
    validator, no repository handle required

0.002     2026-05-27 18:02:46Z
  - Initial release
  - Moo-based high-level wrapper around Git::Libgit2 / libgit2 — no
    fork/exec, no XS
  - Tests: Error throw/catch (t/11-error.t), Credential constructors
    (t/35-credential.t), compile-check of all modules (t/00-load.t)
  - Core classes: Repository, Reference, Config, Blob, Tree, TreeBuilder,
    Commit, Signature, Oid, Object, Error (Throwable)
  - Remote class with fetch/push, wildcard-push expansion (libgit2 quirk),
    push --prune via connect+ls diff
  - Credential class for SSH key/agent, HTTPS userpass, default, username
  - General-purpose surface: clone (non-bare), Revwalker, Branch, Tag,
    status, status_for_path
  - Reference accessors/predicates: resolve, shorthand, symbolic_target,
    is_branch/is_remote/is_tag, plus set_target / symbolic_set_target
  - Repository HEAD surface: head, head_unborn, head_detached, set_head,
    reference_symbolic_create; init(initial_branch => 'main') pins HEAD
  - Commit accessors: summary, time (epoch), time_offset (minutes)
  - RAII handle ownership via DESTROY -> git_*_free, child objects hold
    strong ref to parent to prevent use-after-free
  - All tests run with GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
    to avoid polluting the user's gitconfig
  - Live SSH/HTTPS auth tests (opt-in via TEST_GIT_NATIVE_* env vars)
