Skip to content

Fix the Nix < 2.20 NAR hash fallback for inputs with submodules - #624

Open
joshuaspence wants to merge 1 commit into
DeterminateSystems:mainfrom
joshuaspence:fix/nix219-compat-submodules
Open

Fix the Nix < 2.20 NAR hash fallback for inputs with submodules#624
joshuaspence wants to merge 1 commit into
DeterminateSystems:mainfrom
joshuaspence:fix/nix219-compat-submodules

Conversation

@joshuaspence

@joshuaspence joshuaspence commented Sep 3, 2026

Copy link
Copy Markdown

Motivation

Git inputs fetched with submodules = true cannot be evaluated at all if their locked narHash was produced with Nix < 2.20 semantics. Instead of the intended "please update the NAR hash" warning, Nix fails hard:

error: NAR hash mismatch in input 'git+ssh://git@example.org/repo.git?rev=…&shallow=1&submodules=1',
expected 'sha256-CnkK…' but got 'sha256-XNEq…'

This affects any lock file written by Nix < 2.20, as well as locks written by Nix >= 2.20 with nix-219-compat enabled — that setting was only ever meant to control the write path, with the read path staying backwards compatible in both directions.

The failure is easy to miss, because nix flake update <input> recomputes the hash rather than verifying it. So the lock silently changes for whoever happens to touch that input next, and hard-fails for everybody else.

Context

GitInputScheme::getAccessorFromCommit() chooses between the libgit2 export and the git archive / git checkout export by hashing both candidate trees and comparing against input.getNarHash() — but it did so before mounting submodules. For submodules = true the expected NAR hash covers the fully mounted tree, while both candidates covered only the bare top-level repo, so neither could ever match and the fallback was effectively dead code.

