Skip to content

Conversation

@jovulic
Copy link

@jovulic jovulic commented Dec 15, 2025

The nh clean command now supports a --keep-one flag. When specified, this flag ensures the gcroot for each direnv project is preserved. This overrides the --keep-since flag for direnv roots.

I believe this should address #469 too, though I did make the change according to my needs (which is keeping the latest per project, which if I understand correctly, is what is tracked in /nix/var/nix/gcroots/auto for direnv gcroots).

Also, first time writing rust and first time contributor. Went for it as the change did seem straightforward enough (if I understood things correctly). Happy to make any changes as directed.

Sanity Checking

  • I have updated the changelog as per my changes
  • I have tested, and self-reviewed my code
  • Style and consistency
    • I ran nix fmt to format my Nix code
    • I ran cargo fmt to format my Rust code
    • I have added appropriate documentation to new code
    • My changes are consistent with the rest of the codebase
  • Correctness
    • I ran cargo clippy and fixed any new linter warnings.
  • If new changes are particularly complex:
    • My code includes comments in particularly complex areas to explain the
      logic
    • I have documented the motive for those changes in the PR body or commit
      description.
  • Tested on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin

Add a 👍 reaction to pull requests you find important.

Summary by CodeRabbit

  • New Features
    • Added --keep-one flag to nh clean command to preserve one garbage collection root per direnv project.

✏️ Tip: You can customize this high-level summary in your review settings.

The `nh clean` command now supports a `--keep-one` flag. When specified,
this flag ensures the gcroot for each `direnv` project is preserved.
This overrides the `--keep-since` flag for direnv roots.

nix-community#469
@coderabbitai
Copy link

coderabbitai bot commented Dec 15, 2025

Walkthrough

The changes introduce a new --keep-one CLI flag for the nh clean command that preserves at least one gcroot per direnv project, exempting matching geroots from time-based culling when the flag is active.

Changes

Cohort / File(s) Summary
CLI argument definition
src/interface.rs
Added keep_one: bool field to CleanArgs struct, exposed as --keep-one CLI flag via #[arg(long)] attribute
Command logic
src/clean.rs
Implements conditional gcroot handling: when --keep-one is set and gcroot path matches direnv pattern, the path is preserved without time-based evaluation; otherwise retains original duration-based culling logic
Documentation
CHANGELOG.md
Documented new --keep-one flag functionality for nh clean command

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Verify the direnv pattern matching logic in src/clean.rs to ensure correct gcroot identification
  • Confirm the conditional flow prioritizes direnv preservation over time-based culling when --keep-one is active
  • Check that the runtime message is appropriately logged during execution

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: adding a --keep-one flag to nh clean for direnv gcroots, which matches the core functionality implemented across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
CHANGELOG.md (1)

21-21: Align --keep-one changelog description with actual behavior

The entry says “keeps one gcroot per direnv project”, but the current implementation in src/clean.rs effectively keeps all direnv gcroots whenever --keep-one is set (no “at most one per project” enforcement). Either:

  • change the implementation to actually enforce “one per project”, or
  • reword this line (and other docs) to describe the real behavior (e.g. that direnv gcroots are exempt from time-based culling when --keep-one is used).
src/interface.rs (1)

440-443: Clarify keep_one help text to match implemented semantics

The doc says “Keep at least one gcroot per direnv project”, but the logic in clean.rs currently:

  • skips time-based culling entirely for all direnv gcroots when keep_one is true, rather than enforcing “one per project”, and
  • effectively makes keep_since not apply to direnv roots.

Once you settle on the intended behavior, please update this comment to be explicit, e.g.:

  • If you really want “one per project”: mention that older direnv gcroots for the same project may be deleted, but one (typically the newest) is always kept and --keep-since is ignored for those.
  • If you actually want “never delete direnv gcroots when this is set”: rephrase to something like “Do not delete direnv gcroots (ignores --keep-since for direnv roots)”.

This keeps --help output honest and avoids surprising users.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef8d8c4 and b11f5b9.

📒 Files selected for processing (3)
  • CHANGELOG.md (1 hunks)
  • src/clean.rs (2 hunks)
  • src/interface.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/interface.rs (1)
