Skip to content

feat(diff): init internal differ#192

Merged
water-sucks merged 13 commits intonix-community:mainfrom
water-sucks:create-differ-using-sqlite
Mar 25, 2026
Merged

feat(diff): init internal differ#192
water-sucks merged 13 commits intonix-community:mainfrom
water-sucks:create-differ-using-sqlite

Conversation

@water-sucks
Copy link
Copy Markdown
Collaborator

@water-sucks water-sucks commented Feb 22, 2026

This PR creates an internal differ that uses the Nix store DB to calculate the differences between two NixOS system closures, and also makes the differ settings much more configurable than just use_nvd.

This particular differ is different from tools such as nvd and dix (but takes inspiration!) in that it also queries derivers on the system for more accurate version strings than simple pname-version parsing.

use_nvd is also marked as deprecated in this PR, and will be removed in a future release.

Closes #189.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added configurable diff tool selection via differ settings (supports internal, nix, or custom command options)
    • Added new internal differ for analyzing Nix store closures directly
  • Documentation

    • Updated man pages to reflect the new differ configuration mechanism
    • Marked use_nvd setting as deprecated; use differ.tool instead
  • Chores

    • Updated dependency vendoring
    • Optimized initialization timing for command framework

@water-sucks water-sucks force-pushed the create-differ-using-sqlite branch 3 times, most recently from b06629e to 2eb3562 Compare February 23, 2026 07:53
@water-sucks water-sucks force-pushed the create-differ-using-sqlite branch from 2eb3562 to 208202b Compare March 2, 2026 00:21
@water-sucks water-sucks force-pushed the create-differ-using-sqlite branch 2 times, most recently from 7492abf to 702d074 Compare March 12, 2026 18:13
@water-sucks water-sucks force-pushed the create-differ-using-sqlite branch from 702d074 to c162141 Compare March 17, 2026 16:03
@water-sucks water-sucks changed the title feat(diff): init internal differ (WIP) feat(diff): init internal differ Mar 24, 2026
@water-sucks water-sucks force-pushed the create-differ-using-sqlite branch from fbb48fb to c457a47 Compare March 25, 2026 05:11
@water-sucks water-sucks marked this pull request as ready for review March 25, 2026 05:11
@water-sucks
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

A new builtin differ for Nix store closures has been implemented via SQLite database queries, replacing the previous use_nvd flag-based approach with a configurable differ section supporting internal, nix, and command tools. The diff functionality has been refactored from the generation package to a dedicated internal/diff package, and related command invocations across apply, generation, and rollback commands have been updated accordingly.

Changes

Cohort / File(s) Summary
Command diff runner updates
cmd/apply/apply.go, cmd/generation/diff/diff.go, cmd/generation/rollback/rollback.go, cmd/generation/switch/switch.go
Replaced generation.RunDiffCommand with diff.RunDiffCommand and updated options struct from generation.DiffCommandOptions{UseNvd} to diff.DiffCommandOptions{DiffTool, DiffToolCmd, QueryDerivations}.
Diff package implementation
internal/diff/diff.go, internal/diff/closure.go, internal/diff/display.go, internal/diff/version.go
New diff package providing builtin SQLite-based differ, closure analysis, version parsing, and formatted diff output display. Exports RunDiffCommand, ClosureDiff, PathDiff, and related types with configurable differ selection logic.
Settings configuration
internal/settings/settings.go, internal/settings/settings_test.go, internal/settings/completion.go
Added Differ configuration section with DiffTool enum (internal, nix, command), command list, and query derivations flag. Deprecated use_nvd setting with migration logic in validation. Updated test initialization and completion support.
Diff package removal
internal/generation/diff.go
Removed deprecated diff runner implementation (DiffCommandOptions struct and RunDiffCommand function) previously handling UseNvd flag and nvd fallback logic.
Root command initialization
cmd/root/root.go
Added package-level init() function to enable Cobra traverse run hooks at import time, removing the redundant statement from Execute().
Documentation updates
doc/man/nixos-cli-generation-diff.1.scd, doc/man/nixos-cli.1.scd
Updated manpage references to reflect differ configuration via differ key and changed example from use_nvd=true to differ.tool=nix.
Utilities and constants
internal/utils/set/set.go, internal/constants/constants.go
Added generic Set[T] type with constructors and operations (Intersection, Difference, Add, Remove). Added NixStoreDatabase constant for SQLite database path.
Build configuration
nix/package/unwrapped.nix
Updated vendored Go module hash for buildGo126Module.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #116 — Modifies diff invocation wiring in the apply command with different diff runner and options routing.
  • PR #111 — Changes RunDiffCommand usage and its configuration parameters across command implementations.

Suggested reviewers

  • Sporif
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(diff): init internal differ' directly describes the main change—introducing an internal differ implementation. It is concise, clear, and accurately reflects the primary objective of the changeset.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #189: a builtin internal differ using Nix store SQLite database [#189], derivation querying for version accuracy [#189], configurable differ settings [#189], and deprecation of use_nvd [#189].
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #189 requirements: internal differ implementation, settings refactoring, documentation updates, and deprecation handling. The generic Set utility and constants are necessary infrastructure supporting the core differ functionality.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.3)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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
Copy Markdown