Reproducer, using a repo whose submodule has crlf text eol=crlf in its .gitattributes (git checkout applies that filter, libgit2 doesn't):

$ nix eval --nix-219-compat --raw --expr \
    '(builtins.fetchTree { type = "git"; url = "file:///tmp/root"; rev = "…"; submodules = true; }).narHash'
sha256-iHuh…

$ nix eval --raw --expr \
    '(builtins.fetchTree { type = "git"; url = "file:///tmp/root"; rev = "…"; submodules = true; narHash = "sha256-iHuh…"; }).narHash'
error: NAR hash mismatch …

Implementation

Submodule mounting moves into a getTree(bool legacy) lambda, so both candidate trees are fully mounted before their NAR hashes are compared and the existing selection logic operates on comparable values.

Reproducing a Nix < 2.20 hash also requires exporting the submodules with Nix < 2.20 semantics: git checkout applies eol/text filters inside submodules too, so mounting libgit2-exported submodules over a git checkout-exported top level cannot reconstruct the old hash. That is propagated through the synthesized submodule inputs via a new internal __legacyExport attribute:

  • absent → we're a top-level input, and nix-219-compat decides;
  • present → we're a submodule, and we follow the top-level repo regardless of the setting.

The tri-state matters: without it, getTree(false) inside a nix-219-compat process would still export submodules the legacy way, breaking the modern-hash-under-compat direction.

__legacyExport is deliberately not in allowedAttrs() and is never serialized into a lock file or a URL, following the existing __final convention.

GitAccessorOptions gains a matching legacy field so makeFingerprint() accounts for it. It is appended last, which keeps legacy cache keys byte-identical to the previous hand-concatenated options.makeFingerprint(rev) + ";legacy" — no cache invalidation.

Testing

New regression test in tests/functional/fetchGitSubmodules.sh covering both directions plus a genuinely-bad hash. It computes the two reference hashes in a throwaway store first, because builtins.fetchTree marks inputs carrying a narHash as final, and Input::getAccessorUnchecked() then returns an already-present store path without running the fetcher at all — which masks this bug, and is probably part of why it went unnoticed.

meson test --suite main and --suite flakes pass, other than three build-remote-trustless-* tests that fail identically on an unmodified tree in my environment.

Also verified against the real-world lock entry that prompted this — a repo with submodules = 1 and shallow = 1 whose submodule tree contains 121 files affected by eol/text filters. It now evaluates and emits:

warning: Git input '…' specifies a NAR hash 'sha256-CnkK…' that was created by Nix < 2.20.
Nix >= 2.20 does not apply Git filters, `export-ignore` and `export-subst` by default, which changes the NAR hash.
Please update the NAR hash to 'sha256-XNEq…'.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed Git fetches with submodules so legacy export behavior is applied consistently across the repository and its submodules.
    • Inputs carrying internal legacy export metadata no longer trigger unsupported-attribute errors.
    • Improved handling of legacy and modern archive hashes, including appropriate warnings and validation.
  • Tests

    • Added coverage for legacy Git exports involving submodules and line-ending filters.

`GitInputScheme::getAccessorFromCommit()` decided whether a lock was
produced with Nix < 2.20 or Nix >= 2.20 export semantics *before*
mounting submodules. For an input with `submodules = true` the expected
NAR hash covers the mounted tree, while both candidate hashes covered
only the bare top-level repo, so neither could ever match. Instead of
falling back to `git archive`/`git checkout` and warning, Nix hard-failed
with a NAR hash mismatch:

    error: NAR hash mismatch in input
    'git+ssh://git@example.org/repo.git?rev=...&shallow=1&submodules=1',
    expected 'sha256-CnkK...' but got 'sha256-XNEq...'

`nix flake update <input>` recomputes rather than verifies the hash, so
the failure only showed up on the read path — anyone evaluating a lock
file written by Nix < 2.20 (or by Nix >= 2.20 with `nix-219-compat`
enabled) for a repo with submodules could not evaluate it at all.

Move the submodule mounting into a `getTree()` lambda so both candidate
trees are fully mounted before their NAR hashes are compared. Since the
Git filters that Nix < 2.20 applied also affect submodule contents,
reproducing such a hash requires exporting the submodules with the same
semantics; propagate this through the synthesized submodule inputs via a
new internal `__legacyExport` attribute (not part of `allowedAttrs()`,
never serialized into a lock file, following the `__final` convention).
When it's absent we're a top-level input and `nix-219-compat` decides;
when it's present we follow the top-level repo regardless of the setting.

`GitAccessorOptions` gains a matching `legacy` field so that
`makeFingerprint()` accounts for it. It is appended last, keeping the
legacy cache keys byte-identical to the previous hand-concatenated
`makeFingerprint(rev) + ";legacy"`.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 96f451f5-498d-47b1-a203-6995be4d865f

📥 Commits

Reviewing files that changed from the base of the PR and between 3ed5caa and 22af94b.

📒 Files selected for processing (5)
  • src/libfetchers/fetchers.cc
  • src/libfetchers/git-utils.cc
  • src/libfetchers/git.cc
  • src/libfetchers/include/nix/fetchers/git-utils.hh
  • tests/functional/fetchGitSubmodules.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Changes

The Git fetcher now propagates legacy export semantics to synthesized submodule inputs. Accessor fingerprints include the legacy mode, and NAR-hash fallback uses unified tree construction for both export modes.

Legacy Git export

Layer / File(s) Summary
Legacy export option and attribute handling
src/libfetchers/include/nix/fetchers/git-utils.hh, src/libfetchers/fetchers.cc, src/libfetchers/git.cc, src/libfetchers/git-utils.cc
GitAccessorOptions now stores the legacy mode. Internal __legacyExport attributes are accepted and read. Fingerprints distinguish legacy accessors.
Submodule export and hash fallback
src/libfetchers/git.cc
Accessor creation, submodule mounting, and alternate NAR-hash verification use one path for legacy and modern export semantics.
Submodule compatibility validation
tests/functional/fetchGitSubmodules.sh
Functional tests verify differing hashes, Git filtering, compatibility warnings, modern-hash acceptance, and mismatched-hash rejection.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 22af9

The change makes legacy Git hashes work correctly for inputs with submodules while preserving modern behavior and cache-key compatibility. No merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant GitInputScheme
  participant GitAccessor
  participant SubmoduleFetcher
  participant NARHashVerifier
  GitInputScheme->>GitAccessor: Build tree with selected export mode
  GitAccessor->>SubmoduleFetcher: Mount submodules with __legacyExport
  GitInputScheme->>NARHashVerifier: Verify selected tree hash
  NARHashVerifier-->>GitInputScheme: Report match or mismatch
  GitInputScheme->>GitAccessor: Build tree with alternate mode on mismatch
  GitAccessor->>SubmoduleFetcher: Mount submodules with alternate export mode
Loading

Suggested reviewers: edolstra, xokdvium, mic92

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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 Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing Nix < 2.20 NAR hash fallback behavior for Git inputs with submodules.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@joshuaspence
joshuaspence marked this pull request as draft September 3, 2026 08:23
@joshuaspence
joshuaspence marked this pull request as ready for review September 3, 2026 14:48
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.

1 participant