Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.

feat(lockfile,store-dir): gitHosted on TarballResolution + pickStoreIndexKey (#436 §A)#440

Merged
zkochan merged 1 commit into
mainfrom
feat/436
May 13, 2026
Merged

feat(lockfile,store-dir): gitHosted on TarballResolution + pickStoreIndexKey (#436 §A)#440
zkochan merged 1 commit into
mainfrom
feat/436

Conversation

@zkochan

@zkochan zkochan commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Foundation for git-hosted package install (#436 Section A). Three small,
self-contained changes:

  • TarballResolution.gitHosted: Option<bool> in crates/lockfile.
    Recent pnpm lockfiles ship this field on tarballs sourced from a git
    host. Without it, pacquet's deny_unknown_fields strictness rejected
    real lockfiles before the install dispatcher ran — a silent correctness
    gap independent of the rest of Install git-hosted packages from pnpm-lock.yaml (frozen-lockfile) #436. Mirrors upstream's
    TarballResolution.gitHosted.

  • Back-fill on read. When gitHosted is absent but the tarball URL
    prefix-matches a known git host (codeload.github.com, gitlab.com,
    bitbucket.org) with a tar.gz substring, set it to true during
    deserialize. Mirrors upstream's
    enrichGitHostedFlag
    so every downstream reader can rely on the typed field instead of
    matching URL prefixes at each site.

  • git_hosted_store_index_key + pick_store_index_key helpers in
    pacquet-store-dir. Git-hosted cache content depends on whether the
    build ran during fetch, so the cache key carries a built / not-built
    dimension that the integrity-only key would collapse. Mirrors
    upstream's
    gitHostedStoreIndexKey / pickStoreIndexKey.
    No call sites switch yet — the dispatcher continues to error with
    UnsupportedResolution { resolution_kind: "git" } until the git fetcher
    lands.

Out of scope (follow-ups for #436)

  • Section B — pacquet-git-fetcher crate, git_shallow_hosts config,
    dispatcher wiring at install_package_by_snapshot.rs:110-115 and
    create_virtual_store.rs:513-520.
  • Section C — git-hosted tarball fetcher.
  • Section D — preparePackage port wiring into the existing lifecycle
    runner.
  • Section E — integration tests against a local git fixture.

Test plan

  • cargo nextest run -p pacquet-lockfile — gitHosted round-trip,
    back-fill from codeload/gitlab/bitbucket URLs, no back-fill from
    registry URLs, no back-fill from a github URL without tar.gz.
  • cargo nextest run -p pacquet-store-dirbuilt / not-built
    key, route selection by git_hosted flag, route selection by
    missing integrity.
  • just ready
  • taplo format --check
  • just dylint

Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • Bug Fixes

    • Improved detection and handling of git-hosted tarball dependencies in lockfiles, with automatic back-fill logic for legacy entries to ensure compatibility.
  • New Features

    • Enhanced store indexing with new key-generation utilities to distinguish between different tarball source types and build states, aligning with upstream standards.

Review Change Stack

…ndexKey (#436 §A)

Adds the foundation upstream pnpm uses to address git-hosted packages in the
store. Three pieces, all scoped to lockfile types and store-index keying:

- `TarballResolution.gitHosted: Option<bool>`. Lockfiles produced by recent
  pnpm carry this field on tarballs sourced from a git host. Without it,
  pacquet's strict `deny_unknown_fields` deserialization rejected real
  lockfiles before the install dispatcher ever ran. Mirrors upstream's
  `TarballResolution.gitHosted` at lockfile/types/src/index.ts:88-107.

- Back-fill on read: when `gitHosted` is absent and the tarball URL prefix-
  matches a known git host (`codeload.github.com`, `gitlab.com`,
  `bitbucket.org`) with a `tar.gz` substring, set it to `true` during
  deserialize. Mirrors upstream's `enrichGitHostedFlag` at
  lockfile/fs/src/lockfileFormatConverters.ts:158-168 so every downstream
  reader can rely on the typed field instead of URL pattern-matching at
  each site.

- `git_hosted_store_index_key` + `pick_store_index_key` helpers. Git-hosted
  cache content depends on whether build scripts ran during fetch, so the
  cache key carries a `built` / `not-built` dimension that the integrity-
  only key would collapse. Mirrors upstream's `gitHostedStoreIndexKey` and
  `pickStoreIndexKey` at store/index/src/index.ts:60-95. No call sites
  switch yet — the git fetcher and dispatcher wiring follow in later
  patches for #436.

Foundation only: no behavior change for non-git-hosted installs. The
`UnsupportedResolution { resolution_kind: "git" }` errors at
`install_package_by_snapshot.rs:110-115` and `create_virtual_store.rs:513-520`
remain in place until the fetcher lands.
Copilot AI review requested due to automatic review settings May 13, 2026 10:15
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 064e3ec9-23fe-447e-b566-25348a5121cc

📥 Commits

Reviewing files that changed from the base of the PR and between b514929 and 32db6eb.

📒 Files selected for processing (4)
  • crates/lockfile/src/resolution.rs
  • crates/lockfile/src/resolution/tests.rs
  • crates/store-dir/src/store_index.rs
  • crates/store-dir/src/store_index/tests.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Agent
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Preserve existing method chains and pipe-trait chains; do not break them into intermediate let bindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Path over &PathBuf, &str over &String) when it does not force extra copies.
Prefer Arc::clone(&x) and Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*; for any glob whose target is a module you control. External-crate preludes (e.g., use rayon::prelude::*;) and root-of-module re-exports (e.g., pub use submodule::*; in lib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plain String or &str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only via TryFrom<String> and/or FromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallible From<String> and From<&str> constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bare as assertion), add a from_str_unchecked (or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...

Files:

  • crates/store-dir/src/store_index/tests.rs
  • crates/store-dir/src/store_index.rs
  • crates/lockfile/src/resolution.rs
  • crates/lockfile/src/resolution/tests.rs
🧠 Learnings (2)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/store-dir/src/store_index/tests.rs
  • crates/lockfile/src/resolution/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/store-dir/src/store_index/tests.rs
  • crates/store-dir/src/store_index.rs
  • crates/lockfile/src/resolution.rs
  • crates/lockfile/src/resolution/tests.rs
🔇 Additional comments (4)
crates/lockfile/src/resolution.rs (1)

13-25: LGTM!

Also applies to: 91-101, 109-118

crates/store-dir/src/store_index.rs (1)

710-744: LGTM!

crates/store-dir/src/store_index/tests.rs (1)

1-4: LGTM!

Also applies to: 39-78

crates/lockfile/src/resolution/tests.rs (1)

24-25: LGTM!

Also applies to: 37-41, 43-58, 60-127, 135-136, 148-150, 161-177


📝 Walkthrough

Walkthrough

Tarball resolutions now include an optional git_hosted field that is back-filled during deserialization when the tarball URL matches known git-hosted codeload patterns. Store index key construction is updated with pick_store_index_key() to route between integrity-based and git-hosted key formats based on this flag.

Changes

Git-hosted tarball support

Layer / File(s) Summary
Tarball resolution schema and backfill logic
crates/lockfile/src/resolution.rs
TarballResolution adds optional git_hosted: Option<bool> field with serde defaults. From<ResolutionSerde> for LockfileResolution conversion back-fills git_hosted = Some(true) for tarball URLs matching git-hosted codeload domains (containing tar.gz) via new is_git_hosted_tarball_url helper.
Tarball resolution deserialization and serialization tests
crates/lockfile/src/resolution/tests.rs
Updated and new test cases validate git_hosted field handling: deserialization expects None when absent, Some(true) when explicit or back-filled for matching URLs; serialization omits gitHosted when None and emits it when Some(true).
Store index key construction helpers
crates/store-dir/src/store_index.rs
Added git_hosted_store_index_key() to format git-hosted tarball keys with built/not-built markers, and pick_store_index_key() to select between integrity-based and git-hosted key formats based on integrity presence and git_hosted flag.
Store index key helper tests
crates/store-dir/src/store_index/tests.rs
Unit tests validate key-construction function string formats for plain tarballs, git-hosted variants, and pick_store_index_key() routing behavior across integrity presence and git_hosted flag combinations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Poem

🐰 A tarball hopped in with a brand new tag,
To flag which ones the git-host would drag,
And when the keys were picked for the store,
Git-hosted tarballs knew just what they're for! 📦

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: adding gitHosted field to TarballResolution and new store-index key helpers.
Description check ✅ Passed The description comprehensively covers the changes, includes upstream references, scope boundaries, and test plan, closely following the template structure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/436

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds foundational support for git-hosted package handling by extending lockfile resolution parsing and introducing store-index key helpers that match pnpm’s semantics, without yet wiring any install dispatcher behavior.

Changes:

  • Extend TarballResolution with an optional git_hosted (gitHosted) field and back-fill it on deserialize for known git-hosted tarball URL patterns.
  • Add git_hosted_store_index_key and pick_store_index_key helpers to generate pnpm-compatible store index keys that include the built/not-built dimension for git-hosted content.
  • Add unit tests covering the new lockfile field behavior and store-index key selection logic.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
crates/lockfile/src/resolution.rs Adds git_hosted field and deserialization back-fill via a pnpm-mirroring URL check.
crates/lockfile/src/resolution/tests.rs Adds coverage for gitHosted round-trip and back-fill behavior across supported URL shapes.
crates/store-dir/src/store_index.rs Introduces git-hosted store index key helpers and pnpm-mirroring key selection logic.
crates/store-dir/src/store_index/tests.rs Adds tests for git-hosted key formatting and pick_store_index_key routing rules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.03%. Comparing base (b514929) to head (32db6eb).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #440      +/-   ##
==========================================
+ Coverage   86.98%   87.03%   +0.04%     
==========================================
  Files          93       93              
  Lines        6647     6671      +24     
==========================================
+ Hits         5782     5806      +24     
  Misses        865      865              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00     16.3±0.46ms   266.4 KB/sec    1.00     16.3±0.82ms   265.8 KB/sec

@github-actions

Copy link
Copy Markdown

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.084 ± 0.069 1.994 2.192 1.03 ± 0.04
pacquet@main 2.019 ± 0.048 1.974 2.142 1.00
pnpm 5.372 ± 0.063 5.299 5.469 2.66 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.08415776044,
      "stddev": 0.06855070444357962,
      "median": 2.09442016674,
      "user": 2.65809982,
      "system": 2.0945317199999995,
      "min": 1.99396865824,
      "max": 2.19249465924,
      "times": [
        2.1278053992399997,
        1.99396865824,
        2.19249465924,
        2.15763870224,
        1.9944598682399999,
        2.0947036032399997,
        2.09413673024,
        2.0833011142399998,
        2.1001732342399997,
        2.00289563524
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.0190263005399998,
      "stddev": 0.047851310677944986,
      "median": 2.0061498152399997,
      "user": 2.7083759199999995,
      "system": 2.0612744199999997,
      "min": 1.97424926924,
      "max": 2.14195766924,
      "times": [
        1.99809636724,
        2.02507656424,
        2.14195766924,
        1.97424926924,
        2.03574634224,
        1.99088899724,
        1.99059205524,
        1.98804742724,
        2.0142032632399998,
        2.03140505024
      ]
    },
    {
      "command": "pnpm",
      "mean": 5.372065662540001,
      "stddev": 0.06264884974928055,
      "median": 5.34679472574,
      "user": 8.589173719999998,
      "system": 2.7593678199999996,
      "min": 5.29948588424,
      "max": 5.46933855124,
      "times": [
        5.32042362424,
        5.35149826124,
        5.40737119324,
        5.34000023124,
        5.44990017824,
        5.29948588424,
        5.46933855124,
        5.34209119024,
        5.4345636192399995,
        5.3059838922399996
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 483.9 ± 18.7 461.3 514.8 1.00
pacquet@main 486.6 ± 23.2 463.1 542.6 1.01 ± 0.06
pnpm 2231.2 ± 114.5 2058.1 2423.4 4.61 ± 0.30
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.4838535728,
      "stddev": 0.018670941551688944,
      "median": 0.4850899066,
      "user": 0.34285511999999996,
      "system": 0.8879916800000001,
      "min": 0.4613099491,
      "max": 0.5148187601,
      "times": [
        0.5148187601,
        0.4992555281,
        0.5045250831000001,
        0.46964376210000003,
        0.4613099491,
        0.4651146081,
        0.4872508191,
        0.4637651511,
        0.4899230731,
        0.4829289941
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.48659735810000004,
      "stddev": 0.02324181775345013,
      "median": 0.4820189166,
      "user": 0.34563422000000005,
      "system": 0.8786749799999999,
      "min": 0.4631197221,
      "max": 0.5426225741,
      "times": [
        0.5426225741,
        0.4911660331,
        0.4750851021,
        0.5021981701,
        0.4754490451,
        0.4888125371,
        0.4631197221,
        0.4746948821,
        0.4642367271,
        0.4885887881
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.2311556266,
      "stddev": 0.11449564543804464,
      "median": 2.2532653260999997,
      "user": 2.78809022,
      "system": 1.3165672799999997,
      "min": 2.0581139901,
      "max": 2.4234360171,
      "times": [
        2.4234360171,
        2.3355007591,
        2.2596787641,
        2.1643120530999997,
        2.2901073281,
        2.0592001660999997,
        2.0581139901,
        2.2648877010999997,
        2.2094675991,
        2.2468518880999997
      ]
    }
  ]
}

@zkochan zkochan merged commit d6ff1d5 into main May 13, 2026
20 checks passed
@zkochan zkochan deleted the feat/436 branch May 13, 2026 10:34
zkochan added a commit that referenced this pull request May 13, 2026
…ePackage (#436 §B+D)

Builds on §A (#440). Adds a new `pacquet-git-fetcher` crate that
handles `LockfileResolution::Git` snapshots during frozen-lockfile
installs, plus the supporting `preparePackage` port. The install
dispatcher now routes git resolutions through it instead of returning
`UnsupportedResolution { resolution_kind: "git" }`.

New crate `pacquet-git-fetcher`:

- `GitFetcher` shells out to the system `git` (clone, or `init` +
  `fetch --depth 1` on hosts in `git_shallow_hosts`), checks out the
  pinned commit, verifies it via `rev-parse HEAD`, runs
  `preparePackage`, removes `.git`, computes a packlist, and imports
  the resulting file set into the CAS. Surfaces a friendly
  `GitNotFound` when `git` isn't on `PATH` — pacquet does not bundle
  it. Mirrors `fetching/git-fetcher/src/index.ts`.

- `prepare_package` ports `exec/prepare-package/src/index.ts`. Reads
  the manifest, decides via `package_should_be_built`, honors an
  `AllowBuildFn` (the dispatcher adapts pacquet's `AllowBuildPolicy`
  into this shape), runs `<pm>-install` plus any `prepublish` /
  `prepack` / `publish` scripts via the existing
  `pacquet_executor::run_lifecycle_hook`, then deletes `node_modules`.
  Emits the upstream error codes `GIT_DEP_PREPARE_NOT_ALLOWED`,
  `ERR_PNPM_PREPARE_PACKAGE`, and `INVALID_PATH`.

- `detect_preferred_pm` sniffs the cloned tree for a lockfile to pick
  the install pm (`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, etc.),
  defaulting to npm. Workspace-root walking is deferred — git-hosted
  snapshots almost always ship a lockfile at the repo root.

- A minimal `packlist` honors the manifest's `files` field with a
  tiny `*` / `**` / `?` glob matcher, always-includes the README /
  LICENSE / package.json set, and always-excludes `.git`,
  `node_modules`, lockfiles for sibling pms, and common cruft.
  Full `.npmignore` / `.gitignore` semantics and
  `bundleDependencies` walking are deferred and tracked in module
  docs; `bundleDependencies` emits a `warn!` so the gap is visible.

Wiring in `pacquet-package-manager`:

- `InstallPackageBySnapshot` now matches `LockfileResolution::Git`
  and runs the fetcher in place of `DownloadTarballToStore`. The
  helper `tarball_url_and_integrity` factors out the per-resolution
  branch so each variant builds its own `cas_paths`.
- Threads `&AllowBuildPolicy` from `InstallFrozenLockfile` → through
  `CreateVirtualStore` → into `InstallPackageBySnapshot`, computing
  the policy once per install instead of per snapshot.
- `snapshot_cache_key` returns `Ok(None)` for `Git` resolutions so
  they go cold-batch in this PR. Warm prefetch for git-hosted slots
  is a follow-up alongside §C.

Supporting changes:

- `GitResolution.path: Option<String>` (§A back-fill) so a lockfile
  pinning a sub-directory of a git-hosted monorepo deserializes
  without tripping `deny_unknown_fields`.
- `Config::git_shallow_hosts` with pnpm's default list
  (`github.com` / `gist.github.com` / `gitlab.com` / `bitbucket.com` /
  `bitbucket.org`) and `pnpm-workspace.yaml` override support.
- `pacquet_executor::run_lifecycle_hook` is now `pub` so the
  preparePackage port can dispatch by arbitrary stage name without
  going through `run_postinstall_hooks`'s pre/install/post triple.

Out of scope (follow-ups for #436):

- §C — git-hosted *tarball* fetcher (the `gitHosted: true` shape).
- §E — full integration-test matrix (`plans/TEST_PORTING.md`
  563-572). This PR ships the seven crate-level tests against a
  local bare repo that prove the path works end-to-end.
- Warm prefetch for git resolutions. The fetcher writes nothing to
  `index.db` today, so a second install re-clones — slow but
  correct.
- Full `npm-packlist` semantics (`.npmignore`, gitignore layering,
  `bundleDependencies` walking).
zkochan added a commit that referenced this pull request May 13, 2026
…ePackage (#436 §B+D) (#446)

Builds on §A (#440). Adds a new `pacquet-git-fetcher` crate that handles `LockfileResolution::Git` snapshots during frozen-lockfile installs, plus the supporting `preparePackage` port. The install dispatcher now routes git resolutions through it instead of returning `UnsupportedResolution { resolution_kind: "git" }`.

### New crate `pacquet-git-fetcher`

- **`GitFetcher`** shells out to the system `git` (clone, or `init` + `fetch --depth 1` on hosts in `git_shallow_hosts`), checks out the pinned commit, verifies via `rev-parse HEAD`, runs `preparePackage`, removes `.git`, computes a packlist, and imports the resulting file set into the CAS. Surfaces a friendly `GitNotFound` when `git` isn't on `PATH` — pacquet does not bundle it. Mirrors [`fetching/git-fetcher/src/index.ts`](https://github.com/pnpm/pnpm/blob/94240bc046/fetching/git-fetcher/src/index.ts).
- **`prepare_package`** ports [`exec/prepare-package/src/index.ts`](https://github.com/pnpm/pnpm/blob/94240bc046/exec/prepare-package/src/index.ts). Reads the manifest, decides via `package_should_be_built`, honors an `AllowBuildFn` (the dispatcher adapts pacquet's `AllowBuildPolicy` into this shape), runs `<pm>-install` plus any `prepublish` / `prepack` / `publish` scripts via the existing `pacquet_executor::run_lifecycle_hook`, then deletes `node_modules`. Emits the upstream error codes `GIT_DEP_PREPARE_NOT_ALLOWED`, `ERR_PNPM_PREPARE_PACKAGE`, `INVALID_PATH`.
- **`detect_preferred_pm`** sniffs the cloned tree for a lockfile to pick the install pm. Workspace-root walking deferred.
- **Minimal `packlist`** honors the manifest's `files` field with a tiny `*` / `**` / `?` glob matcher (`?` and single `*` reject `/`; `**` matches across directories), always-includes the README / LICENSE / package.json set, and always-excludes `.git`, `node_modules`, lockfiles for sibling pms, and common cruft. Full `.npmignore` / `.gitignore` semantics and `bundleDependencies` walking are deferred and tracked in module docs.

### Wiring in `pacquet-package-manager`

- `InstallPackageBySnapshot` now matches `LockfileResolution::Git` and runs the fetcher in place of `DownloadTarballToStore`.
- `&AllowBuildPolicy` is computed once per install in `InstallFrozenLockfile` and threaded through `CreateVirtualStore` → `InstallPackageBySnapshot`.
- `snapshot_cache_key` returns `Ok(None)` for `Git` resolutions so they cold-batch. Warm prefetch for git-hosted slots is a follow-up alongside §C.

### Supporting changes

- `GitResolution.path: Option<String>` so a lockfile pinning a sub-directory of a git-hosted monorepo deserializes without tripping `deny_unknown_fields`.
- `Config::git_shallow_hosts` with pnpm's default list + `pnpm-workspace.yaml` override.
- `pacquet_executor::run_lifecycle_hook` is now `pub` so the preparePackage port can dispatch by arbitrary stage name.

## Out of scope (follow-ups for #436)

- §C — git-hosted *tarball* fetcher (the `gitHosted: true` shape).
- §E — full integration-test matrix (`plans/TEST_PORTING.md` 563-572). This PR ships crate-level tests against a local bare repo that prove the path works end-to-end.
- Warm prefetch for git resolutions (re-clones on every install today — slow but correct).
- Full `npm-packlist` semantics (`.npmignore`, gitignore layering, `bundleDependencies` walking).
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants