Skip to content

feat: enhance drift visualization with content drift and orphan detection (go4dot-ov1.5)#124

Merged
nvandessel merged 1 commit intomainfrom
polecat/rust/go4dot-ov1.5@mmfenclb
Mar 7, 2026
Merged

feat: enhance drift visualization with content drift and orphan detection (go4dot-ov1.5)#124
nvandessel merged 1 commit intomainfrom
polecat/rust/go4dot-ov1.5@mmfenclb

Conversation

@nvandessel
Copy link
Copy Markdown
Owner

@nvandessel nvandessel commented Mar 7, 2026

Summary

  • Enhance drift visualization with content drift and orphan detection
  • Source issue: go4dot-ov1.5
  • MR: go-wisp-75aj (pre-verified by witness)

Merge queue processing by Refinery.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added content drift detection to identify files with differing contents between source and destination.
    • Added orphan file tracking to identify untracked files in destination directories.
    • Enhanced drift status display in the dashboard with new visual indicators for content drift and untracked files.

…tion (go4dot-ov1.5)

Add two new drift detection capabilities:
- ContentDriftFiles: detect conflict files where dest content differs from source
- OrphanFiles: detect files in dest managed dirs not tracked by source

Enhanced visualization across CLI and dashboard:
- Status output shows content drift (≠) and untracked (?) counts
- Details panel shows DRIFT STATUS summary section
- File tree shows inline drift indicators (≠ for content drift, ? for orphans)
- Conflict files annotated with [content differs] when content has drifted

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nvandessel nvandessel merged commit 4e14ad1 into main Mar 7, 2026
3 of 4 checks passed
@nvandessel nvandessel deleted the polecat/rust/go4dot-ov1.5@mmfenclb branch March 7, 2026 16:43
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 7, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 03658b3a-e86b-4192-8bcd-511c409f3b3d

📥 Commits

Reviewing files that changed from the base of the PR and between bbc2ab4 and 39c1924.

📒 Files selected for processing (5)
  • internal/status/gather.go
  • internal/status/render.go
  • internal/stow/drift.go
  • internal/stow/symlink.go
  • internal/ui/dashboard/details_panel.go

📝 Walkthrough

Walkthrough

The PR extends the drift detection system to track content differences and orphaned files. New fields are added to ConfigStatus to record content drift and orphan counts. The drift engine now compares file contents and identifies untracked files in managed directories. Status rendering and UI dashboards are updated to display these new drift indicators.

Changes

Cohort / File(s) Summary
Drift Detection
internal/stow/drift.go, internal/stow/symlink.go
Adds ContentDriftFiles and OrphanFiles to DriftResult; implements hasContentDrift for byte-level file comparison; introduces findOrphanFiles to detect unmanaged files in config-managed directories; updates DriftSummary with TotalContentDrift and TotalOrphans counts.
Status Management
internal/status/gather.go
Adds ContentDrift and Orphans fields to ConfigStatus struct; populates ContentDrift when drift exists and Orphans is always set from drift results.
Status Rendering
internal/status/render.go
Appends content drift (≠ format) and orphan (\? format) indicators to drift detail display alongside existing conflict indicators.
UI Dashboard
internal/ui/dashboard/details_panel.go
Enhances DRIFT STATUS block to display content drift and orphan counts; augments file tree with orphan nodes and content-drift markers; adds helper functions (addOrphansToTree, markContentDriftInTree) to annotate tree nodes.

Sequence Diagram

