Revision history for Git-Native
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)