-
Notifications
You must be signed in to change notification settings - Fork 38
fix: handle restack when branches are checked out in other worktrees #672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package git | ||
|
|
||
| import ( | ||
| "context" | ||
| "path/filepath" | ||
| "strings" | ||
| ) | ||
|
|
||
| // WorktreeForBranch returns the worktree path where the given branch is checked out, | ||
| // or an empty string if the branch is not checked out in any worktree. | ||
| // The branch should be in short format (e.g., "my-branch"). | ||
| func (r *Repo) WorktreeForBranch(ctx context.Context, branch string) (string, error) { | ||
| out, err := r.Run(ctx, &RunOpts{ | ||
| Args: []string{"worktree", "list", "--porcelain"}, | ||
| ExitError: true, | ||
| }) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| repoDir, _ := filepath.EvalSymlinks(r.repoDir) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error returned by repoDir, err := filepath.EvalSymlinks(r.repoDir)
if err != nil {
return "", err
} |
||
| var currentWorktree string | ||
| for line := range strings.SplitSeq(string(out.Stdout), "\n") { | ||
| if path, ok := strings.CutPrefix(line, "worktree "); ok { | ||
| currentWorktree = path | ||
| } | ||
| if ref, ok := strings.CutPrefix(line, "branch "); ok { | ||
| shortName := strings.TrimPrefix(ref, "refs/heads/") | ||
| if shortName == branch { | ||
| resolved, _ := filepath.EvalSymlinks(currentWorktree) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the earlier comment, the error from resolved, err := filepath.EvalSymlinks(currentWorktree)
if err != nil {
return "", err
} |
||
| if resolved != repoDir { | ||
| return currentWorktree, nil | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return "", nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,80 @@ | ||||||||||||||
| package git_test | ||||||||||||||
|
|
||||||||||||||
| import ( | ||||||||||||||
| "path/filepath" | ||||||||||||||
| "strings" | ||||||||||||||
| "testing" | ||||||||||||||
|
|
||||||||||||||
| "github.com/aviator-co/av/internal/git" | ||||||||||||||
| "github.com/aviator-co/av/internal/git/gittest" | ||||||||||||||
| "github.com/stretchr/testify/require" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| func TestWorktreeForBranch(t *testing.T) { | ||||||||||||||
| repo := gittest.NewTempRepo(t) | ||||||||||||||
| avRepo := repo.AsAvGitRepo() | ||||||||||||||
|
|
||||||||||||||
| // Create a feature branch with a commit. | ||||||||||||||
| repo.Git(t, "checkout", "-b", "feature-1") | ||||||||||||||
| repo.CommitFile(t, "feature1.txt", "feature 1") | ||||||||||||||
| repo.Git(t, "checkout", "main") | ||||||||||||||
|
|
||||||||||||||
| // feature-1 is not checked out in any worktree, so should return empty. | ||||||||||||||
| wt, err := avRepo.WorktreeForBranch(t.Context(), "feature-1") | ||||||||||||||
| require.NoError(t, err) | ||||||||||||||
| require.Empty(t, wt) | ||||||||||||||
|
|
||||||||||||||
| // Add a worktree for feature-1. | ||||||||||||||
| wtPath := t.TempDir() | ||||||||||||||
| repo.Git(t, "worktree", "add", wtPath, "feature-1") | ||||||||||||||
|
|
||||||||||||||
| // Now feature-1 should be detected in the worktree. | ||||||||||||||
| wt, err = avRepo.WorktreeForBranch(t.Context(), "feature-1") | ||||||||||||||
| require.NoError(t, err) | ||||||||||||||
| // Resolve symlinks for comparison (macOS /var -> /private/var). | ||||||||||||||
| resolvedWt, _ := filepath.EvalSymlinks(wt) | ||||||||||||||
| resolvedExpected, _ := filepath.EvalSymlinks(wtPath) | ||||||||||||||
|
Comment on lines
+35
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Errors from
Suggested change
|
||||||||||||||
| require.Equal(t, resolvedExpected, resolvedWt) | ||||||||||||||
|
|
||||||||||||||
| // main is checked out in the main repo, not a different worktree. | ||||||||||||||
| wt, err = avRepo.WorktreeForBranch(t.Context(), "main") | ||||||||||||||
| require.NoError(t, err) | ||||||||||||||
| require.Empty(t, wt) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| func TestRebaseInWorktree(t *testing.T) { | ||||||||||||||
| repo := gittest.NewTempRepo(t) | ||||||||||||||
| avRepo := repo.AsAvGitRepo() | ||||||||||||||
|
|
||||||||||||||
| // Create a stack: main -> feature-1 -> feature-2 | ||||||||||||||
| repo.Git(t, "checkout", "-b", "feature-1") | ||||||||||||||
| repo.CommitFile(t, "feature1.txt", "feature 1") | ||||||||||||||
| repo.Git(t, "checkout", "-b", "feature-2") | ||||||||||||||
| repo.CommitFile(t, "feature2.txt", "feature 2") | ||||||||||||||
| repo.Git(t, "checkout", "main") | ||||||||||||||
|
|
||||||||||||||
| // Add a new commit on main to create something to restack onto. | ||||||||||||||
| repo.CommitFile(t, "main-update.txt", "main update") | ||||||||||||||
|
|
||||||||||||||
| // Put feature-1 in a separate worktree. | ||||||||||||||
| wtPath := t.TempDir() | ||||||||||||||
| repo.Git(t, "worktree", "add", wtPath, "feature-1") | ||||||||||||||
|
|
||||||||||||||
| // Rebase feature-1 onto main (simulating restack). | ||||||||||||||
| // This would fail without worktree-aware rebase because feature-1 | ||||||||||||||
| // is checked out in another worktree. | ||||||||||||||
| mainHash := strings.TrimSpace(repo.Git(t, "rev-parse", "main")) | ||||||||||||||
|
|
||||||||||||||
| // Get the merge-base for feature-1 and main (the original branch point). | ||||||||||||||
| mergeBase := strings.TrimSpace(repo.Git(t, "merge-base", "main", "feature-1")) | ||||||||||||||
|
|
||||||||||||||
| result, err := avRepo.RebaseParse(t.Context(), git.RebaseOpts{ | ||||||||||||||
| Branch: "feature-1", | ||||||||||||||
| Upstream: mergeBase, | ||||||||||||||
| Onto: mainHash, | ||||||||||||||
| }) | ||||||||||||||
| require.NoError(t, err) | ||||||||||||||
| require.NotNil(t, result) | ||||||||||||||
| // The rebase should succeed (updated or already up to date). | ||||||||||||||
| require.NotEqual(t, git.RebaseConflict, result.Status) | ||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation of
runInDiris not safe for concurrent use. It mutates therepoDirfield of theRepostruct, which can lead to race conditions if other goroutines are using the sameRepoinstance. A safer approach is to create a shallow copy of theRepoobject and modify itsrepoDirfor the scope of this call, avoiding mutation of the original object.