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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Columns:

Bring existing branch worktrees under `git-wt` management.

- Moves the main worktree into `<root>/main/<repo-name>` when it is still at `<root>/main`.
- Moves the main worktree into `<root>/main/<repo-name>` when it is still a plain clone at `<root>` or on the old layout at `<root>/main`.
- Creates managed worktrees for local branches that do not already have one.
- Renames existing non-managed branch worktrees into the managed path format.

Expand Down
26 changes: 20 additions & 6 deletions internal/gitwt/git_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ func gitOutput(directory string, args ...string) (gitCommandResult, error) {
// <mainPath> = <root>/main/<repo>
// managed = <root>/<worktreeName>/<repo>
//
// Old main layout (basename mainPath == "main") is only handled by migrate.
// migrate also accepts non-nested mains:
//
// plain clone: <root> (basename is the repo name)
// old layout: <root>/main
func worktreeRoot(mainPath string) string {
return filepath.Dir(filepath.Dir(mainPath))
}
Expand All @@ -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 <root>/main/<repo>.
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 (<root>/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 <root>: <root>/main/<basename(root)>
// old layout at <root>/main: <root>/main/<basename(root)>
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 {
Expand Down
7 changes: 5 additions & 2 deletions internal/gitwt/gitwt_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,11 @@ func (x *migrateCommandOptions) Execute(command *cobra.Command, args []string) e
return nil
}

// migrateMainWorktree moves main from <root>/main to <root>/main/<repo> via a
// temporary sibling path (a directory cannot be moved into itself).
// migrateMainWorktree moves main into <root>/main/<repo> via a temporary
// sibling path (a directory cannot be moved into a path under itself).
//
// Covers plain clone (<root> -> <root>/main/<repo>) and old layout
// (<root>/main -> <root>/main/<repo>).
//
// git worktree move refuses to move the main working tree, so this uses
// filesystem renames and git worktree repair to fix linked worktree gitdirs.
Expand Down
4 changes: 0 additions & 4 deletions internal/gitwt/gitwt_off.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 67 additions & 3 deletions internal/gitwt/gitwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -1197,7 +1242,23 @@ func newOldLayoutTestRepository(t *testing.T) testRepository {
return initTestRepository(t, rootPath, mainPath)
}

// newPlainCloneTestRepository creates a normal single-checkout clone at
// <root> (basename is the repo name) so migrate can nest main under
// <root>/main/<repo>.
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", "")

Expand All @@ -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)
Expand Down