src/commands.rs (1)
  • arg (248-251)
⏰ 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). (5)
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: treewide-checks
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin

Comment on lines +245 to +270
let is_direnv_root = DIRENV_REGEX.is_match(&dst.to_string_lossy());
if args.keep_one && is_direnv_root {
// If --keep-one is specified AND this is a direnv gcroot, always
// keep it.
gcroots_tagged.insert(dst, false);
} else {
// For other gcroots (non-direnv, or if --keep-one is not
// specified), apply time-based culling.
let dur = now.duration_since(
dst
.symlink_metadata()
.wrap_err("Reading gcroot metadata")?
.modified()?,
);
debug!(?dur);
match dur {
Err(err) => {
warn!(?err, ?now, "Failed to compare time!");
},
Ok(val) if val <= args.keep_since.into() => {
gcroots_tagged.insert(dst, false);
},
Ok(_) => {
gcroots_tagged.insert(dst, true);
},
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--keep-one currently keeps all direnv gcroots, not “one per project”

Right now the logic is:

let is_direnv_root = DIRENV_REGEX.is_match(&dst.to_string_lossy());
if args.keep_one && is_direnv_root {
    gcroots_tagged.insert(dst, false); // always keep
} else {
    // time-based culling using args.keep_since
}

Implications:

  • For any direnv gcroot, once --keep-one is set, it is never marked for deletion, regardless of age.
  • There is no grouping by “direnv project”, and no pass that ensures “at most one gcroot per project” survives. This conflicts with:
    • the field/changelog wording (“keeps one gcroot per direnv project”), and
    • the PR description about matching Nix’s behavior of keeping only the latest per project.

If the intended behavior is indeed “keep exactly (or at least) one gcroot per direnv project while ignoring --keep-since for those roots”, you’ll need something like:

  • First pass over /nix/var/nix/gcroots/auto:
    • collect direnv gcroots into a structure keyed by some project identifier (e.g. path portion up to /.direnv/ or another stable grouping criterion),
    • collect non-direnv gcroots as you do today.
  • Second pass:
    • for each direnv project, keep the newest (by symlink_metadata().modified()) and mark older ones true (to be removed),
    • for non-direnv roots, keep using the existing keep_since logic.

As-is, users enabling --keep-one will get behavior closer to “never prune direnv gcroots”, which may lead to unbounded accumulation and doesn’t match the documented semantics.

Related UX point:

  • The output
println!("Keeping one gcroot per direnv project");
println!("Keeping paths newer than {}", args.keep_since);

is also misleading under current behavior:

  • direnv gcroots older than keep_since are still kept with --keep-one,
  • and more than one gcroot per project is kept.

Once semantics are settled, consider rephrasing these lines to describe exactly what is happening (e.g. “Direnv gcroots are exempt from time-based cleanup” or “Keeping only the newest direnv gcroot per project, regardless of age”).

Finally, tiny optional nit: you call dst.to_string_lossy() twice (once for the regexes filter above and once here). Caching it in a let dst_str = dst.to_string_lossy(); and reusing it would avoid repeated allocations, though this is a micro-optimization.

Also applies to: 285-287

Copy link
Author

@jovulic jovulic Dec 15, 2025

Choose a reason for hiding this comment

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

I believe only the latest gcroot is kept for direnv? My understand of direnv is that each project keeps a since "latest" gcroot, so what is mentioned above would not apply. I could be wrong, so I guess, if someone knowledgeable (I guess, human) can weigh in for a definitive answer.

I feel meh about the other suggestions, but happy to make them if asked.

@NotAShelf
Copy link
Member

Hi, sorry for the late response. Just wanted to let you know that I have not forgotten about this PR, but have not been in the right state of mind to provide you with a proper review---especially given that I'm not that up to date with Direnv's specifics. Once I get my remote-build-stuff PR done, I'll investigate Direnv and hopefully give you a non-AI review. For the record, AI is there mostly to catch those very rare "oh shoot, I forgot" issues and you are free to disregard it's (usually hallucinated) comments if you are confident.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants