Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
115 changes: 115 additions & 0 deletions internal/gitvolume/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
})
}
129 changes: 129 additions & 0 deletions internal/gitvolume/debug_find_common_dir_test.go
Original file line number Diff line number Diff line change
@@ -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", "[email protected]")
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", "[email protected]")
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")
})
}
140 changes: 140 additions & 0 deletions internal/gitvolume/fs_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading