From 6647c0cf5f9e2326ac313a09190e630968d0157e Mon Sep 17 00:00:00 2001 From: laggu91 Date: Thu, 19 Feb 2026 20:51:30 +0900 Subject: [PATCH 1/4] test: improve gitvolume package coverage from 62.9% to 80% - Add fs_test.go: copyFile, copyDir, hashFile, hashDir, cleanEmptyParents - Add remove_test.go: GlobalRemove, beforeAllRemove, beforeRemove, remove, afterRemove - Add git_test.go: FindWorktreeRoot, findCommonDir, isBareRepository - Add init_test.go: Init, beforeInit, init, afterInit (quiet/non-quiet/error) - Enhance sync_test.go: symlink rejection, path traversal, verbose, non-quiet error - Enhance unsync_test.go: type mismatch (source dir vs target file) - Enhance context_test.go: empty config, CheckStatus (9 subtests) - Enhance list_test.go: GlobalList, printNode - Enhance status_test.go: afterAllStatus - Enhance gitvolume_test.go: accessor methods, validatePath .git blocking - Fix git_repro_test.go: remove redundant git init --bare causing clone failure Closes #29 --- internal/gitvolume/context_test.go | 115 ++++++++++++++ .../gitvolume/debug_find_common_dir_test.go | 129 ++++++++++++++++ internal/gitvolume/fs_test.go | 140 ++++++++++++++++++ internal/gitvolume/git_repro_test.go | 114 ++++++++++++++ internal/gitvolume/git_test.go | 85 +++++++++++ internal/gitvolume/gitvolume_test.go | 30 ++++ internal/gitvolume/init_test.go | 115 ++++++++++++++ internal/gitvolume/list_test.go | 62 ++++++++ internal/gitvolume/remove_test.go | 73 +++++++++ internal/gitvolume/status_test.go | 22 +++ internal/gitvolume/sync_test.go | 98 ++++++++++++ internal/gitvolume/unsync_test.go | 29 ++++ 12 files changed, 1012 insertions(+) create mode 100644 internal/gitvolume/debug_find_common_dir_test.go create mode 100644 internal/gitvolume/fs_test.go create mode 100644 internal/gitvolume/git_repro_test.go create mode 100644 internal/gitvolume/git_test.go create mode 100644 internal/gitvolume/init_test.go create mode 100644 internal/gitvolume/remove_test.go diff --git a/internal/gitvolume/context_test.go b/internal/gitvolume/context_test.go index bcdf820..9f22316 100644 --- a/internal/gitvolume/context_test.go +++ b/internal/gitvolume/context_test.go @@ -321,6 +321,25 @@ func TestNewWorkspace_NoConfig(t *testing.T) { assert.Error(t, ctx.Load("", true)) } +func TestNewWorkspace_EmptyConfig(t *testing.T) { + repoDir, cleanup := setupTestGitRepo(t) + defer cleanup() + + configPath := filepath.Join(repoDir, ConfigFileName) + require.NoError(t, os.WriteFile(configPath, []byte(""), 0644)) + + // Change to repo dir + oldDir, _ := os.Getwd() + defer func() { _ = os.Chdir(oldDir) }() + require.NoError(t, os.Chdir(repoDir)) + + ctx, err := NewContext() + require.NoError(t, err) + // Should not error, just empty volumes + require.NoError(t, ctx.Load("", true)) + assert.Equal(t, 0, len(ctx.Volumes)) +} + func TestNewWorkspace_RelativeCustomPath(t *testing.T) { repoDir, cleanup := setupTestGitRepo(t) defer cleanup() @@ -392,3 +411,99 @@ func TestHasGlobalVolumes(t *testing.T) { }) } } + +func TestCheckStatus(t *testing.T) { + sourceDir, targetDir, cleanup := setupTestEnv(t) + defer cleanup() + + t.Run("Link OK", func(t *testing.T) { + vol := Volume{Source: "source1.txt", Target: "link.txt", Mode: ModeLink} + vol.SourcePath = filepath.Join(sourceDir, "source1.txt") + vol.TargetPath = filepath.Join(targetDir, "link.txt") + require.NoError(t, os.Symlink(vol.SourcePath, vol.TargetPath)) + defer os.Remove(vol.TargetPath) + status := vol.CheckStatus() + assert.Equal(t, StatusOKLinked, status.Status) + }) + + t.Run("Link WrongLink", func(t *testing.T) { + vol := Volume{Source: "source1.txt", Target: "wrong_link.txt", Mode: ModeLink} + vol.SourcePath = filepath.Join(sourceDir, "source1.txt") + vol.TargetPath = filepath.Join(targetDir, "wrong_link.txt") + require.NoError(t, os.Symlink(filepath.Join(sourceDir, "source2.txt"), vol.TargetPath)) + defer os.Remove(vol.TargetPath) + status := vol.CheckStatus() + assert.Equal(t, StatusWrongLink, status.Status) + }) + + t.Run("Link ExistsNotLink", func(t *testing.T) { + vol := Volume{Source: "source1.txt", Target: "notlink.txt", Mode: ModeLink} + vol.SourcePath = filepath.Join(sourceDir, "source1.txt") + vol.TargetPath = filepath.Join(targetDir, "notlink.txt") + require.NoError(t, os.WriteFile(vol.TargetPath, []byte("file"), 0644)) + defer os.Remove(vol.TargetPath) + status := vol.CheckStatus() + assert.Equal(t, StatusExistsNotLink, status.Status) + }) + + t.Run("Copy OK", func(t *testing.T) { + vol := Volume{Source: "source1.txt", Target: "copy_ok.txt", Mode: ModeCopy} + vol.SourcePath = filepath.Join(sourceDir, "source1.txt") + vol.TargetPath = filepath.Join(targetDir, "copy_ok.txt") + require.NoError(t, copyFile(vol.SourcePath, vol.TargetPath)) + defer os.Remove(vol.TargetPath) + status := vol.CheckStatus() + assert.Equal(t, StatusOKCopied, status.Status) + }) + + t.Run("Copy Modified", func(t *testing.T) { + vol := Volume{Source: "source1.txt", Target: "copy_mod.txt", Mode: ModeCopy} + vol.SourcePath = filepath.Join(sourceDir, "source1.txt") + vol.TargetPath = filepath.Join(targetDir, "copy_mod.txt") + require.NoError(t, os.WriteFile(vol.TargetPath, []byte("modified"), 0644)) + defer os.Remove(vol.TargetPath) + status := vol.CheckStatus() + assert.Equal(t, StatusModified, status.Status) + }) + + t.Run("MissingSource", func(t *testing.T) { + vol := Volume{Source: "missing.txt", Target: "x.txt", Mode: ModeLink} + vol.SourcePath = filepath.Join(sourceDir, "missing.txt") + vol.TargetPath = filepath.Join(targetDir, "x.txt") + status := vol.CheckStatus() + assert.Equal(t, StatusMissingSource, status.Status) + }) + + t.Run("NotMounted", func(t *testing.T) { + vol := Volume{Source: "source1.txt", Target: "nomount.txt", Mode: ModeLink} + vol.SourcePath = filepath.Join(sourceDir, "source1.txt") + vol.TargetPath = filepath.Join(targetDir, "nomount.txt") + status := vol.CheckStatus() + assert.Equal(t, StatusNotMounted, status.Status) + }) + + t.Run("Copy Dir OK", func(t *testing.T) { + configDir := filepath.Join(sourceDir, "statusdir") + require.NoError(t, os.Mkdir(configDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(configDir, "f.txt"), []byte("data"), 0644)) + vol := Volume{Source: "statusdir", Target: "statusdir", Mode: ModeCopy} + vol.SourcePath = configDir + vol.TargetPath = filepath.Join(targetDir, "statusdir") + require.NoError(t, copyDir(vol.SourcePath, vol.TargetPath)) + status := vol.CheckStatus() + assert.Equal(t, StatusOKCopied, status.Status) + }) + + t.Run("Copy Dir Modified", func(t *testing.T) { + configDir := filepath.Join(sourceDir, "statusdir2") + require.NoError(t, os.Mkdir(configDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(configDir, "f.txt"), []byte("data"), 0644)) + vol := Volume{Source: "statusdir2", Target: "statusdir2", Mode: ModeCopy} + vol.SourcePath = configDir + vol.TargetPath = filepath.Join(targetDir, "statusdir2") + require.NoError(t, copyDir(vol.SourcePath, vol.TargetPath)) + require.NoError(t, os.WriteFile(filepath.Join(vol.TargetPath, "f.txt"), []byte("changed"), 0644)) + status := vol.CheckStatus() + assert.Equal(t, StatusModified, status.Status) + }) +} diff --git a/internal/gitvolume/debug_find_common_dir_test.go b/internal/gitvolume/debug_find_common_dir_test.go new file mode 100644 index 0000000..eee4b04 --- /dev/null +++ b/internal/gitvolume/debug_find_common_dir_test.go @@ -0,0 +1,129 @@ +package gitvolume + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDebugFindCommonDir(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "git-volume-debug-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // 1. Regular Repository + t.Run("Regular Repository", func(t *testing.T) { + repoDir := filepath.Join(tmpDir, "regular") + require.NoError(t, os.MkdirAll(repoDir, 0755)) + + cmd := exec.Command("git", "init", repoDir) + require.NoError(t, cmd.Run()) + + // Set identity + cmd = exec.Command("git", "-C", repoDir, "config", "user.email", "test@test.com") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", repoDir, "config", "user.name", "Test") + require.NoError(t, cmd.Run()) + + // Create a commit so we can create a worktree + require.NoError(t, os.WriteFile(filepath.Join(repoDir, "README.md"), []byte("init"), 0644)) + cmd = exec.Command("git", "-C", repoDir, "add", ".") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", repoDir, "commit", "-m", "Initial commit") + require.NoError(t, cmd.Run()) + + commonDir, err := findCommonDir(repoDir) + require.NoError(t, err) + + realRepoDir, _ := filepath.EvalSymlinks(repoDir) + assert.Equal(t, realRepoDir, commonDir, "Common dir of regular repo root should be itself") + + // Worktree from Regular Repo + wtDir := filepath.Join(tmpDir, "regular-worktree") + cmd = exec.Command("git", "-C", repoDir, "worktree", "add", wtDir) + require.NoError(t, cmd.Run()) + + commonDirWT, err := findCommonDir(wtDir) + require.NoError(t, err) + + commonDirWT, err = filepath.EvalSymlinks(commonDirWT) + require.NoError(t, err) + + assert.Equal(t, realRepoDir, commonDirWT, "Worktree from regular repo should point back to main repo root") + }) + + // 2. Bare Repository + t.Run("Bare Repository", func(t *testing.T) { + bareRepoDir := filepath.Join(tmpDir, "bare.git") + cmd := exec.Command("git", "init", "--bare", bareRepoDir) + require.NoError(t, cmd.Run()) + + realBareDir, err := filepath.EvalSymlinks(bareRepoDir) + require.NoError(t, err) + + commonDir, err := findCommonDir(bareRepoDir) + require.NoError(t, err) + + commonDir, err = filepath.EvalSymlinks(commonDir) + require.NoError(t, err) + + assert.Equal(t, realBareDir, commonDir, "Common dir of bare repo root should be itself") + + // Create a worktree from bare repo + // Need a commit first? Bare repos don't have commits unless pushed or created from existing. + // Let's create a regular repo first, then clone as bare to have commits. + srcRepo := filepath.Join(tmpDir, "src") + require.NoError(t, os.MkdirAll(srcRepo, 0755)) + cmd = exec.Command("git", "init", srcRepo) + require.NoError(t, cmd.Run()) + + cmd = exec.Command("git", "-C", srcRepo, "config", "user.email", "test@test.com") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", srcRepo, "config", "user.name", "Test") + require.NoError(t, cmd.Run()) + + require.NoError(t, os.WriteFile(filepath.Join(srcRepo, "README.md"), []byte("init"), 0644)) + cmd = exec.Command("git", "-C", srcRepo, "add", ".") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", srcRepo, "commit", "-m", "Initial commit") + require.NoError(t, cmd.Run()) + + // Clone as bare + bareCloned := filepath.Join(tmpDir, "bare-cloned.git") + cmd = exec.Command("git", "clone", "--bare", srcRepo, bareCloned) + require.NoError(t, cmd.Run()) + + realBareCloned, err := filepath.EvalSymlinks(bareCloned) + require.NoError(t, err) + + // Create worktree + wtDir := filepath.Join(tmpDir, "bare-worktree") + cmd = exec.Command("git", "-C", bareCloned, "worktree", "add", wtDir) + if out, err := cmd.CombinedOutput(); err != nil { + t.Logf("Git output: %s", string(out)) + require.NoError(t, err) + } + + commonDirWT, err := findCommonDir(wtDir) + require.NoError(t, err) + commonDirWT, err = filepath.EvalSymlinks(commonDirWT) + require.NoError(t, err) + + // DEBUGGING: Check what git rev-parse --git-common-dir returns here + cmd = exec.Command("git", "-C", wtDir, "rev-parse", "--git-common-dir") + out, _ := cmd.Output() + gitCommonDir := strings.TrimSpace(string(out)) + t.Logf("git rev-parse --git-common-dir in worktree: %s", gitCommonDir) + + // Check isBareRepository on that dir + isBare, err := isBareRepository(gitCommonDir) + t.Logf("isBareRepository(%s) = %v, err=%v", gitCommonDir, isBare, err) + + assert.Equal(t, realBareCloned, commonDirWT, "Worktree from bare repo should point back to bare repo root") + }) +} diff --git a/internal/gitvolume/fs_test.go b/internal/gitvolume/fs_test.go new file mode 100644 index 0000000..613d8df --- /dev/null +++ b/internal/gitvolume/fs_test.go @@ -0,0 +1,140 @@ +package gitvolume + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCopyFile(t *testing.T) { + tmpDir := t.TempDir() + src := filepath.Join(tmpDir, "source.txt") + dst := filepath.Join(tmpDir, "dest.txt") + + // 1. Normal copy + err := os.WriteFile(src, []byte("hello"), 0644) + require.NoError(t, err) + + err = copyFile(src, dst) + require.NoError(t, err) + + content, err := os.ReadFile(dst) + require.NoError(t, err) + assert.Equal(t, "hello", string(content)) + + info, err := os.Stat(dst) + require.NoError(t, err) + if runtime.GOOS != "windows" { + assert.Equal(t, os.FileMode(0644), info.Mode().Perm()) + } + + // 2. Source missing + err = copyFile(filepath.Join(tmpDir, "missing"), dst) + assert.Error(t, err) + + // 3. Dest read-only (directory) + // Create a directory where the file should be to trigger error + err = os.Mkdir(filepath.Join(tmpDir, "readonly"), 0755) + require.NoError(t, err) + err = copyFile(src, filepath.Join(tmpDir, "readonly")) + assert.Error(t, err) +} + +func TestCopyDir(t *testing.T) { + tmpDir := t.TempDir() + src := filepath.Join(tmpDir, "src") + dst := filepath.Join(tmpDir, "dst") + + // Setup source structure + require.NoError(t, os.MkdirAll(filepath.Join(src, "subdir"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(src, "file1.txt"), []byte("file1"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(src, "subdir", "file2.txt"), []byte("file2"), 0644)) + + // 1. Normal recursive copy + err := copyDir(src, dst) + require.NoError(t, err) + + assert.FileExists(t, filepath.Join(dst, "file1.txt")) + assert.FileExists(t, filepath.Join(dst, "subdir", "file2.txt")) + + // 2. Symlink in source (should fail) + symLinkSrc := filepath.Join(tmpDir, "symsrc") + require.NoError(t, os.Mkdir(symLinkSrc, 0755)) + require.NoError(t, os.Symlink(dst, filepath.Join(symLinkSrc, "link"))) + + err = copyDir(symLinkSrc, filepath.Join(tmpDir, "symdst")) + assert.Error(t, err) + assert.Contains(t, err.Error(), "symlink, which is not allowed") + + // 3. Target exists and is not a directory + isFile := filepath.Join(tmpDir, "isFile") + require.NoError(t, os.WriteFile(isFile, []byte("data"), 0644)) + err = copyDir(src, isFile) + assert.Error(t, err) + // Error message differs by OS for MkdirAll on existing file + // Linux/Mac: "not a directory" +} + +func TestHashAndVerify(t *testing.T) { + tmpDir := t.TempDir() + file1 := filepath.Join(tmpDir, "file1.txt") + file2 := filepath.Join(tmpDir, "file2.txt") + + require.NoError(t, os.WriteFile(file1, []byte("content"), 0644)) + require.NoError(t, os.WriteFile(file2, []byte("content"), 0644)) + + // 1. hashFile + h1, err := hashFile(file1) + require.NoError(t, err) + h2, err := hashFile(file2) + require.NoError(t, err) + assert.Equal(t, h1, h2) + + // 2. verifyHash matches + match, err := verifyHash(file1, file2) + require.NoError(t, err) + assert.True(t, match) + + // 3. verifyHash mismatch + require.NoError(t, os.WriteFile(file2, []byte("diff"), 0644)) + match, err = verifyHash(file1, file2) + require.NoError(t, err) + assert.False(t, match) + + // 4. Missing file + _, err = hashFile(filepath.Join(tmpDir, "missing")) + assert.Error(t, err) +} + +func TestCleanEmptyParents(t *testing.T) { + tmpDir := t.TempDir() + nested := filepath.Join(tmpDir, "a", "b", "c") + require.NoError(t, os.MkdirAll(nested, 0755)) + + // 1. Clean up empty dirs logic + // Remove 'c', then cleanEmptyParents should remove 'b' and 'a' but stop at tmpDir + err := os.Remove(nested) + require.NoError(t, err) + + cleanEmptyParents(filepath.Join(tmpDir, "a", "b"), tmpDir) + + assert.NoDirExists(t, filepath.Join(tmpDir, "a", "b")) + assert.NoDirExists(t, filepath.Join(tmpDir, "a")) + assert.DirExists(t, tmpDir) + + // 2. Stop if not empty + require.NoError(t, os.MkdirAll(nested, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "a", "file.txt"), []byte("keep"), 0644)) + + err = os.Remove(nested) + require.NoError(t, err) + + cleanEmptyParents(filepath.Join(tmpDir, "a", "b"), tmpDir) + + assert.NoDirExists(t, filepath.Join(tmpDir, "a", "b")) + assert.DirExists(t, filepath.Join(tmpDir, "a")) // Should exist because of file.txt +} diff --git a/internal/gitvolume/git_repro_test.go b/internal/gitvolume/git_repro_test.go new file mode 100644 index 0000000..649f203 --- /dev/null +++ b/internal/gitvolume/git_repro_test.go @@ -0,0 +1,114 @@ +package gitvolume + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindCommonDir_Repro(t *testing.T) { + // Create a temporary directory for our test environment + tmpDir, err := os.MkdirTemp("", "git-volume-repro-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // 1. Test Case: Bare Repository + Worktree + t.Run("Bare Repository with Worktree", func(t *testing.T) { + bareRepo := filepath.Join(tmpDir, "bare.git") + worktree := filepath.Join(tmpDir, "worktree") + + // Create a non-bare origin repo with an initial commit, + // then clone it as bare. + + origin := filepath.Join(tmpDir, "origin") + require.NoError(t, os.MkdirAll(origin, 0755)) + cmd := exec.Command("git", "init", origin) + require.NoError(t, cmd.Run()) + + // config user + cmd = exec.Command("git", "-C", origin, "config", "user.email", "test@example.com") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", origin, "config", "user.name", "Test User") + require.NoError(t, cmd.Run()) + + // commit + require.NoError(t, os.WriteFile(filepath.Join(origin, "README.md"), []byte("test"), 0644)) + cmd = exec.Command("git", "-C", origin, "add", ".") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", origin, "commit", "-m", "initial") + require.NoError(t, cmd.Run()) + + // Clone as bare (this creates the bareRepo directory) + cmd = exec.Command("git", "clone", "--bare", origin, bareRepo) + require.NoError(t, cmd.Run()) + + cmd = exec.Command("git", "-C", bareRepo, "worktree", "add", "-b", "bare-worktree", worktree) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git worktree add failed: %s, output: %s", err, out) + } + + // Resolve symlinks for accurate comparison + bareRepo, err = filepath.EvalSymlinks(bareRepo) + require.NoError(t, err) + + // Run findCommonDir from worktree root + commonDir, err := findCommonDir(worktree) + require.NoError(t, err) + + // For bare repo, common dir should be the bare repo path itself + assert.Equal(t, bareRepo, commonDir, "Should identify bare repo root as common dir") + + // Run findCommonDir from subdirectory of worktree + subDir := filepath.Join(worktree, "subdir") + require.NoError(t, os.MkdirAll(subDir, 0755)) + + commonDirSub, err := findCommonDir(subDir) + require.NoError(t, err) + assert.Equal(t, bareRepo, commonDirSub, "Should identify bare repo root from subdirectory") + }) + + // 2. Test Case: Regular Repository + Worktree + t.Run("Regular Repository with Worktree", func(t *testing.T) { + mainRepo := filepath.Join(tmpDir, "main") + worktree := filepath.Join(tmpDir, "main-worktree") + + // Initialize main repo + require.NoError(t, os.MkdirAll(mainRepo, 0755)) + cmd := exec.Command("git", "init", mainRepo) + require.NoError(t, cmd.Run()) + + // config user + cmd = exec.Command("git", "-C", mainRepo, "config", "user.email", "test@example.com") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", mainRepo, "config", "user.name", "Test User") + require.NoError(t, cmd.Run()) + + // commit + require.NoError(t, os.WriteFile(filepath.Join(mainRepo, "README.md"), []byte("test"), 0644)) + cmd = exec.Command("git", "-C", mainRepo, "add", ".") + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", mainRepo, "commit", "-m", "initial") + require.NoError(t, cmd.Run()) + + cmd = exec.Command("git", "-C", mainRepo, "worktree", "add", "-b", "main-worktree-branch", worktree) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git worktree add failed: %s, output: %s", err, out) + } + + // Resolve symlinks + mainRepo, err = filepath.EvalSymlinks(mainRepo) + require.NoError(t, err) + + // Run findCommonDir from worktree root + commonDir, err := findCommonDir(worktree) + require.NoError(t, err) + + // For regular repo, common dir is .git dir, so root is parent of .git + // BUT findCommonDir returns the ROOT of the main repo, not the .git dir. + assert.Equal(t, mainRepo, commonDir, "Should identify main repo root") + }) +} diff --git a/internal/gitvolume/git_test.go b/internal/gitvolume/git_test.go new file mode 100644 index 0000000..72c3476 --- /dev/null +++ b/internal/gitvolume/git_test.go @@ -0,0 +1,85 @@ +package gitvolume + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindWorktreeRoot(t *testing.T) { + // 1. Regular git repo + tmpDir := t.TempDir() + cmd := exec.Command("git", "init", tmpDir) + require.NoError(t, cmd.Run()) + + // Create a subdirectory + subDir := filepath.Join(tmpDir, "subdir") + require.NoError(t, os.Mkdir(subDir, 0755)) + + // Find root from root + root, err := FindWorktreeRoot(tmpDir) + require.NoError(t, err) + + // Evaluate symlinks in case /var vs /private/var on Mac + evalTmpDir, _ := filepath.EvalSymlinks(tmpDir) + evalRoot, _ := filepath.EvalSymlinks(root) + assert.Equal(t, evalTmpDir, evalRoot) + + // Find root from subdir + root, err = FindWorktreeRoot(subDir) + require.NoError(t, err) + evalRoot, _ = filepath.EvalSymlinks(root) + assert.Equal(t, evalTmpDir, evalRoot) + + // 2. Not a git repo + nonGitDir := t.TempDir() + _, err = FindWorktreeRoot(nonGitDir) + assert.Error(t, err) +} + +func TestFindCommonDir(t *testing.T) { + tmpDir := t.TempDir() + + // 1. Regular repo + repoDir := filepath.Join(tmpDir, "repo") + require.NoError(t, os.Mkdir(repoDir, 0755)) + cmd := exec.Command("git", "init", repoDir) + require.NoError(t, cmd.Run()) + + commonDir, err := findCommonDir(repoDir) + require.NoError(t, err) + + evalRepoDir, _ := filepath.EvalSymlinks(repoDir) + evalCommonDir, _ := filepath.EvalSymlinks(commonDir) + assert.Equal(t, evalRepoDir, evalCommonDir) + + // 2. Worktree (standard layout) is harder to set up without bare repo or commits + // but findCommonDir should return main repo root for the main repo itself. +} + +func TestIsBareRepository(t *testing.T) { + tmpDir := t.TempDir() + + // 1. Regular repo + repoDir := filepath.Join(tmpDir, "repo") + require.NoError(t, os.Mkdir(repoDir, 0755)) + cmd := exec.Command("git", "init", repoDir) + require.NoError(t, cmd.Run()) + + isBare, err := isBareRepository(repoDir) + require.NoError(t, err) + assert.False(t, isBare) + + // 2. Bare repo + bareDir := filepath.Join(tmpDir, "bare.git") + cmd = exec.Command("git", "init", "--bare", bareDir) + require.NoError(t, cmd.Run()) + + isBare, err = isBareRepository(bareDir) + require.NoError(t, err) + assert.True(t, isBare) +} diff --git a/internal/gitvolume/gitvolume_test.go b/internal/gitvolume/gitvolume_test.go index 91aa9c3..c612cb2 100644 --- a/internal/gitvolume/gitvolume_test.go +++ b/internal/gitvolume/gitvolume_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -67,3 +68,32 @@ func createTestGitVolume(sourceDir, targetDir, globalDir string, volumes []Volum quiet: true, } } + +func TestGitVolumeAccessors(t *testing.T) { + sourceDir, targetDir, globalDir, cleanup := setupTestEnvWithGlobal(t) + defer cleanup() + + volumes := []Volume{ + {Source: "source1.txt", Target: "link1.txt", Mode: ModeLink}, + {Source: "global.txt", Target: "global.txt", Mode: ModeLink, IsGlobal: true}, + } + gv := createTestGitVolume(sourceDir, targetDir, globalDir, volumes) + + assert.Equal(t, sourceDir, gv.SourceDir()) + assert.Equal(t, targetDir, gv.TargetDir()) + assert.Equal(t, globalDir, gv.GlobalDir()) + assert.NotNil(t, gv.Context()) + assert.True(t, gv.HasGlobalVolumes()) +} + +func TestValidatePath_GitDirectory(t *testing.T) { + err := validatePath(".git") + assert.Error(t, err) + assert.Contains(t, err.Error(), ".git") + + err = validatePath(".git/config") + assert.Error(t, err) + + err = validatePath(".") + assert.Error(t, err) +} diff --git a/internal/gitvolume/init_test.go b/internal/gitvolume/init_test.go new file mode 100644 index 0000000..ca99eed --- /dev/null +++ b/internal/gitvolume/init_test.go @@ -0,0 +1,115 @@ +package gitvolume + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + // 1. Setup git repo + tmpDir := t.TempDir() + require.NoError(t, os.Mkdir(filepath.Join(tmpDir, "repo"), 0755)) + repoDir := filepath.Join(tmpDir, "repo") + cmd := exec.Command("git", "init", repoDir) + require.NoError(t, cmd.Run()) + + // Change CWD to repo + wd, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(wd) // Restore CWD + require.NoError(t, os.Chdir(repoDir)) + + // Setup GitVolume (GlobalDir will be in tmp) + globalDir := filepath.Join(tmpDir, "global") + gv := createTestGitVolume(repoDir, repoDir, globalDir, nil) + gv.quiet = true + + // Test 1: Init success + err = gv.Init() + require.NoError(t, err) + + assert.DirExists(t, globalDir) + assert.FileExists(t, filepath.Join(repoDir, "git-volume.yaml")) + + // Verify content + content, err := os.ReadFile(filepath.Join(repoDir, "git-volume.yaml")) + require.NoError(t, err) + assert.Contains(t, string(content), "volumes:") + + // Test 2: Idempotency (should not overwrite) + err = os.WriteFile(filepath.Join(repoDir, "git-volume.yaml"), []byte("modified: true"), 0644) + require.NoError(t, err) + + err = gv.Init() + require.NoError(t, err) + + content, err = os.ReadFile(filepath.Join(repoDir, "git-volume.yaml")) + require.NoError(t, err) + assert.Equal(t, "modified: true", string(content)) +} + +func TestInit_OutsideGit(t *testing.T) { + // 1. Setup non-git dir + tmpDir := t.TempDir() + + // Change CWD + wd, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(wd) + require.NoError(t, os.Chdir(tmpDir)) + + gv := createTestGitVolume(tmpDir, tmpDir, filepath.Join(tmpDir, "global"), nil) + gv.quiet = true + + // Test: Init fails + err = gv.Init() + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to find git repository root") +} + +func TestInit_NonQuiet(t *testing.T) { + tmpDir := t.TempDir() + repoDir := filepath.Join(tmpDir, "repo") + require.NoError(t, os.Mkdir(repoDir, 0755)) + cmd := exec.Command("git", "init", repoDir) + require.NoError(t, cmd.Run()) + + wd, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(wd) + require.NoError(t, os.Chdir(repoDir)) + + globalDir := filepath.Join(tmpDir, "global") + gv := createTestGitVolume(repoDir, repoDir, globalDir, nil) + gv.quiet = false // Non-quiet to cover afterInit output branches + + // First init (configCreated branch) + err = gv.Init() + require.NoError(t, err) + assert.DirExists(t, globalDir) + assert.FileExists(t, filepath.Join(repoDir, "git-volume.yaml")) + + // Second init (configExists branch) + err = gv.Init() + require.NoError(t, err) +} + +func TestInit_NonQuiet_Error(t *testing.T) { + tmpDir := t.TempDir() + + wd, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(wd) + require.NoError(t, os.Chdir(tmpDir)) + + gv := createTestGitVolume(tmpDir, tmpDir, filepath.Join(tmpDir, "global"), nil) + gv.quiet = false // Non-quiet to cover error branch + + err = gv.Init() + assert.Error(t, err) +} diff --git a/internal/gitvolume/list_test.go b/internal/gitvolume/list_test.go index 0c86cdd..8096f3b 100644 --- a/internal/gitvolume/list_test.go +++ b/internal/gitvolume/list_test.go @@ -87,3 +87,65 @@ func TestSortTree(t *testing.T) { sortTree(root) verifyOrder() } + +func TestGlobalList(t *testing.T) { + globalDir, cleanup := setupGlobalTestEnv(t) + defer cleanup() + + require.NoError(t, os.MkdirAll(globalDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(globalDir, "file1.txt"), []byte("a"), 0644)) + + gv := createTestGitVolumeWithGlobal(globalDir) + err := gv.GlobalList() + require.NoError(t, err) +} + +func TestGlobalList_Empty(t *testing.T) { + globalDir, cleanup := setupGlobalTestEnv(t) + defer cleanup() + + require.NoError(t, os.MkdirAll(globalDir, 0755)) + + gv := createTestGitVolumeWithGlobal(globalDir) + err := gv.GlobalList() + require.NoError(t, err) +} + +func TestGlobalList_NotInitialized(t *testing.T) { + tmpDir := t.TempDir() + globalDir := filepath.Join(tmpDir, "nonexistent") + + gv := createTestGitVolumeWithGlobal(globalDir) + err := gv.GlobalList() + require.NoError(t, err) +} + +func TestGlobalList_NestedDirs(t *testing.T) { + globalDir, cleanup := setupGlobalTestEnv(t) + defer cleanup() + + require.NoError(t, os.MkdirAll(filepath.Join(globalDir, "dir1", "subdir"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(globalDir, "file1.txt"), []byte("a"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(globalDir, "dir1", "file2.txt"), []byte("b"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(globalDir, "dir1", "subdir", "file3.txt"), []byte("c"), 0644)) + + gv := createTestGitVolumeWithGlobal(globalDir) + err := gv.GlobalList() + require.NoError(t, err) +} + +func TestPrintNode(t *testing.T) { + // Test printNode directly for both isLast=true and isLast=false + node := &treeNode{ + name: "parent", + isDir: true, + children: []*treeNode{ + {name: "child1.txt", isDir: false}, + {name: "child2.txt", isDir: false}, + }, + } + // Should not panic + printNode(node, "", true) + printNode(node, "", false) + printNode(node, "│ ", true) +} diff --git a/internal/gitvolume/remove_test.go b/internal/gitvolume/remove_test.go new file mode 100644 index 0000000..b8776a1 --- /dev/null +++ b/internal/gitvolume/remove_test.go @@ -0,0 +1,73 @@ +package gitvolume + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGlobalRemove(t *testing.T) { + _, _, globalDir, cleanup := setupTestEnvWithGlobal(t) + defer cleanup() + + // 1. Setup - Create additional files for testing + require.NoError(t, os.WriteFile(filepath.Join(globalDir, "file1.txt"), []byte("content"), 0644)) + require.NoError(t, os.MkdirAll(filepath.Join(globalDir, "dir1", "subdir"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(globalDir, "dir1", "file2.txt"), []byte("content"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(globalDir, "dir1", "subdir", "file3.txt"), []byte("content"), 0644)) + + gv := createTestGitVolume("", "", globalDir, nil) + + // Test 1: success - remove file + err := gv.GlobalRemove([]string{"file1.txt"}) + require.NoError(t, err) + assert.NoFileExists(t, filepath.Join(globalDir, "file1.txt")) + + // Test 2: success - remove directory + // Removing 'subdir' should remove file3.txt + err = gv.GlobalRemove([]string{"dir1/subdir"}) + require.NoError(t, err) + assert.NoDirExists(t, filepath.Join(globalDir, "dir1", "subdir")) + assert.FileExists(t, filepath.Join(globalDir, "dir1", "file2.txt")) // Sibling preserved + + // Test 3: cleanup - remove last file in dir1, dir1 should be removed + err = gv.GlobalRemove([]string{"dir1/file2.txt"}) + require.NoError(t, err) + assert.NoFileExists(t, filepath.Join(globalDir, "dir1", "file2.txt")) + assert.NoDirExists(t, filepath.Join(globalDir, "dir1")) // Cleanup happened + + // Test 4: missing file + err = gv.GlobalRemove([]string{"missing.txt"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") + + // Test 5: path traversal + err = gv.GlobalRemove([]string{"../outside.txt"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid path") + + // Test 6: multiple files with error aggregation + // Create one valid file + require.NoError(t, os.WriteFile(filepath.Join(globalDir, "valid.txt"), []byte("content"), 0644)) + + err = gv.GlobalRemove([]string{"valid.txt", "missing.txt"}) + assert.Error(t, err) // Should return error because one failed + assert.NoFileExists(t, filepath.Join(globalDir, "valid.txt")) // Valid one should still be removed +} + +func TestGlobalRemove_NotInitialized(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "git-volume-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Point to a non-existent directory + globalDir := filepath.Join(tmpDir, "non_existent_global") + gv := createTestGitVolume("", "", globalDir, nil) + + err = gv.GlobalRemove([]string{"file.txt"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "global storage not initialized") +} diff --git a/internal/gitvolume/status_test.go b/internal/gitvolume/status_test.go index 7ebde6e..0374d49 100644 --- a/internal/gitvolume/status_test.go +++ b/internal/gitvolume/status_test.go @@ -1,6 +1,7 @@ package gitvolume import ( + "fmt" "os" "path/filepath" "testing" @@ -71,3 +72,24 @@ func TestGitVolume_status_CopyDirectoryModified(t *testing.T) { require.NoError(t, err) assert.Equal(t, StatusModified, statuses[0].Status) } + +func TestAfterAllStatus(t *testing.T) { + sourceDir, targetDir, cleanup := setupTestEnv(t) + defer cleanup() + + volumes := []Volume{ + {Source: "source1.txt", Target: "local.txt", Mode: ModeLink}, + } + gv := createTestGitVolume(sourceDir, targetDir, "", volumes) + + // Test afterAllStatus with nil error + statuses := []VolumeStatus{ + {Source: "source1.txt", Target: "local.txt", Mode: ModeLink, Status: StatusNotMounted}, + } + err := gv.afterAllStatus(statuses, nil) + assert.NoError(t, err) + + // Test afterAllStatus with error + err = gv.afterAllStatus(nil, fmt.Errorf("test error")) + assert.Error(t, err) +} diff --git a/internal/gitvolume/sync_test.go b/internal/gitvolume/sync_test.go index da2bdc6..b34a81b 100644 --- a/internal/gitvolume/sync_test.go +++ b/internal/gitvolume/sync_test.go @@ -334,3 +334,101 @@ func TestGitVolume_Sync_GlobalSource_EmptyGlobalBase(t *testing.T) { assert.Error(t, err, "expected error when GlobalDir is empty") assert.Contains(t, err.Error(), "global directory not configured") } + +func TestGitVolume_Sync_SourceIsSymlink(t *testing.T) { + sourceDir, targetDir, cleanup := setupTestEnv(t) + defer cleanup() + + // Create symlink source + // We need absolute path for symlink source to be valid in most OSs or relative + targetFile := filepath.Join(sourceDir, "target.txt") + require.NoError(t, os.WriteFile(targetFile, []byte("target"), 0644)) + + symlinkSource := filepath.Join(sourceDir, "symlink-source") + require.NoError(t, os.Symlink(targetFile, symlinkSource)) + + volumes := []Volume{ + {Source: "symlink-source", Target: "dest.txt", Mode: ModeCopy}, + } + gv := createTestGitVolume(sourceDir, targetDir, "", volumes) + + err := gv.Sync(SyncOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "source file is a symlink") +} + +func TestGitVolume_Sync_PathTraversal(t *testing.T) { + sourceDir, targetDir, cleanup := setupTestEnv(t) + defer cleanup() + + // 1. Source traversal + volumes := []Volume{ + {Source: "../outside.txt", Target: "dest.txt", Mode: ModeCopy}, + } + gv := createTestGitVolume(sourceDir, targetDir, "", volumes) + err := gv.Sync(SyncOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "security error for source") + + // 2. Target traversal + volumes = []Volume{ + {Source: "source1.txt", Target: "../outside.txt", Mode: ModeCopy}, + } + gv = createTestGitVolume(sourceDir, targetDir, "", volumes) + err = gv.Sync(SyncOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "security error for target") +} + +func TestGitVolume_Sync_Verbose(t *testing.T) { + sourceDir, targetDir, globalDir, cleanup := setupTestEnvWithGlobal(t) + defer cleanup() + + volumes := []Volume{ + {Source: "source1.txt", Target: "link.txt", Mode: ModeLink}, + {Source: "source2.txt", Target: "copy.txt", Mode: ModeCopy}, + {Source: "global.txt", Target: "global.txt", Mode: ModeLink, IsGlobal: true}, + } + ctx := &Context{ + SourceDir: sourceDir, + TargetDir: targetDir, + GlobalDir: globalDir, + Volumes: volumes, + } + ctx.ResolveVolumePaths() + + gv := &GitVolume{ctx: ctx, verbose: true, quiet: false} + + // Sync with verbose output + require.NoError(t, gv.Sync(SyncOptions{})) + + // Verify all files created + _, err := os.Lstat(filepath.Join(targetDir, "link.txt")) + assert.NoError(t, err) + _, err = os.Stat(filepath.Join(targetDir, "copy.txt")) + assert.NoError(t, err) + + // Sync with relative links and verbose + require.NoError(t, gv.Sync(SyncOptions{RelativeLinks: true})) +} + +func TestGitVolume_Sync_NonQuiet_Error(t *testing.T) { + sourceDir, targetDir, cleanup := setupTestEnv(t) + defer cleanup() + + volumes := []Volume{ + {Source: "nonexistent.txt", Target: "dest.txt", Mode: ModeLink}, + } + ctx := &Context{ + SourceDir: sourceDir, + TargetDir: targetDir, + Volumes: volumes, + } + ctx.ResolveVolumePaths() + + gv := &GitVolume{ctx: ctx, verbose: false, quiet: false} + + // Should report error non-quietly + err := gv.Sync(SyncOptions{}) + assert.Error(t, err) +} diff --git a/internal/gitvolume/unsync_test.go b/internal/gitvolume/unsync_test.go index 9062ec2..9674e79 100644 --- a/internal/gitvolume/unsync_test.go +++ b/internal/gitvolume/unsync_test.go @@ -351,3 +351,32 @@ func TestGitVolume_Unsync_CopyDirectory_MissingSource(t *testing.T) { _, err := os.Stat(filepath.Join(targetDir, "config")) assert.NoError(t, err, "Config directory should NOT be removed if source is missing") } + +func TestGitVolume_Unsync_TypeMismatch(t *testing.T) { + sourceDir, targetDir, cleanup := setupTestEnv(t) + defer cleanup() + + // 1. Setup Source as Directory + configDir := filepath.Join(sourceDir, "config") + require.NoError(t, os.MkdirAll(configDir, 0755)) + + volumes := []Volume{ + {Source: "config", Target: "config", Mode: ModeCopy}, + } + gv := createTestGitVolume(sourceDir, targetDir, "", volumes) + + // Manually create Target as File (simulating a change or weird state) + require.NoError(t, os.WriteFile(filepath.Join(targetDir, "config"), []byte("I am a file"), 0644)) + + // Unsync + require.NoError(t, gv.Unsync(UnsyncOptions{})) + + // Verify Target File Preserved (Safety check should fail due to type mismatch) + // checkRemovable sees source is Dir, target is File -> mismatch -> returns false + info, err := os.Stat(filepath.Join(targetDir, "config")) + require.NoError(t, err) + assert.True(t, !info.IsDir()) + + data, _ := os.ReadFile(filepath.Join(targetDir, "config")) + assert.Equal(t, "I am a file", string(data)) +} From ff2f1dfb4c851f8b6cc3405dacecff03bf2ef816 Mon Sep 17 00:00:00 2001 From: laggu91 Date: Thu, 19 Feb 2026 20:57:48 +0900 Subject: [PATCH 2/4] fix: suppress errcheck lint for deferred os.Chdir in init_test.go --- internal/gitvolume/init_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/gitvolume/init_test.go b/internal/gitvolume/init_test.go index ca99eed..c669eb1 100644 --- a/internal/gitvolume/init_test.go +++ b/internal/gitvolume/init_test.go @@ -21,7 +21,7 @@ func TestInit(t *testing.T) { // Change CWD to repo wd, err := os.Getwd() require.NoError(t, err) - defer os.Chdir(wd) // Restore CWD + defer func() { _ = os.Chdir(wd) }() // Restore CWD require.NoError(t, os.Chdir(repoDir)) // Setup GitVolume (GlobalDir will be in tmp) @@ -60,7 +60,7 @@ func TestInit_OutsideGit(t *testing.T) { // Change CWD wd, err := os.Getwd() require.NoError(t, err) - defer os.Chdir(wd) + defer func() { _ = os.Chdir(wd) }() require.NoError(t, os.Chdir(tmpDir)) gv := createTestGitVolume(tmpDir, tmpDir, filepath.Join(tmpDir, "global"), nil) @@ -81,7 +81,7 @@ func TestInit_NonQuiet(t *testing.T) { wd, err := os.Getwd() require.NoError(t, err) - defer os.Chdir(wd) + defer func() { _ = os.Chdir(wd) }() require.NoError(t, os.Chdir(repoDir)) globalDir := filepath.Join(tmpDir, "global") @@ -104,7 +104,7 @@ func TestInit_NonQuiet_Error(t *testing.T) { wd, err := os.Getwd() require.NoError(t, err) - defer os.Chdir(wd) + defer func() { _ = os.Chdir(wd) }() require.NoError(t, os.Chdir(tmpDir)) gv := createTestGitVolume(tmpDir, tmpDir, filepath.Join(tmpDir, "global"), nil) From c1cee327e5238f323a8b98b895b5a343a55be509 Mon Sep 17 00:00:00 2001 From: laggu91 Date: Fri, 20 Feb 2026 12:36:28 +0900 Subject: [PATCH 3/4] fix: address PR review - check all error return values (errcheck) - context_test.go: check os.Getwd() error in TestNewWorkspace_EmptyConfig - debug_find_common_dir_test.go: check cmd.Output() error with t.Logf - git_test.go: check filepath.EvalSymlinks() errors with require.NoError --- internal/gitvolume/context_test.go | 3 ++- internal/gitvolume/debug_find_common_dir_test.go | 5 ++++- internal/gitvolume/git_test.go | 15 ++++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/internal/gitvolume/context_test.go b/internal/gitvolume/context_test.go index 9f22316..654c6d2 100644 --- a/internal/gitvolume/context_test.go +++ b/internal/gitvolume/context_test.go @@ -329,7 +329,8 @@ func TestNewWorkspace_EmptyConfig(t *testing.T) { require.NoError(t, os.WriteFile(configPath, []byte(""), 0644)) // Change to repo dir - oldDir, _ := os.Getwd() + oldDir, err := os.Getwd() + require.NoError(t, err) defer func() { _ = os.Chdir(oldDir) }() require.NoError(t, os.Chdir(repoDir)) diff --git a/internal/gitvolume/debug_find_common_dir_test.go b/internal/gitvolume/debug_find_common_dir_test.go index eee4b04..e2ac53f 100644 --- a/internal/gitvolume/debug_find_common_dir_test.go +++ b/internal/gitvolume/debug_find_common_dir_test.go @@ -116,7 +116,10 @@ func TestDebugFindCommonDir(t *testing.T) { // DEBUGGING: Check what git rev-parse --git-common-dir returns here cmd = exec.Command("git", "-C", wtDir, "rev-parse", "--git-common-dir") - out, _ := cmd.Output() + out, err := cmd.Output() + if err != nil { + t.Logf("git rev-parse --git-common-dir failed: %v", err) + } gitCommonDir := strings.TrimSpace(string(out)) t.Logf("git rev-parse --git-common-dir in worktree: %s", gitCommonDir) diff --git a/internal/gitvolume/git_test.go b/internal/gitvolume/git_test.go index 72c3476..5d2e507 100644 --- a/internal/gitvolume/git_test.go +++ b/internal/gitvolume/git_test.go @@ -25,14 +25,17 @@ func TestFindWorktreeRoot(t *testing.T) { require.NoError(t, err) // Evaluate symlinks in case /var vs /private/var on Mac - evalTmpDir, _ := filepath.EvalSymlinks(tmpDir) - evalRoot, _ := filepath.EvalSymlinks(root) + evalTmpDir, err := filepath.EvalSymlinks(tmpDir) + require.NoError(t, err) + evalRoot, err := filepath.EvalSymlinks(root) + require.NoError(t, err) assert.Equal(t, evalTmpDir, evalRoot) // Find root from subdir root, err = FindWorktreeRoot(subDir) require.NoError(t, err) - evalRoot, _ = filepath.EvalSymlinks(root) + evalRoot, err = filepath.EvalSymlinks(root) + require.NoError(t, err) assert.Equal(t, evalTmpDir, evalRoot) // 2. Not a git repo @@ -53,8 +56,10 @@ func TestFindCommonDir(t *testing.T) { commonDir, err := findCommonDir(repoDir) require.NoError(t, err) - evalRepoDir, _ := filepath.EvalSymlinks(repoDir) - evalCommonDir, _ := filepath.EvalSymlinks(commonDir) + evalRepoDir, err := filepath.EvalSymlinks(repoDir) + require.NoError(t, err) + evalCommonDir, err := filepath.EvalSymlinks(commonDir) + require.NoError(t, err) assert.Equal(t, evalRepoDir, evalCommonDir) // 2. Worktree (standard layout) is harder to set up without bare repo or commits From 070e4dd1ffae19bf336ed19e3a0ba2f7c1d0f272 Mon Sep 17 00:00:00 2001 From: laggu91 Date: Fri, 20 Feb 2026 13:19:18 +0900 Subject: [PATCH 4/4] refactor: address PR review - table-driven TestCheckStatus, remove debug test - context_test.go: refactor TestCheckStatus to table-driven test pattern - debug_find_common_dir_test.go: remove duplicate debug test file (covered by git_repro_test.go) --- internal/gitvolume/context_test.go | 190 +++++++++--------- .../gitvolume/debug_find_common_dir_test.go | 132 ------------ 2 files changed, 100 insertions(+), 222 deletions(-) delete mode 100644 internal/gitvolume/debug_find_common_dir_test.go diff --git a/internal/gitvolume/context_test.go b/internal/gitvolume/context_test.go index 654c6d2..eab4dd6 100644 --- a/internal/gitvolume/context_test.go +++ b/internal/gitvolume/context_test.go @@ -417,94 +417,104 @@ func TestCheckStatus(t *testing.T) { sourceDir, targetDir, cleanup := setupTestEnv(t) defer cleanup() - t.Run("Link OK", func(t *testing.T) { - vol := Volume{Source: "source1.txt", Target: "link.txt", Mode: ModeLink} - vol.SourcePath = filepath.Join(sourceDir, "source1.txt") - vol.TargetPath = filepath.Join(targetDir, "link.txt") - require.NoError(t, os.Symlink(vol.SourcePath, vol.TargetPath)) - defer os.Remove(vol.TargetPath) - status := vol.CheckStatus() - assert.Equal(t, StatusOKLinked, status.Status) - }) - - t.Run("Link WrongLink", func(t *testing.T) { - vol := Volume{Source: "source1.txt", Target: "wrong_link.txt", Mode: ModeLink} - vol.SourcePath = filepath.Join(sourceDir, "source1.txt") - vol.TargetPath = filepath.Join(targetDir, "wrong_link.txt") - require.NoError(t, os.Symlink(filepath.Join(sourceDir, "source2.txt"), vol.TargetPath)) - defer os.Remove(vol.TargetPath) - status := vol.CheckStatus() - assert.Equal(t, StatusWrongLink, status.Status) - }) - - t.Run("Link ExistsNotLink", func(t *testing.T) { - vol := Volume{Source: "source1.txt", Target: "notlink.txt", Mode: ModeLink} - vol.SourcePath = filepath.Join(sourceDir, "source1.txt") - vol.TargetPath = filepath.Join(targetDir, "notlink.txt") - require.NoError(t, os.WriteFile(vol.TargetPath, []byte("file"), 0644)) - defer os.Remove(vol.TargetPath) - status := vol.CheckStatus() - assert.Equal(t, StatusExistsNotLink, status.Status) - }) - - t.Run("Copy OK", func(t *testing.T) { - vol := Volume{Source: "source1.txt", Target: "copy_ok.txt", Mode: ModeCopy} - vol.SourcePath = filepath.Join(sourceDir, "source1.txt") - vol.TargetPath = filepath.Join(targetDir, "copy_ok.txt") - require.NoError(t, copyFile(vol.SourcePath, vol.TargetPath)) - defer os.Remove(vol.TargetPath) - status := vol.CheckStatus() - assert.Equal(t, StatusOKCopied, status.Status) - }) - - t.Run("Copy Modified", func(t *testing.T) { - vol := Volume{Source: "source1.txt", Target: "copy_mod.txt", Mode: ModeCopy} - vol.SourcePath = filepath.Join(sourceDir, "source1.txt") - vol.TargetPath = filepath.Join(targetDir, "copy_mod.txt") - require.NoError(t, os.WriteFile(vol.TargetPath, []byte("modified"), 0644)) - defer os.Remove(vol.TargetPath) - status := vol.CheckStatus() - assert.Equal(t, StatusModified, status.Status) - }) - - t.Run("MissingSource", func(t *testing.T) { - vol := Volume{Source: "missing.txt", Target: "x.txt", Mode: ModeLink} - vol.SourcePath = filepath.Join(sourceDir, "missing.txt") - vol.TargetPath = filepath.Join(targetDir, "x.txt") - status := vol.CheckStatus() - assert.Equal(t, StatusMissingSource, status.Status) - }) - - t.Run("NotMounted", func(t *testing.T) { - vol := Volume{Source: "source1.txt", Target: "nomount.txt", Mode: ModeLink} - vol.SourcePath = filepath.Join(sourceDir, "source1.txt") - vol.TargetPath = filepath.Join(targetDir, "nomount.txt") - status := vol.CheckStatus() - assert.Equal(t, StatusNotMounted, status.Status) - }) - - t.Run("Copy Dir OK", func(t *testing.T) { - configDir := filepath.Join(sourceDir, "statusdir") - require.NoError(t, os.Mkdir(configDir, 0755)) - require.NoError(t, os.WriteFile(filepath.Join(configDir, "f.txt"), []byte("data"), 0644)) - vol := Volume{Source: "statusdir", Target: "statusdir", Mode: ModeCopy} - vol.SourcePath = configDir - vol.TargetPath = filepath.Join(targetDir, "statusdir") - require.NoError(t, copyDir(vol.SourcePath, vol.TargetPath)) - status := vol.CheckStatus() - assert.Equal(t, StatusOKCopied, status.Status) - }) - - t.Run("Copy Dir Modified", func(t *testing.T) { - configDir := filepath.Join(sourceDir, "statusdir2") - require.NoError(t, os.Mkdir(configDir, 0755)) - require.NoError(t, os.WriteFile(filepath.Join(configDir, "f.txt"), []byte("data"), 0644)) - vol := Volume{Source: "statusdir2", Target: "statusdir2", Mode: ModeCopy} - vol.SourcePath = configDir - vol.TargetPath = filepath.Join(targetDir, "statusdir2") - require.NoError(t, copyDir(vol.SourcePath, vol.TargetPath)) - require.NoError(t, os.WriteFile(filepath.Join(vol.TargetPath, "f.txt"), []byte("changed"), 0644)) - status := vol.CheckStatus() - assert.Equal(t, StatusModified, status.Status) - }) + createVol := func(source, target, mode string) Volume { + return Volume{ + Source: source, + Target: target, + Mode: mode, + SourcePath: filepath.Join(sourceDir, source), + TargetPath: filepath.Join(targetDir, target), + } + } + + testCases := []struct { + name string + vol Volume + setup func(t *testing.T, vol Volume) + wantStatus string + }{ + { + name: "Link OK", + vol: createVol("source1.txt", "link.txt", ModeLink), + setup: func(t *testing.T, vol Volume) { + require.NoError(t, os.Symlink(vol.SourcePath, vol.TargetPath)) + }, + wantStatus: StatusOKLinked, + }, + { + name: "Link WrongLink", + vol: createVol("source1.txt", "wrong_link.txt", ModeLink), + setup: func(t *testing.T, vol Volume) { + require.NoError(t, os.Symlink(filepath.Join(sourceDir, "source2.txt"), vol.TargetPath)) + }, + wantStatus: StatusWrongLink, + }, + { + name: "Link ExistsNotLink", + vol: createVol("source1.txt", "notlink.txt", ModeLink), + setup: func(t *testing.T, vol Volume) { + require.NoError(t, os.WriteFile(vol.TargetPath, []byte("file"), 0644)) + }, + wantStatus: StatusExistsNotLink, + }, + { + name: "Copy OK", + vol: createVol("source1.txt", "copy_ok.txt", ModeCopy), + setup: func(t *testing.T, vol Volume) { + require.NoError(t, copyFile(vol.SourcePath, vol.TargetPath)) + }, + wantStatus: StatusOKCopied, + }, + { + name: "Copy Modified", + vol: createVol("source1.txt", "copy_mod.txt", ModeCopy), + setup: func(t *testing.T, vol Volume) { + require.NoError(t, os.WriteFile(vol.TargetPath, []byte("modified"), 0644)) + }, + wantStatus: StatusModified, + }, + { + name: "MissingSource", + vol: createVol("missing.txt", "x.txt", ModeLink), + setup: func(t *testing.T, vol Volume) {}, + wantStatus: StatusMissingSource, + }, + { + name: "NotMounted", + vol: createVol("source1.txt", "nomount.txt", ModeLink), + setup: func(t *testing.T, vol Volume) {}, + wantStatus: StatusNotMounted, + }, + { + name: "Copy Dir OK", + vol: createVol("statusdir", "statusdir_ok", ModeCopy), + setup: func(t *testing.T, vol Volume) { + require.NoError(t, os.Mkdir(vol.SourcePath, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(vol.SourcePath, "f.txt"), []byte("data"), 0644)) + require.NoError(t, copyDir(vol.SourcePath, vol.TargetPath)) + }, + wantStatus: StatusOKCopied, + }, + { + name: "Copy Dir Modified", + vol: createVol("statusdir2", "statusdir_mod", ModeCopy), + setup: func(t *testing.T, vol Volume) { + require.NoError(t, os.Mkdir(vol.SourcePath, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(vol.SourcePath, "f.txt"), []byte("data"), 0644)) + require.NoError(t, copyDir(vol.SourcePath, vol.TargetPath)) + require.NoError(t, os.WriteFile(filepath.Join(vol.TargetPath, "f.txt"), []byte("changed"), 0644)) + }, + wantStatus: StatusModified, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.setup(t, tc.vol) + defer os.RemoveAll(tc.vol.TargetPath) + + status := tc.vol.CheckStatus() + assert.Equal(t, tc.wantStatus, status.Status) + }) + } } diff --git a/internal/gitvolume/debug_find_common_dir_test.go b/internal/gitvolume/debug_find_common_dir_test.go deleted file mode 100644 index e2ac53f..0000000 --- a/internal/gitvolume/debug_find_common_dir_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package gitvolume - -import ( - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDebugFindCommonDir(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "git-volume-debug-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - // 1. Regular Repository - t.Run("Regular Repository", func(t *testing.T) { - repoDir := filepath.Join(tmpDir, "regular") - require.NoError(t, os.MkdirAll(repoDir, 0755)) - - cmd := exec.Command("git", "init", repoDir) - require.NoError(t, cmd.Run()) - - // Set identity - cmd = exec.Command("git", "-C", repoDir, "config", "user.email", "test@test.com") - require.NoError(t, cmd.Run()) - cmd = exec.Command("git", "-C", repoDir, "config", "user.name", "Test") - require.NoError(t, cmd.Run()) - - // Create a commit so we can create a worktree - require.NoError(t, os.WriteFile(filepath.Join(repoDir, "README.md"), []byte("init"), 0644)) - cmd = exec.Command("git", "-C", repoDir, "add", ".") - require.NoError(t, cmd.Run()) - cmd = exec.Command("git", "-C", repoDir, "commit", "-m", "Initial commit") - require.NoError(t, cmd.Run()) - - commonDir, err := findCommonDir(repoDir) - require.NoError(t, err) - - realRepoDir, _ := filepath.EvalSymlinks(repoDir) - assert.Equal(t, realRepoDir, commonDir, "Common dir of regular repo root should be itself") - - // Worktree from Regular Repo - wtDir := filepath.Join(tmpDir, "regular-worktree") - cmd = exec.Command("git", "-C", repoDir, "worktree", "add", wtDir) - require.NoError(t, cmd.Run()) - - commonDirWT, err := findCommonDir(wtDir) - require.NoError(t, err) - - commonDirWT, err = filepath.EvalSymlinks(commonDirWT) - require.NoError(t, err) - - assert.Equal(t, realRepoDir, commonDirWT, "Worktree from regular repo should point back to main repo root") - }) - - // 2. Bare Repository - t.Run("Bare Repository", func(t *testing.T) { - bareRepoDir := filepath.Join(tmpDir, "bare.git") - cmd := exec.Command("git", "init", "--bare", bareRepoDir) - require.NoError(t, cmd.Run()) - - realBareDir, err := filepath.EvalSymlinks(bareRepoDir) - require.NoError(t, err) - - commonDir, err := findCommonDir(bareRepoDir) - require.NoError(t, err) - - commonDir, err = filepath.EvalSymlinks(commonDir) - require.NoError(t, err) - - assert.Equal(t, realBareDir, commonDir, "Common dir of bare repo root should be itself") - - // Create a worktree from bare repo - // Need a commit first? Bare repos don't have commits unless pushed or created from existing. - // Let's create a regular repo first, then clone as bare to have commits. - srcRepo := filepath.Join(tmpDir, "src") - require.NoError(t, os.MkdirAll(srcRepo, 0755)) - cmd = exec.Command("git", "init", srcRepo) - require.NoError(t, cmd.Run()) - - cmd = exec.Command("git", "-C", srcRepo, "config", "user.email", "test@test.com") - require.NoError(t, cmd.Run()) - cmd = exec.Command("git", "-C", srcRepo, "config", "user.name", "Test") - require.NoError(t, cmd.Run()) - - require.NoError(t, os.WriteFile(filepath.Join(srcRepo, "README.md"), []byte("init"), 0644)) - cmd = exec.Command("git", "-C", srcRepo, "add", ".") - require.NoError(t, cmd.Run()) - cmd = exec.Command("git", "-C", srcRepo, "commit", "-m", "Initial commit") - require.NoError(t, cmd.Run()) - - // Clone as bare - bareCloned := filepath.Join(tmpDir, "bare-cloned.git") - cmd = exec.Command("git", "clone", "--bare", srcRepo, bareCloned) - require.NoError(t, cmd.Run()) - - realBareCloned, err := filepath.EvalSymlinks(bareCloned) - require.NoError(t, err) - - // Create worktree - wtDir := filepath.Join(tmpDir, "bare-worktree") - cmd = exec.Command("git", "-C", bareCloned, "worktree", "add", wtDir) - if out, err := cmd.CombinedOutput(); err != nil { - t.Logf("Git output: %s", string(out)) - require.NoError(t, err) - } - - commonDirWT, err := findCommonDir(wtDir) - require.NoError(t, err) - commonDirWT, err = filepath.EvalSymlinks(commonDirWT) - require.NoError(t, err) - - // DEBUGGING: Check what git rev-parse --git-common-dir returns here - cmd = exec.Command("git", "-C", wtDir, "rev-parse", "--git-common-dir") - out, err := cmd.Output() - if err != nil { - t.Logf("git rev-parse --git-common-dir failed: %v", err) - } - gitCommonDir := strings.TrimSpace(string(out)) - t.Logf("git rev-parse --git-common-dir in worktree: %s", gitCommonDir) - - // Check isBareRepository on that dir - isBare, err := isBareRepository(gitCommonDir) - t.Logf("isBareRepository(%s) = %v, err=%v", gitCommonDir, isBare, err) - - assert.Equal(t, realBareCloned, commonDirWT, "Worktree from bare repo should point back to bare repo root") - }) -}