51cc14d028
* refactor(worktree): drop unused flat-layout worktrees/ path
Every worktree creation now routes through the task-dir layout
(~/.kandev/tasks/{title}_{suffix}/{repo}/); the legacy flat layout
under ~/.kandev/worktrees/ has been unreachable since the subtasks
PR populated TaskDirName/RepoName unconditionally. Removes the dead
Config.BasePath, Manager.createWorktree, and the flat reconciler.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(worktree): remove unused Reconcile, add guard regression test
Address PR review feedback:
- Reconcile (and reconcileTasksDir / isOrphanedTaskDir) had no callers
anywhere in the tree after the flat-layout removal. Deleted rather than
left dangling; if startup orphan cleanup is wired up later it can return
alongside its actual call site. Also moots the stale doc comment that
CodeRabbit flagged.
- Add TestCreate_MissingTaskDirFields_ReturnsErrTaskDirRequired covering
the three permutations (missing RepoName, missing TaskDirName, both
missing) so a regression in the ErrTaskDirRequired guard surfaces in
CI rather than silently.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
235 lines
7.5 KiB
Go
235 lines
7.5 KiB
Go
package worktree
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func allowFileProtocol(t *testing.T) {
|
|
t.Helper()
|
|
t.Setenv("GIT_CONFIG_COUNT", "1")
|
|
t.Setenv("GIT_CONFIG_KEY_0", "protocol.file.allow")
|
|
t.Setenv("GIT_CONFIG_VALUE_0", "always")
|
|
}
|
|
|
|
func initRepoWithSubmodule(t *testing.T) (repoPath, submodulePath string) {
|
|
t.Helper()
|
|
allowFileProtocol(t)
|
|
|
|
// Create the submodule repository first.
|
|
submodulePath = t.TempDir()
|
|
runGit(t, submodulePath, "init", "-b", "main")
|
|
runGit(t, submodulePath, "config", "user.email", "test@example.com")
|
|
runGit(t, submodulePath, "config", "user.name", "Test User")
|
|
runGit(t, submodulePath, "config", "commit.gpgsign", "false")
|
|
if err := os.WriteFile(filepath.Join(submodulePath, "lib.txt"), []byte("library code\n"), 0644); err != nil {
|
|
t.Fatalf("write lib.txt: %v", err)
|
|
}
|
|
runGit(t, submodulePath, "add", ".")
|
|
runGit(t, submodulePath, "commit", "-m", "initial submodule commit")
|
|
|
|
// Create the main repository.
|
|
repoPath = t.TempDir()
|
|
runGit(t, repoPath, "init", "-b", "main")
|
|
runGit(t, repoPath, "config", "user.email", "test@example.com")
|
|
runGit(t, repoPath, "config", "user.name", "Test User")
|
|
runGit(t, repoPath, "config", "commit.gpgsign", "false")
|
|
if err := os.WriteFile(filepath.Join(repoPath, "main.txt"), []byte("main code\n"), 0644); err != nil {
|
|
t.Fatalf("write main.txt: %v", err)
|
|
}
|
|
runGit(t, repoPath, "add", ".")
|
|
runGit(t, repoPath, "commit", "-m", "initial main commit")
|
|
|
|
// Add the submodule.
|
|
runGit(t, repoPath, "submodule", "add", submodulePath, "libs/external")
|
|
runGit(t, repoPath, "commit", "-m", "add submodule")
|
|
|
|
return repoPath, submodulePath
|
|
}
|
|
|
|
func TestGetSubmodulePaths(t *testing.T) {
|
|
repoPath, _ := initRepoWithSubmodule(t)
|
|
|
|
paths, err := getSubmodulePaths(context.Background(), repoPath)
|
|
if err != nil {
|
|
t.Fatalf("getSubmodulePaths() error: %v", err)
|
|
}
|
|
if len(paths) != 1 || paths[0] != "libs/external" {
|
|
t.Errorf("getSubmodulePaths() = %v, want [libs/external]", paths)
|
|
}
|
|
}
|
|
|
|
func TestGetSubmodulePaths_NoSubmodules(t *testing.T) {
|
|
repoPath := t.TempDir()
|
|
runGit(t, repoPath, "init", "-b", "main")
|
|
runGit(t, repoPath, "config", "user.email", "test@example.com")
|
|
runGit(t, repoPath, "config", "user.name", "Test User")
|
|
runGit(t, repoPath, "config", "commit.gpgsign", "false")
|
|
if err := os.WriteFile(filepath.Join(repoPath, "file.txt"), []byte("content\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runGit(t, repoPath, "add", ".")
|
|
runGit(t, repoPath, "commit", "-m", "initial")
|
|
|
|
paths, err := getSubmodulePaths(context.Background(), repoPath)
|
|
if err != nil {
|
|
t.Fatalf("getSubmodulePaths() error: %v", err)
|
|
}
|
|
if len(paths) != 0 {
|
|
t.Errorf("getSubmodulePaths() = %v, want empty", paths)
|
|
}
|
|
}
|
|
|
|
func TestGetSubmodulePaths_MultipleSubmodules(t *testing.T) {
|
|
allowFileProtocol(t)
|
|
// Create two submodule repos.
|
|
sub1 := t.TempDir()
|
|
runGit(t, sub1, "init", "-b", "main")
|
|
runGit(t, sub1, "config", "user.email", "test@example.com")
|
|
runGit(t, sub1, "config", "user.name", "Test User")
|
|
runGit(t, sub1, "config", "commit.gpgsign", "false")
|
|
if err := os.WriteFile(filepath.Join(sub1, "a.txt"), []byte("a\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runGit(t, sub1, "add", ".")
|
|
runGit(t, sub1, "commit", "-m", "init")
|
|
|
|
sub2 := t.TempDir()
|
|
runGit(t, sub2, "init", "-b", "main")
|
|
runGit(t, sub2, "config", "user.email", "test@example.com")
|
|
runGit(t, sub2, "config", "user.name", "Test User")
|
|
runGit(t, sub2, "config", "commit.gpgsign", "false")
|
|
if err := os.WriteFile(filepath.Join(sub2, "b.txt"), []byte("b\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runGit(t, sub2, "add", ".")
|
|
runGit(t, sub2, "commit", "-m", "init")
|
|
|
|
// Main repo with both submodules.
|
|
repoPath := t.TempDir()
|
|
runGit(t, repoPath, "init", "-b", "main")
|
|
runGit(t, repoPath, "config", "user.email", "test@example.com")
|
|
runGit(t, repoPath, "config", "user.name", "Test User")
|
|
runGit(t, repoPath, "config", "commit.gpgsign", "false")
|
|
if err := os.WriteFile(filepath.Join(repoPath, "root.txt"), []byte("root\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runGit(t, repoPath, "add", ".")
|
|
runGit(t, repoPath, "commit", "-m", "initial")
|
|
|
|
runGit(t, repoPath, "submodule", "add", sub1, "vendor/alpha")
|
|
runGit(t, repoPath, "submodule", "add", sub2, "vendor/beta")
|
|
runGit(t, repoPath, "commit", "-m", "add submodules")
|
|
|
|
paths, err := getSubmodulePaths(context.Background(), repoPath)
|
|
if err != nil {
|
|
t.Fatalf("getSubmodulePaths() error: %v", err)
|
|
}
|
|
sort.Strings(paths)
|
|
if len(paths) != 2 || paths[0] != "vendor/alpha" || paths[1] != "vendor/beta" {
|
|
t.Errorf("getSubmodulePaths() = %v, want [vendor/alpha vendor/beta]", paths)
|
|
}
|
|
}
|
|
|
|
func TestGetSubmodulePaths_WorktreeNoCheckout(t *testing.T) {
|
|
repoPath, _ := initRepoWithSubmodule(t)
|
|
|
|
wtPath := filepath.Join(t.TempDir(), "wt")
|
|
runGit(t, repoPath, "worktree", "add", "--no-checkout", "-b", "test-branch", wtPath, "main")
|
|
|
|
paths, err := getSubmodulePaths(context.Background(), wtPath)
|
|
if err != nil {
|
|
t.Fatalf("getSubmodulePaths() in --no-checkout worktree error: %v", err)
|
|
}
|
|
if len(paths) != 1 || paths[0] != "libs/external" {
|
|
t.Errorf("getSubmodulePaths() = %v, want [libs/external]", paths)
|
|
}
|
|
}
|
|
|
|
func TestInitSubmodules(t *testing.T) {
|
|
repoPath, _ := initRepoWithSubmodule(t)
|
|
|
|
// Create a worktree — submodule won't be initialized.
|
|
wtPath := filepath.Join(t.TempDir(), "wt")
|
|
runGit(t, repoPath, "worktree", "add", "-b", "sub-test", wtPath, "main")
|
|
|
|
// Submodule directory should exist but be empty (not initialized).
|
|
subDir := filepath.Join(wtPath, "libs", "external")
|
|
entries, err := os.ReadDir(subDir)
|
|
if err != nil {
|
|
t.Fatalf("submodule dir should exist: %v", err)
|
|
}
|
|
if len(entries) != 0 {
|
|
t.Fatalf("submodule dir should be empty before init, got %d entries", len(entries))
|
|
}
|
|
|
|
// Run initSubmodules.
|
|
log := newTestLogger()
|
|
m := &Manager{logger: log}
|
|
m.initSubmodules(context.Background(), wtPath)
|
|
|
|
// Submodule should now have content.
|
|
content, err := os.ReadFile(filepath.Join(subDir, "lib.txt"))
|
|
if err != nil {
|
|
t.Fatalf("read submodule file after init: %v", err)
|
|
}
|
|
if strings.TrimSpace(string(content)) != "library code" {
|
|
t.Errorf("submodule lib.txt = %q, want %q", string(content), "library code")
|
|
}
|
|
}
|
|
|
|
func TestCreateWorktree_WithSubmodules(t *testing.T) {
|
|
repoPath, _ := initRepoWithSubmodule(t)
|
|
|
|
cfg := newTestConfig(t)
|
|
log := newTestLogger()
|
|
store := newMockStore()
|
|
|
|
mgr, err := NewManager(cfg, store, log)
|
|
if err != nil {
|
|
t.Fatalf("NewManager: %v", err)
|
|
}
|
|
|
|
wt, err := mgr.Create(context.Background(), CreateRequest{
|
|
TaskID: "sub-task-1",
|
|
SessionID: "sub-session-1",
|
|
TaskTitle: "Submodule Test",
|
|
RepositoryID: "repo-sub",
|
|
RepositoryPath: repoPath,
|
|
BaseBranch: "main",
|
|
TaskDirName: "sub-task-1",
|
|
RepoName: "repo-sub",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Create() failed: %v", err)
|
|
}
|
|
|
|
// Main file should be present.
|
|
mainContent, err := os.ReadFile(filepath.Join(wt.Path, "main.txt"))
|
|
if err != nil {
|
|
t.Fatalf("read main.txt: %v", err)
|
|
}
|
|
if strings.TrimSpace(string(mainContent)) != "main code" {
|
|
t.Errorf("main.txt = %q, want %q", string(mainContent), "main code")
|
|
}
|
|
|
|
// Submodule should be initialized with content.
|
|
subContent, err := os.ReadFile(filepath.Join(wt.Path, "libs", "external", "lib.txt"))
|
|
if err != nil {
|
|
t.Fatalf("read submodule lib.txt: %v", err)
|
|
}
|
|
if strings.TrimSpace(string(subContent)) != "library code" {
|
|
t.Errorf("submodule lib.txt = %q, want %q", string(subContent), "library code")
|
|
}
|
|
|
|
// Working tree should be clean.
|
|
status := strings.TrimSpace(runGit(t, wt.Path, "status", "--porcelain"))
|
|
if status != "" {
|
|
t.Errorf("worktree is not clean: %s", status)
|
|
}
|
|
}
|