Skip to content

fix: detect local dotfile changes and merge profile+global dotfiles#8

Merged
paddo merged 9 commits intomainfrom
fix/dotfile-change-detection
Apr 11, 2026
Merged

fix: detect local dotfile changes and merge profile+global dotfiles#8
paddo merged 9 commits intomainfrom
fix/dotfile-change-detection

Conversation

@stefanzvonar
Copy link
Copy Markdown
Collaborator

@stefanzvonar stefanzvonar commented Apr 4, 2026

Summary

Fixes four bugs that prevent dotfile sync from working correctly when files are added to the config or edited locally. Closes #7. Supersedes #6.

Root Cause Analysis

Bug 1: No-sync-history treated as conflict (conflict.rs)

When a dotfile has no sync history (last_synced_hash is None) and local differs from remote, is_true_conflict() returns true. In daemon mode (non-interactive), conflicts can't be resolved, so the file is skipped in both import AND export — stuck indefinitely.

Fix: When last_synced_hash is None, return false — local is authoritative and the export step will push to establish a baseline.

Bug 2: Remote content not applied on first sync for create_if_missing files (sync.rs)

When a dotfile with create_if_missing = true is synced to a new machine, an app may create a default file (e.g. Claude Code writes {} to settings.json) before tether runs. Tether sees the locally-created default as a "local edit" and refuses to overwrite it with the remote version.

Fix: When last_synced_hash is None AND create_if_missing is true, bypass conflict detection entirely and let remote win. The local default is backed up before overwriting.

Bug 3: Profile dotfiles override global dotfiles (config.rs)

effective_dotfiles() returns only profile dotfiles when a profile exists, silently excluding any dotfiles defined only in the global [dotfiles].files config.

Fix: Merge profile dotfiles with global dotfiles. Profile entries take priority for duplicates (same path).

Bug 4: KeepLocal resolution doesn't clear conflict state (sync.rs)

When the user chooses "Keep Local" during interactive conflict resolution, the conflict isn't removed from state. It re-prompts on every subsequent sync.

Fix: Call conflict_state.remove_conflict(&file) in the KeepLocal branch.

Combined first-sync behavior

create_if_missing last_synced_hash Who wins
true None Remote — file was expected to come from remote (Bug 2 fix)
false None Local — file was already there, not a conflict (Bug 1 fix)
any Some(hash) Normal three-way conflict detection

Test plan

  • cargo clippy --all-targets -- -D warnings passes
  • cargo test — 216 tests pass
  • Daemon correctly syncs new create_if_missing dotfiles on first run
  • Profile dotfiles merge with global dotfiles (no silent drops)
  • KeepLocal resolution clears conflict state and doesn't re-prompt

… dotfiles

Two bugs that prevented locally-edited dotfiles from being pushed:

1. When a dotfile has no sync history (newly added to config) and local
   differs from remote, is_true_conflict() returned true. In daemon mode,
   conflicts can't be resolved interactively, so the file was skipped in
   both import AND export — stuck indefinitely. Fix: treat no-sync-history
   as "local is authoritative" (not a conflict), letting the export step
   push to establish a baseline.

2. effective_dotfiles() returned ONLY profile dotfiles when a profile
   existed, silently excluding any dotfiles only in the global [dotfiles]
   config. Fix: merge profile entries with global entries (profile takes
   priority for duplicates).

Closes #7
- Fix test_detect_conflict_returns_some_when_differ_no_history: was
  asserting is_some() but fix makes it return None. Renamed and updated.
- Add test for profile-overrides-global semantics (create_if_missing)
- Add test for disjoint profile/global dotfile sets (union behavior)
When a dotfile has create_if_missing=true and no sync history, skip
conflict detection and let remote content win. This handles the case
where an app creates a default file before tether runs (e.g. Claude Code
writing {} to settings.json on startup).

For files without create_if_missing, local remains authoritative when
there's no sync history (from the is_true_conflict fix).

Incorporates the fix from PR #6.
KeepLocal resolution wasn't calling remove_conflict, causing the user
to be re-prompted on every sync until the export step ran.
@paddo paddo force-pushed the fix/dotfile-change-detection branch from e545b25 to 70b67eb Compare April 7, 2026 15:44
paddo added 5 commits April 8, 2026 01:52
- Extract backup_and_write_dotfile helper to remove two identical backup+
  write+preserve_executable_bit blocks in decrypt_from_repo
- detect_conflict now takes pre-computed local content and hashes so the
  caller can read and hash the file once instead of twice per sync
- effective_dirs merges profile + global dirs like effective_dotfiles
@paddo paddo merged commit b29de0d into main Apr 11, 2026
1 check passed
@paddo paddo deleted the fix/dotfile-change-detection branch April 11, 2026 05:06
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.

Encrypted dotfiles not re-synced after local edits

2 participants