diff --git a/README.md b/README.md index c1b9695..2c950a7 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Columns: Bring existing branch worktrees under `git-wt` management. -- Moves the main worktree into `/main/` when it is still at `/main`. +- Moves the main worktree into `/main/` when it is still a plain clone at `` or on the old layout at `/main`. - Creates managed worktrees for local branches that do not already have one. - Renames existing non-managed branch worktrees into the managed path format. diff --git a/internal/gitwt/git_helpers.go b/internal/gitwt/git_helpers.go index bb1adb7..1092d75 100644 --- a/internal/gitwt/git_helpers.go +++ b/internal/gitwt/git_helpers.go @@ -45,7 +45,10 @@ func gitOutput(directory string, args ...string) (gitCommandResult, error) { // = /main/ // managed = // // -// Old main layout (basename mainPath == "main") is only handled by migrate. +// migrate also accepts non-nested mains: +// +// plain clone: (basename is the repo name) +// old layout: /main func worktreeRoot(mainPath string) string { return filepath.Dir(filepath.Dir(mainPath)) } @@ -58,15 +61,26 @@ func managedWorktreePath(mainPath string, worktreeName string) string { return filepath.Join(worktreeRoot(mainPath), worktreeName, repoName(mainPath)) } +// mainIsNestedLayout reports whether main is already at /main/. +func mainIsNestedLayout(mainPath string) bool { + return filepath.Base(filepath.Dir(mainPath)) == "main" && filepath.Base(mainPath) != "main" +} + func mainNeedsLayoutMigration(mainPath string) bool { - return filepath.Base(mainPath) == "main" + return !mainIsNestedLayout(mainPath) } -// migratedMainPath returns the nested main path when main is still on the old -// layout (/main). repo name is the basename of the worktree root. +// migratedMainPath returns the nested main path for a non-nested main checkout. +// +// plain clone at : /main/ +// old layout at /main: /main/ func migratedMainPath(mainPath string) string { - root := filepath.Dir(mainPath) - return filepath.Join(root, "main", filepath.Base(root)) + if filepath.Base(mainPath) == "main" { + root := filepath.Dir(mainPath) + return filepath.Join(root, "main", filepath.Base(root)) + } + // Plain clone: the checkout path is the worktree root. + return filepath.Join(mainPath, "main", filepath.Base(mainPath)) } func ensureWorktreeDirectory(worktreePath string) error { diff --git a/internal/gitwt/gitwt_migrate.go b/internal/gitwt/gitwt_migrate.go index e98a606..9477bf8 100644 --- a/internal/gitwt/gitwt_migrate.go +++ b/internal/gitwt/gitwt_migrate.go @@ -103,8 +103,11 @@ func (x *migrateCommandOptions) Execute(command *cobra.Command, args []string) e return nil } -// migrateMainWorktree moves main from /main to /main/ via a -// temporary sibling path (a directory cannot be moved into itself). +// migrateMainWorktree moves main into /main/ via a temporary +// sibling path (a directory cannot be moved into a path under itself). +// +// Covers plain clone ( -> /main/) and old layout +// (/main -> /main/). // // git worktree move refuses to move the main working tree, so this uses // filesystem renames and git worktree repair to fix linked worktree gitdirs. diff --git a/internal/gitwt/gitwt_off.go b/internal/gitwt/gitwt_off.go index 17892c1..8f7c5ee 100644 --- a/internal/gitwt/gitwt_off.go +++ b/internal/gitwt/gitwt_off.go @@ -74,10 +74,6 @@ func (x *offCommandOptions) Execute(command *cobra.Command, args []string) error return err } -func mainIsNestedLayout(mainPath string) bool { - return filepath.Base(filepath.Dir(mainPath)) == "main" && filepath.Base(mainPath) != "main" -} - func ensureManagedWorktreesClean(worktrees []managedWorktree, force bool) error { if force { return nil diff --git a/internal/gitwt/gitwt_test.go b/internal/gitwt/gitwt_test.go index 52719e3..b0b916b 100644 --- a/internal/gitwt/gitwt_test.go +++ b/internal/gitwt/gitwt_test.go @@ -1084,6 +1084,51 @@ func TestMigrateMovesMainIntoNestedLayout(t *testing.T) { } } +func TestMigrateMovesPlainCloneMainIntoNestedLayout(t *testing.T) { + testRepository := newPlainCloneTestRepository(t) + oldMainPath := testRepository.mainPath + nestedMainPath := migratedMainPath(oldMainPath) + + result := testRepository.runGitWT(t, "migrate") + if result.err != nil { + t.Fatalf("migrate failed: %v\n%s", result.err, result.stderr) + } + + testRepository.assertPathPresent(t, nestedMainPath) + assertCurrentBranchAtPath(t, nestedMainPath, "main") + assertMainWorktreePath(t, nestedMainPath) + if filepath.Dir(filepath.Dir(nestedMainPath)) != oldMainPath { + t.Fatalf("expected nested main under plain clone root %s, got %s", oldMainPath, nestedMainPath) + } + if !strings.Contains(result.stderr, "migrated main to") { + t.Fatalf("expected main migration message, got stderr:\n%s", result.stderr) + } +} + +func TestMigratePlainCloneCreatesBranchWorktreesUnderRoot(t *testing.T) { + const branchName = "dev" + + testRepository := newPlainCloneTestRepository(t) + nestedMainPath := migratedMainPath(testRepository.mainPath) + nestedBranchPath := managedWorktreePath(nestedMainPath, branchName) + + testRepository.createLocalBranch(t, branchName) + + result := testRepository.runGitWT(t, "migrate") + if result.err != nil { + t.Fatalf("migrate failed: %v\n%s", result.err, result.stderr) + } + + testRepository.assertPathPresent(t, nestedMainPath) + testRepository.assertPathPresent(t, nestedBranchPath) + assertCurrentBranchAtPath(t, nestedMainPath, "main") + assertCurrentBranchAtPath(t, nestedBranchPath, branchName) + assertMainWorktreePath(t, nestedMainPath) + if filepath.Dir(filepath.Dir(nestedBranchPath)) != testRepository.rootPath { + t.Fatalf("expected branch worktree under plain clone root %s, got %s", testRepository.rootPath, nestedBranchPath) + } +} + func TestMigrateMovesMainAndOldLayoutFeatureWorktrees(t *testing.T) { const branchName = "feature/login" @@ -1197,7 +1242,23 @@ func newOldLayoutTestRepository(t *testing.T) testRepository { return initTestRepository(t, rootPath, mainPath) } +// newPlainCloneTestRepository creates a normal single-checkout clone at +// (basename is the repo name) so migrate can nest main under +// /main/. +func newPlainCloneTestRepository(t *testing.T) testRepository { + t.Helper() + basePath := t.TempDir() + rootPath := filepath.Join(basePath, testRepoName) + // Keep the bare remote outside the clone so moving main does not move it. + return initTestRepositoryWithRemoteParent(t, rootPath, rootPath, basePath) +} + func initTestRepository(t *testing.T, rootPath string, mainPath string) testRepository { + t.Helper() + return initTestRepositoryWithRemoteParent(t, rootPath, mainPath, rootPath) +} + +func initTestRepositoryWithRemoteParent(t *testing.T, rootPath string, mainPath string, remoteParent string) testRepository { t.Helper() t.Setenv("HERDR_ENV", "") @@ -1207,10 +1268,13 @@ func initTestRepository(t *testing.T, rootPath string, mainPath string) testRepo if err := os.MkdirAll(rootPath, 0o755); err != nil { t.Fatalf("create root: %v", err) } + if err := os.MkdirAll(remoteParent, 0o755); err != nil { + t.Fatalf("create remote parent: %v", err) + } - remotePath := filepath.Join(rootPath, "remote.git") - runGitCommand(t, rootPath, "init", "--bare", remotePath) - runGitCommand(t, rootPath, "init", "--initial-branch=main", mainPath) + remotePath := filepath.Join(remoteParent, "remote.git") + runGitCommand(t, remoteParent, "init", "--bare", remotePath) + runGitCommand(t, filepath.Dir(mainPath), "init", "--initial-branch=main", mainPath) runGitCommand(t, mainPath, "config", "user.name", "Test User") runGitCommand(t, mainPath, "config", "user.email", "test@example.com") runGitCommand(t, mainPath, "remote", "add", remoteName, remotePath)