sequenceDiagram
    participant DriftEngine as Drift Engine
    participant ContentCheck as Content Comparison
    participant OrphanDetect as Orphan Detector
    participant StatusGather as Status Gatherer
    participant StatusRender as Status Renderer
    participant UIDisplay as UI Dashboard

    DriftEngine->>ContentCheck: hasContentDrift(source, dest)
    ContentCheck->>ContentCheck: Compare file contents
    ContentCheck-->>DriftEngine: Return drift status
    DriftEngine->>OrphanDetect: findOrphanFiles(configPath, home)
    OrphanDetect->>OrphanDetect: Walk config & home dirs
    OrphanDetect-->>DriftEngine: Return orphan files list
    DriftEngine-->>DriftEngine: Populate ContentDriftFiles & OrphanFiles
    
    StatusGather->>DriftEngine: Get DriftResult
    DriftEngine-->>StatusGather: Return with drift & orphan data
    StatusGather->>StatusGather: Set ContentDrift & Orphans fields
    StatusGather-->>StatusRender: Provide ConfigStatus
    
    StatusRender->>StatusRender: Format drift indicators
    StatusRender-->>UIDisplay: Return rendered drift details
    UIDisplay->>UIDisplay: Render tree with orphan/drift marks
    UIDisplay-->>UIDisplay: Display DRIFT STATUS block
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Drift now measured byte by byte,
Orphan files brought into light,
Content changes marked with care,
Trees now show what's hiding there.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch polecat/rust/go4dot-ov1.5@mmfenclb

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR extends the drift detection pipeline to report two new categories of filesystem divergence — content drift (conflict files whose byte content differs from the dotfiles source) and orphan files (untracked files in home directories managed by a config) — and surfaces both in the TUI details panel and the status text renderer.

The feature fits naturally into the existing DriftResult/DriftSummary model and the changes are well-scoped. Three issues were found:

  • Silent false negative in hasContentDrift (internal/stow/drift.go): Files that are larger than 10 MB and happen to share the same byte count are skipped entirely; different content in such files is never detected. The skip is documented but callers receive no indication that the result is inconclusive, making it an invisible false negative for large config files.
  • Symlinks-to-directories misclassified as orphan files (internal/stow/symlink.go): entry.IsDir() returns false for a symlink whose target is a directory, so such entries pass the early-exit guard and can be reported as file orphans if they do not point into the config path. Plugin managers and other tools commonly place directory symlinks next to config files.
  • UX inconsistency: "DRIFT STATUS" shown for synced configs (internal/ui/dashboard/details_panel.go): A config that has orphan files but no conventional drift (HasDrift == false) is reported as SyncStatusSynced in the list view but renders a "DRIFT STATUS" section in the details panel. The section heading implies a problem that the list view does not acknowledge.

Confidence Score: 3/5

  • Safe to merge with low risk of breaking existing functionality, but three correctness issues should be addressed before this is relied upon in production workflows.
  • The core drift detection logic is sound and the new fields are additive. However, the symlink-to-directory misclassification in findOrphanFiles can produce false orphan reports in real-world setups (e.g. plugin managers), the silent large-file false negative in hasContentDrift could cause users to trust an incorrect "no content drift" result, and the synced/DRIFT STATUS UX inconsistency creates confusion in the dashboard.
  • internal/stow/symlink.go and internal/stow/drift.go need the most attention; internal/ui/dashboard/details_panel.go needs a UX review for the orphan-only synced config case.

Important Files Changed

Filename Overview
internal/stow/drift.go Adds ContentDriftFiles and OrphanFiles to DriftResult/DriftSummary, implements hasContentDrift comparison; silently returns false for >10MB same-size files — a potential false negative.
internal/stow/symlink.go Adds findOrphanFiles; symlinks-to-directories are not excluded before orphan classification, leading to possible false orphan reports for directory symlinks placed by other tools.
internal/status/gather.go Adds ContentDrift and Orphans fields to ConfigStatus; orphans correctly set independent of HasDrift; logic is consistent with drift state machine.
internal/status/render.go Extends driftDetails with content drift and orphan rendering; only called for drifted configs, so orphan-only synced configs are not surfaced here — minor inconsistency with the details panel behavior.
internal/ui/dashboard/details_panel.go Adds DRIFT STATUS section, orphan tree augmentation, and content-drift markers; a synced config with orphans shows a "DRIFT STATUS" heading inconsistently with the list view's synced indicator.

Sequence Diagram

