Git-Native

 view release on metacpan or  search on metacpan

.claude/skills/git-native-core/SKILL.md  view on Meta::CPAN

---
name: git-native-core
description: Architecture, stack, memory ownership, error handling, and libgit2 quirks specific to the Git::Native CPAN distribution. Load on any Git::Native wrapper edit.
---

# Git::Native — project-specific architecture

The conventions in `CLAUDE.md` are the source of truth. This skill encodes the load-bearing
invariants an implementer must respect when editing the Moo wrappers under `lib/Git/Native/`.
The CLAUDE.md content is non-negotiable — apply silently, do not restate.

## Stack (CLAUDE.md → "Stack")

`Git::Native` (Moo) → `Git::Libgit2` (FFI) → `Alien::Libgit2` (libgit2 C lib).
Touching any of those layers requires going up or down through the same path; never bypass
`Git::Libgit2` to call libgit2 directly from a wrapper.

## Memory ownership (CLAUDE.md → "Memory Ownership")

Each Moo wrapper holds exactly one opaque libgit2 handle. `DESTROY` calls the matching
`git_*_free`. **Child objects** (a `Tree` returned from a `Commit`, a `Reference` returned
from `Repository->head`, etc.) hold a **strong ref to their parent in `_owner`** so the
parent outlives the child — no use-after-free. Pattern to follow when adding a child:

```perl
has _owner => ( is => 'ro', weak_ref => 0 );   # strong — child keeps parent alive
```

`weak_ref => 1` is for the *parent* holding a child when lifetime is shared; the **child**
holding the parent must be a strong ref, otherwise the parent can be freed mid-method and
the child's `git_*_owner` calls segfault.

## Error handling (CLAUDE.md → "Error Handling")

Every FFI call with an `int` return code goes through `check_rc($rc)` from
`Git::Native::Error`. Negative rc → `Git::Libgit2::Error->last` → re-throw as
`Git::Native::Error` (Throwable). Three rules:

1. **Never leak `Git::Libgit2::Error` above this layer.** `t/46-error-paths.t` asserts
   this. A new wrapper method that swallows or rethrows raw libgit2 errors is a regression.
2. **`code` is the primary discriminator** — use the curated `is_*` predicates on
   `Git::Native::Error` (`is_not_found`, `is_auth`, `is_certificate`, …) or compare
   `->code` to the `GIT_E*` constants from `Git::Libgit2`. `klass` (the `git_error_t`
   category) is a secondary signal only.
3. **`check_rc` is exported by `Git::Native::Error`**, not by `Git::Libgit2`. Wrong
   import path = wrong exception type escaping.

## libgit2 quirks that are not negotiable

The following are libgit2 behaviors that other languages (shell-git, JGit, libgit2
older versions) do differently. Each is a footgun for an unwary contributor — read this
list before touching the matching code.

- **Push wildcards are not expanded by libgit2.** `git_remote_push` rejects
  `+refs/karr/*:refs/karr/*` with "not a valid reference". `Git::Native::Remote::push`
  expands patterns client-side via `_owner->reference_names(glob => ...)` and emits one
  concrete refspec per matching local ref. Fetch is unaffected (server-side enumeration).
- **No native `--prune` on push.** Implemented by `_connect(DIRECTION_PUSH)` +
  `git_remote_ls` + diffing remote heads against the expanded local set, then prepending
  `:refs/...` delete refspecs to the push call. `_connect` uses the credential callback,
  so prune works against authenticated remotes.
- **Clone bare is not exposed.** `git_clone_options` embeds two large structs
  (`git_checkout_options`, `git_fetch_options`) before the `bare` field; the offset shifts
  across libgit2 versions. The wrapper errors on `bare => 1` and points users at
  `init(bare=>1) + remote + fetch`. Don't try to add this without a probe of the offsets
  on the supported libgit2 floor (1.5).
- **Clone auth callback not yet plumbed.** Same offset story for the embedded
  `fetch_options.callbacks` pointer. Public HTTPS / git:// / file:// works today.
- **`tag()` returns undef for lightweight tags** — they're plain refs under `refs/tags/*`
  with no annotated object to wrap; use `reference()` instead. Document, don't "fix".
- **`status` uses `git_status_foreach` with a Perl closure** rather than walking
  `git_status_entry` structs by index. Avoids depending on `git_diff_file` layout, which
  grew an extra field in libgit2 1.7.
- **`tag_names()` walks a `git_strarray` via `unpack`** (16 bytes: pointer + count). Stable
  since 1.0 — keep it that way.

## Closure lifetime for the credential callback

`git_credential_acquire_cb` is a `FFI::Platypus::Closure`. The C signature has a
`git_credential **out` out-param — FFI::Platypus closures only accept native types +
strings, so it's declared as plain `opaque` (the pointer value). The Perl closure:

1. Calls the user's coderef.
2. Calls `_disown` on the returned `Git::Native::Credential` to hand ownership to libgit2.
3. `memcpy`s the pointer into the out address.
4. Returning `undef` from the user coderef maps to `GIT_PASSTHROUGH (-30)`, letting libgit2



( run in 3.619 seconds using v1.01-cache-2.11-cpan-14f38c9f855 )