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

feat(git-fetcher): real npm-packlist semantics with .npmignore + bundleDependencies (#436 follow-up)#468

Merged
zkochan merged 3 commits into
mainfrom
feat/436-real-packlist
May 13, 2026
Merged

feat(git-fetcher): real npm-packlist semantics with .npmignore + bundleDependencies (#436 follow-up)#468
zkochan merged 3 commits into
mainfrom
feat/436-real-packlist

Conversation

@zkochan

@zkochan zkochan commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the MVP files-field-only packlist with a faithful port of npm-packlist / pnpm/fs/packlist. Four passes:

  1. .npmignore + .gitignore walk via ignore::WalkBuilder with per-directory inheritance. A .npmignore in lib/ applies to lib/** only; the root .gitignore applies to the whole tree. parents(false) keeps ignore-file lookup confined to pkg_dir so a .gitignore above the package can't leak into the published file set.
  2. files-field allowlist compiled into a single Gitignore matcher. Uses matched_path_or_any_parents so a pattern like cli matches both bin/cli (file match) and lib/cli/index.js (directory match, where lib/cli is a dir).
  3. Always-included files: package.json, README*, LICEN[SC]E*, CHANGES*, CHANGELOG*, HISTORY*, NOTICE* at the root, plus main / bin paths. Override .npmignore and the files-field filter, matching npm-packlist's alwaysIncluded. main and bin are vetted through the always-excluded check before insertion — a manifest with "main": "package-lock.json" or "bin": ".git/hook" does not get to re-add cruft.
  4. bundleDependencies / bundledDependencies recursion with cycle detection: each bundled dep gets its own packlist pass under node_modules/<name>/. Both spellings accepted plus the bundleDependencies: true shorthand. Cycle protection uses a HashSet<PathBuf> of canonical visited paths + a MAX_BUNDLE_DEPTH = 32 belt-and-braces cap so a symlink loop or self-referencing bundle can't stack-overflow the fetcher. bundleDependencies names are validated against the same path-traversal discipline cas_io::join_checked uses (Component::Normal / CurDir only; rejects .., root, drive prefix).

Always-excluded set, split by type

  • ALWAYS_EXCLUDED_DIR_SEGMENTS.git, .svn, .hg, CVS. Matched at any path segment, so VCS state is dropped at any depth.
  • ALWAYS_EXCLUDED_BASENAMES.npmrc, npm-debug.log, .DS_Store, package-lock.json, yarn.lock, pnpm-lock.yaml. Basename-only match, so a regular file like lib/cvs-tools.txt ships even though it mentions CVS.
  • ALWAYS_EXCLUDED_SUFFIXES.orig family.

New runtime dep

ignore = "0.4.25" (BurntSushi, used by ripgrep; Unlicense OR MIT, cargo deny clean). Replaces the hand-rolled * / ** / ? matcher in the old packlist.rs — the new matcher uses ignore::gitignore::GitignoreBuilder::add_line, which gives full gitignore semantics (negation, anchored vs unanchored, directory-suffix rules) for free.

Intentional upstream divergences (documented in the module header)

  • When both .npmignore and .gitignore exist in the same directory, ignore combines their rules; npm-packlist would use only .npmignore. Same outcome for the common case (a .npmignore that's a strict superset); divergence only when .npmignore explicitly includes a path .gitignore excludes — rare in published packages.
  • .git/info/exclude and global ~/.gitignore are NOT honored. The fetcher operates on a clean tarball / checkout, not the user's working tree.

Out of scope (still on #436)

  • Two-slot CAS layout (${filesIndexFile}\traw + final) for skipping the re-import when packlist == raw. Pure perf optimization.
  • §E real-npm test deferrals (need a skip-if-no-npm helper) + PATH-shim shallow-fetch test + Stage 2 resolver-side install tests.

Test plan

  • cargo nextest run -p pacquet-git-fetcher — 21 packlist tests pass:
    - 7 carried over from the MVP packlist (files-field, *.orig, glob edge cases, etc.).
    - 7 new for .npmignore / .gitignore / bundleDependencies core behaviour: npmignore_excludes_listed_paths, gitignore_excludes_when_no_npmignore, npmignore_does_not_drop_always_included_files, npmignore_in_subdir_applies_to_subtree_only, bundle_dependencies_subtree_is_included, bundled_dependencies_legacy_spelling_works, bundle_dependency_missing_dir_is_silently_skipped.
    - 4 new safety regressions from review round 1: npmignore_in_parent_dir_does_not_leak_in, bundle_dependencies_rejects_path_traversal, always_excluded_dir_segments_only_match_vcs, files_field_bare_basename_matches_at_depth.
    - 3 new safety regressions from review round 2: bundle_dependencies_self_cycle_is_caught (symlink loop), main_field_pointing_at_always_excluded_basename_is_refused, bin_field_pointing_at_vcs_segment_is_refused.
  • just ready — 928 tests, 928 pass.
  • RUSTDOCFLAGS=-D warnings cargo doc --no-deps --workspace --document-private-items — clean.
  • taplo format --check
  • just dylint
  • cargo deny check — license + bans + advisories clean for the new ignore dep.

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

Copilot AI review requested due to automatic review settings May 13, 2026 16:18
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Rewrites packlist to use the ignore crate for .npmignore/.gitignore, compiles manifest files into a Gitignore matcher, centralizes exclusion rules, re-adds root/main/bin files, and recursively includes declared bundled dependencies with cycle/depth protections. Tests added covering ignore precedence, bundled deps, security, and edge cases.

Changes

Packlist algorithm upgrade with ignore crate integration

Layer / File(s) Summary
Add ignore crate dependency
Cargo.toml, crates/git-fetcher/Cargo.toml
Workspace dependency ignore = "0.4.25" is added and the git-fetcher crate uses the workspace ignore dependency.
Packlist algorithm and helpers
crates/git-fetcher/src/packlist.rs
Rewrites packlist into four passes: ignore-based tree walk honoring .npmignore/.gitignore, re-add root always-included files, force-include main/bin, and recursively compute/splice packlists for bundleDependencies/bundledDependencies. Adds *.orig suffix exclusion, manifest files -> Gitignore matcher, path normalization, bundled-deps extraction (including bundleDependencies: true), cycle/depth protections, and ignore::Error -> io::Error adapter.
Packlist behavior tests
crates/git-fetcher/src/packlist/tests.rs
Adds unit tests for .npmignore exclusion, .gitignore precedence and scoping, always-included file protection, files basename matching behavior, bundleDependencies/bundledDependencies handling (including safety and cycles), and main/bin rejection for excluded paths.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant PacklistFn as packlist()
  participant Matcher as files matcher
  participant IgnoreWalker as ignore crate walker
  participant BundleHandler as bundled deps

  Caller->>PacklistFn: call(pkg_dir, manifest)
  PacklistFn->>Matcher: compile manifest.files into Gitignore matcher
  PacklistFn->>IgnoreWalker: walk tree with .npmignore/.gitignore
  IgnoreWalker->>PacklistFn: stream entries (filtered)
  PacklistFn->>Matcher: check files_field inclusion
  PacklistFn->>PacklistFn: re-add root always-included files
  PacklistFn->>PacklistFn: force-include main and bin paths
  PacklistFn->>BundleHandler: read bundleDependencies keys and splice results
  BundleHandler->>PacklistFn: return bundled subtree entries under node_modules/<name>/
  PacklistFn->>Caller: return combined file list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • anthonyshew

Poem

🐰 I hopped through trees of files and rules,
Kept README safe and skirted noisy tools.
Bundled friends tucked under node_modules' care,
Ignores obeyed and cycles caught with flair.
A tiny patch, but tidy — carrots shared! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing real npm-packlist semantics including .npmignore and bundleDependencies support.
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.
Description check ✅ Passed PR description is comprehensive and follows the template with clear Summary, Linked issue, and Checklist sections, though Upstream reference is minimal.

✏️ 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-real-packlist

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

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.83234% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.57%. Comparing base (c614d64) to head (141b8cd).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
crates/git-fetcher/src/packlist.rs 83.83% 27 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #468      +/-   ##
==========================================
- Coverage   88.64%   88.57%   -0.08%     
==========================================
  Files         116      116              
  Lines        9952    10048      +96     
==========================================
+ Hits         8822     8900      +78     
- Misses       1130     1148      +18     

☔ 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.

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

Replaces the MVP files-field-only packlist in crates/git-fetcher with a faithful port of npm-packlist / pnpm fs/packlist. The walker now honors per-directory .npmignore/.gitignore, treats the files field as a gitignore-style allowlist, force-includes the standard always-included files plus main/bin, and recurses into bundleDependencies (and the legacy bundledDependencies / true shorthand). The hand-rolled glob matcher is removed in favor of the ignore crate.

Changes:

  • New 4-pass packlist() using ignore::WalkBuilder and gitignore::Gitignore, with bundleDependencies recursion under node_modules/<name>/.
  • Adds ignore = "0.4.25" workspace dependency and wires it into pacquet-git-fetcher.
  • Expands packlist/tests.rs with 7 new unit tests covering .npmignore/.gitignore, always-included overrides, nested .npmignore, both bundle-deps spellings, and a missing-bundle-dir case.

Reviewed changes

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

Show a summary per file
File Description
crates/git-fetcher/src/packlist.rs Rewrites the packlist algorithm to use ignore + four explicit passes, including bundleDependencies recursion.
crates/git-fetcher/src/packlist/tests.rs Adds 7 tests covering the new .npmignore, always-included, bundleDependencies, and missing-dir behaviors.
crates/git-fetcher/Cargo.toml Adds ignore workspace dep.
Cargo.toml Adds ignore = "0.4.25" to [workspace.dependencies].
Cargo.lock Lockfile entry for ignore 0.4.25 and its transitive deps.
Comments suppressed due to low confidence (1)

crates/git-fetcher/src/packlist.rs:204

  • There is no cycle guard on bundleDependencies recursion. If a bundled dep at node_modules/a itself bundles b which is materialized at node_modules/a/node_modules/b/node_modules/a/..., the recursive packlist() will not terminate. Consider tracking visited canonical pkg_dir paths and short-circuiting on repeats, or imposing a depth limit, to keep a malformed git-hosted package from hanging the fetcher.
    for bundle_name in bundle_dep_names(manifest) {
        let bundle_pkg_dir = pkg_dir.join("node_modules").join(&bundle_name);
        if !bundle_pkg_dir.is_dir() {
            tracing::debug!(
                target: "pacquet::git_fetcher::packlist",
                bundle_name = %bundle_name,
                pkg_dir = %pkg_dir.display(),
                "bundleDependencies entry not present under node_modules/; skipping",
            );
            continue;
        }
        let bundle_manifest = safe_read_package_json_from_dir(&bundle_pkg_dir)
            .ok()
            .flatten()
            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
        let nested = packlist(&bundle_pkg_dir, &bundle_manifest)?;
        for rel in nested {
            out.insert(format!("node_modules/{bundle_name}/{rel}"));
        }
    }

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

Comment thread crates/git-fetcher/src/packlist.rs
Comment thread crates/git-fetcher/src/packlist.rs
Comment thread crates/git-fetcher/src/packlist.rs Outdated
Comment thread crates/git-fetcher/src/packlist.rs
…leDependencies (#436 follow-up)

Replaces the MVP `files`-field-only packlist with a faithful port of
[`npm-packlist`](https://github.com/npm/npm-packlist) /
[`pnpm/fs/packlist`](https://github.com/pnpm/pnpm/blob/94240bc046/fs/packlist/src/index.ts).
Four passes:

  1. Walk via `ignore::WalkBuilder` configured for `.npmignore` +
     `.gitignore` per-directory inheritance.
  2. Apply the `files`-field allowlist on top, compiled into a single
     `Gitignore` matcher.
  3. Always-include `package.json`, `README*`, `LICEN[SC]E*`,
     `CHANGES*`, `CHANGELOG*`, `HISTORY*`, `NOTICE*` at the root, plus
     `main` / `bin` paths — these override `.npmignore` *and* the
     `files`-field filter, matching npm-packlist's `alwaysIncluded`.
  4. Recurse into `bundleDependencies` (and the legacy
     `bundledDependencies`) under `node_modules/<name>/`. Each
     bundled dep gets its own `packlist()` pass and the result
     splices in under its name. Accepts both `[<name>]` arrays and
     `true` (bundle every entry in `dependencies`, npm's lesser-known
     shorthand).

New runtime dep: `ignore = "0.4.25"` (BurntSushi, used by ripgrep;
MIT/Unlicense, `cargo deny` clean). Replaces the hand-rolled
`*` / `**` / `?` glob matcher in the old packlist.rs — the new
matcher uses `ignore::gitignore::GitignoreBuilder::add_line`, which
gives us full gitignore semantics (negation, anchored vs unanchored,
directory-suffix rules) for free.

Two intentional divergences from upstream documented in the module
header:

  - When both `.npmignore` and `.gitignore` exist in the same
    directory, `ignore` combines their rules; npm-packlist would
    use only `.npmignore`. Same outcome for the common case (a
    `.npmignore` that's a strict superset); divergence only when
    `.npmignore` explicitly includes a path `.gitignore` excludes
    (rare in published packages).
  - `.git/info/exclude` and global `~/.gitignore` are NOT honored.
    The fetcher operates on a clean tarball / checkout, not the
    user's working tree.

Tests: 7 new packlist tests cover `.npmignore` exclusion,
`.gitignore` fallback when no `.npmignore`, always-included
override of `.npmignore`, per-subdir `.npmignore` inheritance,
`bundleDependencies` subtree inclusion, legacy `bundledDependencies`
spelling, and the missing-bundle-dir graceful skip. All 7 existing
packlist tests still pass under the new implementation (the
`*` / `**` / `?` glob assertions still hold because the `ignore`
crate's globset is a superset of what the old matcher did).
@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00     15.7±0.38ms   276.8 KB/sec    1.01     15.8±0.47ms   275.0 KB/sec

@zkochan zkochan force-pushed the feat/436-real-packlist branch from cf50582 to 8aa2341 Compare May 13, 2026 16:29
…versal, dir-vs-basename excludes (#468 review)

Four CodeRabbit findings on the npm-packlist port:

- **`WalkBuilder::parents` defaults to `true`**. `ignore` would
  search above `pkg_dir` for `.gitignore` / `.npmignore` and apply
  them to the walk; for a packlist the pack must depend only on the
  package's own contents. Adding `.parents(false)` to the builder
  isolates the walk to `pkg_dir`. Test:
  `npmignore_in_parent_dir_does_not_leak_in` plants a `.gitignore`
  in the temp parent and asserts `index.js` still ships.
- **`bundleDependencies` path traversal**. `bundle_name` was joined
  into `pkg_dir/node_modules/<name>` without validation; a malicious
  manifest with `bundleDependencies: ["../../etc"]` could escape.
  New `is_safe_bundle_name` runs the same `Component`-based check
  `cas_io::join_checked` uses: only `Normal` and `CurDir` components
  pass; `ParentDir`, `RootDir`, `Prefix` are refused. Scoped names
  like `@scope/foo` still pass (their `/` resolves under
  `node_modules`, not above it). Test:
  `bundle_dependencies_rejects_path_traversal`.
- **`should_always_exclude` over-eager segment walk**. The single
  list mixed dir-typed exclusions (`.git`, `.svn`, `.hg`, `CVS`)
  with file-typed exclusions (`.npmrc`, lockfiles, debug logs).
  Splitting into `ALWAYS_EXCLUDED_DIR_SEGMENTS` (matched at any
  segment) and `ALWAYS_EXCLUDED_BASENAMES` (basename-only) keeps
  the VCS-state filter while removing the dead double-fire on
  files. Test: `always_excluded_dir_segments_only_match_vcs` plants
  both a `lib/CVS/Root` (must exclude) and `lib/cvs-tools.txt`
  (must include).
- **`files_field_includes` leaf fallback was redundant *and* wrong**.
  `Gitignore::matched` correctly handles unanchored patterns at any
  depth (verified empirically: `cli` matches `bin/cli`). The leaf
  fallback was dead code. But it also failed to handle the
  directory-include case (`files: ["cli"]` should include
  `lib/cli/index.js` if `lib/cli` is a directory matching the
  pattern). Switch to `matched_path_or_any_parents`, which walks
  ancestor segments and returns `Ignore` when any segment matches.
  Test: `files_field_bare_basename_matches_at_depth` pins root +
  nested file + directory-match cases.

No production code touched outside `packlist.rs`. 18 packlist tests
pass (14 from the first commit + 4 new regressions). `just ready`:
925 → 925 passes locally.
Copilot AI review requested due to automatic review settings May 13, 2026 16:42

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

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

Comment thread crates/git-fetcher/src/packlist.rs
Comment thread crates/git-fetcher/src/packlist.rs Outdated
…ain/bin (#468 review round 2)

Two more CodeRabbit findings:

- **Recursive `packlist()` had no depth limit or cycle detection.**
  A bundled dep whose own `bundleDependencies` points back at itself
  (or any symlink loop in `node_modules`) would stack-overflow the
  fetcher. Refactor `packlist` into an inner `packlist_inner` that
  threads a `HashSet<PathBuf>` of canonical visited paths plus a
  `depth: u32` counter through every recursion. `fs::canonicalize`
  resolves symlinks so a loop shows up as the same path; the
  `MAX_BUNDLE_DEPTH = 32` is a belt-and-braces guard against any
  cycle the canonical-path check can't see (filesystem mount
  tricks, etc.). Test:
  `bundle_dependencies_self_cycle_is_caught` plants a symlink loop
  through `node_modules/self/node_modules/self`, declares a
  self-bundling manifest, asserts the recursion is cut after one
  level.
- **Pass 3 force-included `main` / `bin` without consulting
  `should_always_exclude`.** A manifest with
  `"main": "package-lock.json"` would re-add the lockfile after
  pass 1 filtered it. Run both `main` and `bin` paths through
  `should_always_exclude` before insertion — the always-excluded
  set wins over manifest fields, matching npm-packlist's silent
  override. Tests:
  `main_field_pointing_at_always_excluded_basename_is_refused`
  (lockfile case) and `bin_field_pointing_at_vcs_segment_is_refused`
  (`.git/`-prefixed path case).

No production code changes outside `packlist.rs`. Local test count:
925 → 928. `cargo deny`, dylint, doc-deny-warnings all green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/git-fetcher/src/packlist.rs (1)

393-415: 💤 Low value

Minor edge case: bundle name . would be accepted.

If bundleDependencies contains ".", the validation passes (CurDir is allowed), and pkg_dir.join("node_modules").join(".") resolves to pkg_dir/node_modules itself. This would attempt to recurse into node_modules/ as if it were a package.

In practice this is unlikely to occur since . is not a valid npm package name, and node_modules/package.json typically doesn't exist. The primary defense against .. traversal is working correctly. Consider rejecting names that resolve to empty or consist only of . components if you want to tighten this further:

🛡️ Optional tightening
 fn is_safe_bundle_name(name: &str) -> bool {
     if name.is_empty() {
         return false;
     }
     let path = std::path::Path::new(name);
     if path.is_absolute() {
         return false;
     }
+    let mut has_normal = false;
     for component in path.components() {
         match component {
-            std::path::Component::Normal(_) => {}
+            std::path::Component::Normal(_) => { has_normal = true; }
             std::path::Component::ParentDir
             | std::path::Component::RootDir
             | std::path::Component::Prefix(_) => {
                 return false;
             }
             // `.` components are stripped silently — `./foo` resolves
             // the same as `foo` on every platform.
             std::path::Component::CurDir => {}
         }
     }
-    true
+    has_normal
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/git-fetcher/src/packlist.rs` around lines 393 - 415, The function
is_safe_bundle_name currently allows a bare "." name via
std::path::Component::CurDir; update is_safe_bundle_name to explicitly reject
names that are just "." or that normalize to only CurDir components: after the
empty and absolute checks, return false if name == "." or if
path.components().all(|c| matches!(c, std::path::Component::CurDir)); keep the
existing disallow list for ParentDir/RootDir/Prefix and preserve other behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/git-fetcher/src/packlist.rs`:
- Around line 393-415: The function is_safe_bundle_name currently allows a bare
"." name via std::path::Component::CurDir; update is_safe_bundle_name to
explicitly reject names that are just "." or that normalize to only CurDir
components: after the empty and absolute checks, return false if name == "." or
if path.components().all(|c| matches!(c, std::path::Component::CurDir)); keep
the existing disallow list for ParentDir/RootDir/Prefix and preserve other
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ffad2d7e-5aee-4486-a345-5c6a441a1e2e

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa2341 and 141b8cd.

📒 Files selected for processing (2)
  • crates/git-fetcher/src/packlist.rs
  • crates/git-fetcher/src/packlist/tests.rs
📜 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). (6)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-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/git-fetcher/src/packlist/tests.rs
  • crates/git-fetcher/src/packlist.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/git-fetcher/src/packlist/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/git-fetcher/src/packlist/tests.rs
  • crates/git-fetcher/src/packlist.rs
🔇 Additional comments (21)
crates/git-fetcher/src/packlist.rs (13)

1-49: LGTM!


51-84: LGTM!


90-131: LGTM!


145-195: LGTM!


197-238: LGTM!


240-278: LGTM!


283-325: LGTM!


327-342: LGTM!


344-357: LGTM!


359-366: LGTM!


368-383: LGTM!


417-437: LGTM!


439-463: LGTM!

crates/git-fetcher/src/packlist/tests.rs (8)

1-44: LGTM!


46-94: LGTM!


96-158: LGTM!


159-241: LGTM!


243-305: LGTM!


307-393: LGTM!


395-476: LGTM!


478-529: LGTM!

@github-actions

Copy link
Copy Markdown

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.142 ± 0.090 2.028 2.315 1.01 ± 0.06
pacquet@main 2.113 ± 0.075 2.027 2.235 1.00
pnpm 5.339 ± 0.049 5.271 5.427 2.53 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.1416566493399998,
      "stddev": 0.0904564328222664,
      "median": 2.1177318321399996,
      "user": 2.71823808,
      "system": 2.12927274,
      "min": 2.02783760864,
      "max": 2.31478877564,
      "times": [
        2.2334458906399997,
        2.02783760864,
        2.1255782066399997,
        2.0902521246399997,
        2.0347339226399996,
        2.09713628364,
        2.19247074764,
        2.19043747564,
        2.10988545764,
        2.31478877564
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.11319864914,
      "stddev": 0.07543856045353234,
      "median": 2.10035476164,
      "user": 2.68716948,
      "system": 2.14028554,
      "min": 2.02687580564,
      "max": 2.23488773364,
      "times": [
        2.08446285364,
        2.02687580564,
        2.0355271026399997,
        2.18484261264,
        2.06095341864,
        2.0423744356399998,
        2.20517345564,
        2.1162466696399997,
        2.14064240364,
        2.23488773364
      ]
    },
    {
      "command": "pnpm",
      "mean": 5.339317037340001,
      "stddev": 0.04858982432602792,
      "median": 5.33115434464,
      "user": 8.61042938,
      "system": 2.69370734,
      "min": 5.27132011364,
      "max": 5.42717210264,
      "times": [
        5.42717210264,
        5.38037035364,
        5.391129766640001,
        5.3041227516400005,
        5.353317474640001,
        5.29728051264,
        5.30614860864,
        5.27132011364,
        5.32402734464,
        5.33828134464
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 538.7 ± 46.0 505.4 658.3 1.00
pacquet@main 629.4 ± 62.0 571.8 789.5 1.17 ± 0.15
pnpm 2105.4 ± 69.4 2000.9 2212.1 3.91 ± 0.36
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.5387498851799999,
      "stddev": 0.045955947753293945,
      "median": 0.51873091208,
      "user": 0.36655486,
      "system": 0.9549635999999999,
      "min": 0.50544199758,
      "max": 0.65830282958,
      "times": [
        0.65830282958,
        0.51977137058,
        0.51176597758,
        0.50544199758,
        0.51769045358,
        0.56285927058,
        0.51559844658,
        0.51663126758,
        0.55496728558,
        0.52446995258
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.62935082258,
      "stddev": 0.06198814461213402,
      "median": 0.61905508658,
      "user": 0.37442756,
      "system": 0.9554202,
      "min": 0.57180072258,
      "max": 0.78946010958,
      "times": [
        0.78946010958,
        0.6243984325799999,
        0.63499201458,
        0.64466974858,
        0.61371174058,
        0.64489044258,
        0.58712170958,
        0.57180072258,
        0.60043092558,
        0.58203237958
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.1053951213800004,
      "stddev": 0.06938621196694894,
      "median": 2.10458274608,
      "user": 2.79143266,
      "system": 1.2785768999999998,
      "min": 2.0009420285800004,
      "max": 2.2120577795800003,
      "times": [
        2.0997278135800004,
        2.0976499525800003,
        2.17409619958,
        2.2120577795800003,
        2.0009420285800004,
        2.0028287115800003,
        2.10943767858,
        2.1607501675800003,
        2.13378072358,
        2.06268015858
      ]
    }
  ]
}

@zkochan zkochan merged commit c0aff20 into main May 13, 2026
14 of 16 checks passed
@zkochan zkochan deleted the feat/436-real-packlist branch May 13, 2026 18:23
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