sequenceDiagram
    participant G as Gatherer
    participant DC as DriftChecker
    participant FDC as FullDriftCheckWithHome
    participant HCD as hasContentDrift
    participant FOF as findOrphanFiles

    G->>DC: FullDriftCheck(cfg, dotfilesPath)
    DC->>FDC: FullDriftCheckWithHome(cfg, path, home, state)

    loop For each config
        FDC->>FDC: Walk configPath files
        alt target missing
            FDC->>FDC: append NewFiles
        else target is regular file (conflict)
            FDC->>FDC: append ConflictFiles
            FDC->>HCD: hasContentDrift(source, dest)
            HCD-->>FDC: true/false (skips >10MB same-size)
            alt content differs
                FDC->>FDC: append ContentDriftFiles
            end
        else target is symlink to wrong location
            FDC->>FDC: append ConflictFiles
            FDC->>HCD: hasContentDrift(source, dest)
            HCD-->>FDC: true/false
            alt content differs
                FDC->>FDC: append ContentDriftFiles
            end
        end
        FDC->>FOF: findOrphanFiles(configPath, home)
        FOF-->>FDC: OrphanFiles (untracked in managed dirs)
        FDC->>FDC: set HasDrift (NewFiles OR ConflictFiles OR MissingFiles)
    end

    FDC-->>DC: DriftSummary
    DC-->>G: DriftSummary

    G->>G: Build ConfigStatus per config
    note over G: ContentDrift set only when HasDrift=true\nOrphans set regardless of HasDrift
    G-->>G: Overview with Configs[]
Loading

Last reviewed commit: 39c1924