@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: 3

🧹 Nitpick comments (4)
cmd/root/root.go (1)

32-34: Remove duplicate cobra.EnableTraverseRunHooks initialization.

After moving this to init(), the assignment in Execute() is redundant. Keep it in one place to avoid configuration drift.

Proposed cleanup
 func Execute() {
-	cobra.EnableTraverseRunHooks = true
-
 	cmd, err := mainCommand()
 	if err != nil {
 		os.Exit(1)
 	}

Also applies to: 179-181

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/root/root.go` around lines 32 - 34, The init() function already sets
cobra.EnableTraverseRunHooks, so remove the redundant assignment from Execute()
(and any other duplicate at the other mentioned location) to keep configuration
in one place; locate the duplicate use of cobra.EnableTraverseRunHooks in the
Execute() function and delete that line, leaving the init() initialization as
the sole assignment.
cmd/generation/rollback/rollback.go (1)

72-74: Consider updating the comment to reflect the new differ options.

The comment mentions "nvd" and "nix store diff-closures" but doesn't account for the new internal differ, which is now the default. Consider updating to reflect all available differ options.

📝 Suggested comment update
 	// While it is possible to use the `rollback` command, we still need
 	// to find the previous generation number ourselves in order to run
-	// `nvd` or `nix store diff-closures` properly.
+	// the configured differ properly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/generation/rollback/rollback.go` around lines 72 - 74, Update the
explanatory comment above the rollback logic to mention the new internal differ
(now the default) alongside the legacy tools; specifically reference the
`rollback` command behavior and list/acknowledge the available differ options
(e.g., the internal differ, `nvd`, and `nix store diff-closures`) so the comment
reflects current behavior and choices used when finding the previous generation
number in functions around rollback handling in rollback.go.
internal/settings/completion.go (1)

30-31: Inconsistent use of reflect.Pointer vs reflect.Ptr.

Lines 30 and 42 use reflect.Pointer, but line 60 still uses reflect.Ptr. While both are functionally equivalent (aliases since Go 1.18), consider using one consistently throughout the file for clarity.

♻️ Suggested fix for consistency
 	if current.Kind() == reflect.Ptr {
+	if current.Kind() == reflect.Pointer {
 		if current.IsNil() {
 			current.Set(reflect.New(current.Type().Elem()))
 		}
 		current = current.Elem()
 	}

Also applies to: 42-48, 60-65

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/settings/completion.go` around lines 30 - 31, The code uses both
reflect.Pointer and reflect.Ptr inconsistently (seen where current.Kind() is
compared), so standardize on one alias throughout the file (e.g., replace all
occurrences of reflect.Pointer with reflect.Ptr or vice versa) so every check
like current.Kind() == reflect.Pointer/current.Kind() == reflect.Ptr uses the
same symbol; update every instance in internal/settings/completion.go (including
the blocks around the current.Kind() checks and any other uses) to the chosen
alias for consistency.
internal/diff/diff.go (1)

218-237: Redundant changed variable.

The changed variable (line 218-221) duplicates the condition already checked at line 233. The logic can be simplified.

Proposed simplification
 			if len(uniqueOldVersions) == 0 && len(uniqueNewVersions) == 0 {
 				continue
 			}

-			changed := false
-			if len(uniqueOldVersions) > 0 && len(uniqueNewVersions) > 0 {
-				changed = true
-			}
-
 			diff := PathDiff{
 				Name: name,
 			}

 			if len(uniqueOldVersions) > 0 && len(uniqueNewVersions) == 0 {
 				diff.Change = ChangeTypeRemove
 				diff.Old = uniqueOldVersions
 			} else if len(uniqueOldVersions) == 0 && len(uniqueNewVersions) > 0 {
 				diff.Change = ChangeTypeAdd
 				diff.New = uniqueNewVersions
-			} else if changed {
+			} else {
+				// Both have unique versions - this is a change
 				diff.Change = ChangeTypeChange
 				diff.Old = uniqueOldVersions
 				diff.New = uniqueNewVersions
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/diff/diff.go` around lines 218 - 237, The local boolean variable
`changed` is redundant—remove its declaration and assignment and instead use the
direct condition when building the PathDiff: replace the `else if changed { ...
}` branch with `else if len(uniqueOldVersions) > 0 && len(uniqueNewVersions) > 0
{ ... }`, leaving the other branches that set `diff.Change` to
ChangeTypeRemove/Add and populate `diff.Old`/`diff.New` unchanged; this
simplifies the logic around constructing the PathDiff (the surrounding symbols
are `changed`, `PathDiff`, `ChangeTypeChange`, `ChangeTypeRemove`,
`ChangeTypeAdd`, and the slices `uniqueOldVersions`/`uniqueNewVersions`).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/diff/closure.go`:
- Around line 126-129: The error return after rows.Scan currently returns
partial results plus an error; make it consistent with getClosure by returning
nil and the wrapped error instead: replace the "return results, fmt.Errorf(...)"
in the rows.Scan error path with "return nil, fmt.Errorf(...)" so both the rows
scanning block and getClosure return nil on error (adjust callers if they expect
partial slices).

In `@internal/diff/display.go`:
- Around line 114-128: The formatSize function currently truncates sizes < 1024
to "0 KiB"; update formatSize so that when size < kb it returns the size in
bytes (e.g., "123 B") instead of dividing by kb, otherwise keep existing
KiB/MiB/GiB branches; modify the format logic inside formatSize to check size <
kb first and format with "%d B" for bytes, then fall through to KiB, MiB, GiB
branches.

In `@internal/settings/settings.go`:
- Around line 435-454: The migration for cfg.UseNvd currently unconditionally
overwrites any explicit differ settings; change it to only apply when the
existing cfg.Differ.Tool is the default/unspecified (e.g., empty string or the
default constant DifferNix) so we don't clobber user-provided values. In the
block that appends DeprecatedSettingError for UseNvd, guard the assignments to
cfg.Differ.Tool and cfg.Differ.Command with a conditional that checks
cfg.Differ.Tool is unset/default before setting it to DifferCommand and
[]string{"nvd","diff"}; leave explicit user settings intact. Ensure the
subsequent switch on cfg.Differ.Tool still validates and falls back as before.

---

Nitpick comments:
In `@cmd/generation/rollback/rollback.go`:
- Around line 72-74: Update the explanatory comment above the rollback logic to
mention the new internal differ (now the default) alongside the legacy tools;
specifically reference the `rollback` command behavior and list/acknowledge the
available differ options (e.g., the internal differ, `nvd`, and `nix store
diff-closures`) so the comment reflects current behavior and choices used when
finding the previous generation number in functions around rollback handling in
rollback.go.

In `@cmd/root/root.go`:
- Around line 32-34: The init() function already sets
cobra.EnableTraverseRunHooks, so remove the redundant assignment from Execute()
(and any other duplicate at the other mentioned location) to keep configuration
in one place; locate the duplicate use of cobra.EnableTraverseRunHooks in the
Execute() function and delete that line, leaving the init() initialization as
the sole assignment.

In `@internal/diff/diff.go`:
- Around line 218-237: The local boolean variable `changed` is redundant—remove
its declaration and assignment and instead use the direct condition when
building the PathDiff: replace the `else if changed { ... }` branch with `else
if len(uniqueOldVersions) > 0 && len(uniqueNewVersions) > 0 { ... }`, leaving
the other branches that set `diff.Change` to ChangeTypeRemove/Add and populate
`diff.Old`/`diff.New` unchanged; this simplifies the logic around constructing
the PathDiff (the surrounding symbols are `changed`, `PathDiff`,
`ChangeTypeChange`, `ChangeTypeRemove`, `ChangeTypeAdd`, and the slices
`uniqueOldVersions`/`uniqueNewVersions`).

In `@internal/settings/completion.go`:
- Around line 30-31: The code uses both reflect.Pointer and reflect.Ptr
inconsistently (seen where current.Kind() is compared), so standardize on one
alias throughout the file (e.g., replace all occurrences of reflect.Pointer with
reflect.Ptr or vice versa) so every check like current.Kind() ==
reflect.Pointer/current.Kind() == reflect.Ptr uses the same symbol; update every
instance in internal/settings/completion.go (including the blocks around the
current.Kind() checks and any other uses) to the chosen alias for consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03740ae1-2033-4cdd-bac8-7ef868958b29

📥 Commits

Reviewing files that changed from the base of the PR and between 35c0129 and 4f03d4e.

⛔ Files ignored due to path filters (2)
  • go.mod is excluded by !go.mod
  • go.sum is excluded by !**/*.sum, !go.sum
📒 Files selected for processing (18)
  • cmd/apply/apply.go
  • cmd/generation/diff/diff.go
  • cmd/generation/rollback/rollback.go
  • cmd/generation/switch/switch.go
  • cmd/root/root.go
  • doc/man/nixos-cli-generation-diff.1.scd
  • doc/man/nixos-cli.1.scd
  • internal/constants/constants.go
  • internal/diff/closure.go
  • internal/diff/diff.go
  • internal/diff/display.go
  • internal/diff/version.go
  • internal/generation/diff.go
  • internal/settings/completion.go
  • internal/settings/settings.go
  • internal/settings/settings_test.go
  • internal/utils/set/set.go
  • nix/package/unwrapped.nix
💤 Files with no reviewable changes (1)
  • internal/generation/diff.go

@water-sucks water-sucks force-pushed the create-differ-using-sqlite branch from 4f03d4e to 089b85d Compare March 25, 2026 08:24
@water-sucks water-sucks enabled auto-merge (rebase) March 25, 2026 08:24
@water-sucks water-sucks merged commit 23e7540 into nix-community:main Mar 25, 2026
2 checks passed
@water-sucks water-sucks deleted the create-differ-using-sqlite branch March 25, 2026 08:28
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.

diff: create a builtin differ, make more configurable

1 participant