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
126 changes: 126 additions & 0 deletions internal/gitvolume/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,26 @@ 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, err := os.Getwd()
require.NoError(t, err)
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 +412,109 @@ func TestHasGlobalVolumes(t *testing.T) {
})
}
}

func TestCheckStatus(t *testing.T) {
sourceDir, targetDir, cleanup := setupTestEnv(t)
defer cleanup()

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)
})
}
}
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
}
114 changes: 114 additions & 0 deletions internal/gitvolume/git_repro_test.go
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +15 to +17

Choose a reason for hiding this comment

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

medium

For modern Go tests (1.15+), it is recommended to use t.TempDir() instead of os.MkdirTemp. t.TempDir() automatically handles directory creation and cleanup, and ensures a unique directory for each test. Additionally, since this is an integration test that relies heavily on the git binary, it's good practice to check for its availability and skip the test if it's missing, preventing unnecessary failures in environments without git.

Suggested change
tmpDir, err := os.MkdirTemp("", "git-volume-repro-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git command not found, skipping integration test")
}
tmpDir := t.TempDir()
References
  1. The project follows standard Go conventions. Using t.TempDir() is the idiomatic way to manage temporary directories in tests. (link)


// 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)

Choose a reason for hiding this comment

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

medium

Since err is no longer declared at the start of the function (if using the suggested t.TempDir() change), it should be declared here using the short variable declaration operator :=.

Suggested change
bareRepo, err = filepath.EvalSymlinks(bareRepo)
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)

Choose a reason for hiding this comment

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

medium

Since err is no longer declared at the start of the function (if using the suggested t.TempDir() change), it should be declared here using the short variable declaration operator :=.

Suggested change
mainRepo, err = filepath.EvalSymlinks(mainRepo)
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")
})
}
Loading