Comment on lines +237 to +240
// Skip very large files (>10MB)
if sourceInfo.Size() > 10*1024*1024 {
return false
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Silent false negative for large same-size files

When two files both exceed 10 MB and happen to have identical sizes, this block returns false (i.e. "no drift"), even if their contents actually differ. A config file that was edited so that the byte count was preserved — characters replaced without adding or removing bytes — would be silently missed. For a dotfiles manager, that scenario is common (e.g. editing a value in-place in a config).

Because the size equality is already checked on line 233, this skip only applies to the narrow case of equal-size files above 10 MB. It would be safer to return a distinct "unknown" sentinel or at least add a comment that explicitly warns callers that content drift may go undetected for large same-size files.

Suggested change
// Skip very large files (>10MB)
if sourceInfo.Size() > 10*1024*1024 {
return false
}
// Skip very large files (>10MB): content comparison is skipped for performance.
// NOTE: files with the same size but different content will NOT be detected as
// having content drift. Callers should be aware of this false-negative.
if sourceInfo.Size() > 10*1024*1024 {
return false
}

Comment on lines +210 to +231
if driftResult != nil && (driftResult.HasDrift || len(driftResult.OrphanFiles) > 0) {
lines = append(lines, headerStyle.Render("DRIFT STATUS"))
var driftParts []string
if len(driftResult.NewFiles) > 0 {
driftParts = append(driftParts, okStyle.Render(fmt.Sprintf("+%d new", len(driftResult.NewFiles))))
}
if len(driftResult.MissingFiles) > 0 {
driftParts = append(driftParts, errStyle.Render(fmt.Sprintf("-%d missing", len(driftResult.MissingFiles))))
}
if len(driftResult.ConflictFiles) > 0 {
conflictText := fmt.Sprintf("!%d conflicts", len(driftResult.ConflictFiles))
if len(driftResult.ContentDriftFiles) > 0 {
conflictText += fmt.Sprintf(" (%d content differs)", len(driftResult.ContentDriftFiles))
}
driftParts = append(driftParts, warnStyle.Render(conflictText))
}
if len(driftResult.OrphanFiles) > 0 {
driftParts = append(driftParts, subtleStyle.Render(fmt.Sprintf("?%d untracked", len(driftResult.OrphanFiles))))
}
lines = append(lines, " "+strings.Join(driftParts, ", "))
lines = append(lines, "")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"DRIFT STATUS" section shown for configs reported as synced

A config that has orphan files but no actual drift (NewFiles, MissingFiles, and ConflictFiles are all empty) will have driftResult.HasDrift == false. In gather.go this causes its ConfigStatus.Status to be set to SyncStatusSynced, so the config appears as fully synced in the list view.

However, because this panel checks len(driftResult.OrphanFiles) > 0 independently, a "DRIFT STATUS" heading is rendered in the detail view while the list already shows a green check mark. A user navigating to a config that appears synced would unexpectedly find a drift warning in the details pane.

Consider either:

  1. Surfacing orphan-only configs with a distinct status (e.g. a SyncStatusOrphaned or an informational icon), or
  2. Renaming/reframing the section (e.g. "FILESYSTEM INFO") so it doesn't imply drift on a synced config.
Suggested change
if driftResult != nil && (driftResult.HasDrift || len(driftResult.OrphanFiles) > 0) {
lines = append(lines, headerStyle.Render("DRIFT STATUS"))
var driftParts []string
if len(driftResult.NewFiles) > 0 {
driftParts = append(driftParts, okStyle.Render(fmt.Sprintf("+%d new", len(driftResult.NewFiles))))
}
if len(driftResult.MissingFiles) > 0 {
driftParts = append(driftParts, errStyle.Render(fmt.Sprintf("-%d missing", len(driftResult.MissingFiles))))
}
if len(driftResult.ConflictFiles) > 0 {
conflictText := fmt.Sprintf("!%d conflicts", len(driftResult.ConflictFiles))
if len(driftResult.ContentDriftFiles) > 0 {
conflictText += fmt.Sprintf(" (%d content differs)", len(driftResult.ContentDriftFiles))
}
driftParts = append(driftParts, warnStyle.Render(conflictText))
}
if len(driftResult.OrphanFiles) > 0 {
driftParts = append(driftParts, subtleStyle.Render(fmt.Sprintf("?%d untracked", len(driftResult.OrphanFiles))))
}
lines = append(lines, " "+strings.Join(driftParts, ", "))
lines = append(lines, "")
}
// Show drift status summary (orphans are informational and shown even on synced configs)
if driftResult != nil && (driftResult.HasDrift || len(driftResult.OrphanFiles) > 0) {
if driftResult.HasDrift {
lines = append(lines, headerStyle.Render("DRIFT STATUS"))
} else {
lines = append(lines, headerStyle.Render("FILESYSTEM INFO"))
}

Comment on lines +161 to +163
for _, entry := range entries {
if entry.IsDir() {
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Symlinks-to-directories treated as orphan files

entry.IsDir() returns false for symlinks whose target is a directory (because the type bit is ModeSymlink, not ModeDir). Such an entry will fall through the entry.IsDir() guard, reach the symlink-check block, and — if the symlink does not point back into the config path — be appended to orphans as a file-orphan even though it is actually a directory symlink.

This can produce spurious orphan reports for directories that are symlinked into a managed folder by another tool (e.g. a plugin manager placing a symlinked directory beside managed dotfiles).

Suggested change
for _, entry := range entries {
if entry.IsDir() {
continue
for _, entry := range entries {
// Skip regular directories and symlinks-to-directories
if entry.IsDir() {
continue
}
entryPath := filepath.Join(targetDir, entry.Name())
if entry.Type()&os.ModeSymlink != 0 {
if fi, err := os.Stat(entryPath); err == nil && fi.IsDir() {
continue
}
}

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 7, 2026

Codecov Report

❌ Patch coverage is 39.43662% with 86 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.25%. Comparing base (bbc2ab4) to head (39c1924).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/ui/dashboard/details_panel.go 16.90% 59 Missing ⚠️
internal/stow/drift.go 48.00% 13 Missing ⚠️
internal/stow/symlink.go 69.23% 12 Missing ⚠️
internal/status/render.go 50.00% 2 Missing ⚠️

❌ Your patch status has failed because the patch coverage (39.43%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #124      +/-   ##
==========================================
- Coverage   49.37%   49.25%   -0.12%     
==========================================
  Files         110      110              
  Lines       12977    13116     +139     
==========================================
+ Hits         6407     6460      +53     
- Misses       6570     6656      +86     
Files with missing lines Coverage Δ
internal/status/gather.go 91.25% <100.00%> (+0.34%) ⬆️
internal/status/render.go 98.14% <50.00%> (-1.22%) ⬇️
internal/stow/symlink.go 81.05% <69.23%> (-8.24%) ⬇️
internal/stow/drift.go 78.44% <48.00%> (-5.36%) ⬇️
internal/ui/dashboard/details_panel.go 41.56% <16.90%> (-5.46%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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