Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions internal/appsettings/appsettings.go
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,18 @@ func DetectCloudCandidate(home string) string {
return DetectDriveCandidate(home)
}

// DefaultMirrorPath is the folder `dot sync` mirrors into when a workspace
// store sets no target: "<cloud>/work" on the same cloud as the detected
// backup root, or ~/gdrive-workspace/work when no cloud root qualifies.
func DefaultMirrorPath(home string) string {
if cloud := DetectCloudCandidate(home); cloud != "" {
// cloud is "<root>/secrets/dotfiles-backup"; the mirror lives at
// "<root>/work", two levels up from the secrets marker.
return filepath.Join(filepath.Dir(filepath.Dir(cloud)), "work")
}
return filepath.Join(home, "gdrive-workspace", "work")
}

// detectDropboxCandidate scans for a Dropbox cloud root (under
// ~/Library/CloudStorage, then the ~/Dropbox symlink in home) that has a
// "secrets" subdirectory marker. CloudStorage is scanned first so the
Expand Down
27 changes: 14 additions & 13 deletions internal/cli/sync_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func newSyncCLIFixture(t *testing.T) *syncCLIFixture {
if err := os.MkdirAll(filepath.Dir(stateFile), 0o755); err != nil {
t.Fatal(err)
}
state := "modules:\n gsync:\n local_path: " + local + "\n mirror_path: " + mirror + "\n"
state := "modules:\n gsync:\n local_path: " + local + "\n"
if err := os.WriteFile(stateFile, []byte(state), 0o644); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -414,33 +414,34 @@ func TestSyncMirrorCLI_PrintAndDryRunAreReadOnly(t *testing.T) {
}
}

func TestSyncMirrorCLI_HonorsHomeOverride(t *testing.T) {
func TestSyncTargetCLI_HonorsHomeOverride(t *testing.T) {
f := newSyncCLIFixture(t)
other := t.TempDir() // a different user's home
if err := os.MkdirAll(filepath.Join(other, ".config", "dotfiles"), 0o755); err != nil {
t.Fatal(err)
}

// ~ in the path must expand against --home, and global state must be
// written for that home — not the current HOME.
// ~ in the path must expand against --home, and the store that records it
// must be that home's store — not the current user's.
out, errOut, err := runDotForTest("sync", "target", "~/Dropbox/work", "--home", other)
if err != nil {
t.Fatalf("gsync mirror --home: %v\nstdout=%s\nstderr=%s", err, out, errOut)
t.Fatalf("sync target --home: %v\nstdout=%s\nstderr=%s", err, out, errOut)
}
wantMirror := filepath.Join(other, "Dropbox", "work")
if !strings.Contains(out, wantMirror) {
t.Errorf("~ should expand against --home %q:\n%s", wantMirror, out)
}
otherCfg, err := os.ReadFile(filepath.Join(other, ".config", "dotfiles", "config.yaml"))
otherStore := filepath.Join(other, "workspace", "work", ".dotfiles", "sync", "config.yaml")
otherCfg, err := os.ReadFile(otherStore)
if err != nil {
t.Fatalf("read --home state: %v", err)
t.Fatalf("read --home store: %v", err)
}
if !strings.Contains(string(otherCfg), "mirror_path: "+wantMirror) {
t.Errorf("--home global state missing mirror_path:\n%s", otherCfg)
if !strings.Contains(string(otherCfg), "target: local:"+wantMirror) {
t.Errorf("--home store missing the target:\n%s", otherCfg)
}
// The current user's state must NOT have been written with this mirror.
if cur, _ := os.ReadFile(filepath.Join(f.home, ".config", "dotfiles", "config.yaml")); strings.Contains(string(cur), wantMirror) {
t.Errorf("current-user state wrongly written under --home:\n%s", cur)
// The current user's store must NOT have been written with this target.
if cur, _ := os.ReadFile(filepath.Join(f.local, ".dotfiles", "sync", "config.yaml")); strings.Contains(string(cur), wantMirror) {
t.Errorf("current-user store wrongly written under --home:\n%s", cur)
}
}

Expand Down Expand Up @@ -580,7 +581,7 @@ func TestSyncPushRefusesLegacyOnlyStore(t *testing.T) {
f := newSyncCLIFixture(t)
legacy := filepath.Join(f.local, ".dotfiles", "gdrive-sync")
writeCLITestFile(t, filepath.Join(legacy, "baseline.manifest"), "docs/a.md\t1\n")
writeCLITestFile(t, filepath.Join(legacy, "config.yaml"), "mirror_path: "+f.mirror+"\n")
writeCLITestFile(t, filepath.Join(legacy, "config.yaml"), "target: local:"+f.mirror+"\n")
writeCLITestFile(t, filepath.Join(f.local, "docs", "a.md"), "local edit\n")
writeCLITestFile(t, filepath.Join(f.mirror, "docs", "a.md"), "mirror copy\n")
before := snapshotTree(t, f.home)
Expand Down
1 change: 0 additions & 1 deletion internal/cli/sync_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ func runSyncConfigure(cmd *cobra.Command, _ []string) error {
}
if target.Kind == syncer.TargetLocal {
target.Path = appsettings.ExpandHome(target.Path, homeFor(cmd))
local.MirrorPath = target.Path
}
local.Target = target.String()
}
Expand Down
38 changes: 9 additions & 29 deletions internal/cli/sync_target_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,39 +68,19 @@ func runSyncTarget(cmd *cobra.Command, args []string) error {
p.KV("Target", bs.Config.Target.String())
return nil
}
p.Line("[dry-run] would set target to %s (local config%s)", target.String(),
map[bool]string{true: " + global state", false: ""}[target.Kind == syncer.TargetLocal])
p.Line("[dry-run] would set target to %s (local config)", target.String())
return nil
}

homeOverride, _ := cmd.Flags().GetString("home")

// Local config governs the current workspace (global state is ignored
// once it exists), so write it for immediate effect — but only for the
// current user. Under --home the admin isn't in the target user's
// workspace, so the local config (always current-workspace) doesn't
// apply; only the home-aware global state below is meaningful there.
if homeOverride == "" {
bs, err := syncer.Bootstrap(syncBootstrapOptions(cmd, false))
if err != nil {
return err
}
if err := syncer.SetLocalTarget(bs.Config, target); err != nil {
return err
}
// The store belongs to the workspace and the workspace belongs to the home:
// Bootstrap resolved both against --home, so an admin running this for
// another user writes that user's store, not the invoking user's.
bs, err := syncer.Bootstrap(syncBootstrapOptions(cmd, false))
if err != nil {
return err
}

// Global state, home-aware: local targets are inherited by future
// workspaces. SSH targets stay workspace-local.
if target.Kind == syncer.TargetLocal {
state, err := loadStateForCmd(cmd)
if err != nil {
return fmt.Errorf("load global state: %w", err)
}
state.Modules.Gsync.MirrorPath = target.Path
if err := persistUserState(cmd, state); err != nil {
p.Warn("could not update global state: %v", err)
}
if err := syncer.SetLocalTarget(bs.Config, target); err != nil {
return err
}

p.Line("%s", ui.StyleSuccess.Render("✓ sync target set"))
Expand Down
Loading
Loading