Skip to content

Commit c762657

Browse files
committed
Reuse validated worktree metadata within operations
1 parent af28053 commit c762657

5 files changed

Lines changed: 181 additions & 36 deletions

File tree

‎internal/git/gitops.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func CurrentOps() Ops {
124124
// --- defaultOps method implementations ---
125125

126126
func (d *defaultOps) GitDir() (string, error) {
127-
return d.path("rev-parse", "--absolute-git-dir")
127+
return d.directory("--absolute-git-dir")
128128
}
129129

130130
func (d *defaultOps) RootDir() (string, error) {

‎internal/git/gitops_test.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,39 @@ func TestIntegration_ForWorktreeIndependentExecutors(t *testing.T) {
10581058
assert.Equal(t, "main", gitExec(t, dir, "branch", "--show-current"))
10591059
}
10601060

1061+
func TestIntegration_WorktreeDirectoriesReuseOnlyCurrentValidation(t *testing.T) {
1062+
root, dir := setupWorktreeRepo(t)
1063+
linked, path := addTestWorktree(t, root, dir, "feature")
1064+
tracePath := filepath.Join(t.TempDir(), "trace.jsonl")
1065+
t.Setenv("GIT_TRACE2_EVENT", tracePath)
1066+
common, err := linked.CommonDir()
1067+
require.NoError(t, err)
1068+
assert.Equal(t, canonicalGitTestPath(t, filepath.Join(dir, ".git")), common)
1069+
gitDir, err := linked.GitDir()
1070+
require.NoError(t, err)
1071+
assert.NotEqual(t, common, gitDir)
1072+
requireWorktree(t, linked, path)
1073+
trace, err := os.ReadFile(tracePath)
1074+
require.NoError(t, err)
1075+
probes := 0
1076+
for _, line := range strings.Split(strings.TrimSpace(string(trace)), "\n") {
1077+
var event struct {
1078+
Event string
1079+
Argv []string
1080+
}
1081+
require.NoError(t, json.Unmarshal([]byte(line), &event))
1082+
if event.Event == "start" && slices.Contains(event.Argv, "rev-parse") {
1083+
probes++
1084+
}
1085+
}
1086+
assert.Equal(t, 8, probes, "directory queries reuse current validation; selecting a scope still validates both directories")
1087+
require.NoError(t, os.Rename(path, path+"-moved"))
1088+
_, err = linked.CommonDir()
1089+
require.Error(t, err, "the previous validation must not be cached across calls")
1090+
_, err = linked.GitDir()
1091+
require.Error(t, err)
1092+
}
1093+
10611094
func TestIntegration_WorktreeMissingCheckoutDoesNotReportStagedChanges(t *testing.T) {
10621095
t.Parallel()
10631096
root, dir := setupWorktreeRepo(t)

‎internal/git/worktree.go‎

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,42 +32,81 @@ func (d *defaultOps) path(args ...string) (string, error) {
3232
}
3333

3434
func (d *defaultOps) CommonDir() (string, error) {
35-
return d.path("rev-parse", "--path-format=absolute", "--git-common-dir")
35+
return d.directory("--git-common-dir")
3636
}
3737

38-
func (d *defaultOps) gitClient() (*cligit.Client, error) {
38+
type validatedGitClient struct {
39+
client *cligit.Client
40+
gitDir string
41+
commonDir string
42+
}
43+
44+
func (d *defaultOps) inspectGitClient() (validatedGitClient, error) {
3945
c := d.client
4046
if c == nil {
4147
c = client
4248
}
49+
validated := validatedGitClient{client: c}
4350
for _, directory := range []struct {
4451
option string
4552
info os.FileInfo
53+
path *string
4654
}{
47-
{"--absolute-git-dir", d.gitDir},
48-
{"--git-common-dir", d.commonDir},
55+
{"--absolute-git-dir", d.gitDir, &validated.gitDir},
56+
{"--git-common-dir", d.commonDir, &validated.commonDir},
4957
} {
5058
if directory.info == nil {
5159
continue
5260
}
5361
cmd, err := c.Command(context.Background(), "rev-parse", "--path-format=absolute", directory.option)
5462
if err != nil {
55-
return nil, err
63+
return validatedGitClient{}, err
5664
}
5765
d.configureCommand(cmd)
5866
out, err := cmd.Output()
5967
if err != nil {
60-
return nil, fmt.Errorf("inspecting worktree %q: %w", c.RepoDir, err)
68+
return validatedGitClient{}, fmt.Errorf("inspecting worktree %q: %w", c.RepoDir, err)
6169
}
62-
info, err := os.Stat(strings.TrimSuffix(string(out), "\n"))
70+
path := strings.TrimSuffix(string(out), "\n")
71+
info, err := os.Stat(path)
6372
if err != nil {
64-
return nil, fmt.Errorf("inspecting worktree %q: %w", c.RepoDir, err)
73+
return validatedGitClient{}, fmt.Errorf("inspecting worktree %q: %w", c.RepoDir, err)
6574
}
6675
if !os.SameFile(directory.info, info) {
67-
return nil, fmt.Errorf("worktree %q no longer refers to the selected Git directory; rediscover worktrees before continuing", c.RepoDir)
76+
return validatedGitClient{}, fmt.Errorf("worktree %q no longer refers to the selected Git directory; rediscover worktrees before continuing", c.RepoDir)
6877
}
78+
*directory.path = path
6979
}
70-
return c, nil
80+
return validated, nil
81+
}
82+
83+
func (d *defaultOps) gitClient() (*cligit.Client, error) {
84+
validated, err := d.inspectGitClient()
85+
return validated.client, err
86+
}
87+
88+
func (d *defaultOps) directory(option string) (string, error) {
89+
validated, err := d.inspectGitClient()
90+
if err != nil {
91+
return "", err
92+
}
93+
// Reuse only paths validated by this call, never across commands.
94+
if option == "--absolute-git-dir" && validated.gitDir != "" {
95+
return validated.gitDir, nil
96+
}
97+
if option == "--git-common-dir" && validated.commonDir != "" {
98+
return validated.commonDir, nil
99+
}
100+
cmd, err := validated.client.Command(context.Background(), "rev-parse", "--path-format=absolute", option)
101+
if err != nil {
102+
return "", err
103+
}
104+
d.configureCommand(cmd)
105+
out, err := cmd.Output()
106+
if err != nil {
107+
return "", err
108+
}
109+
return strings.TrimSuffix(string(out), "\n"), nil
71110
}
72111

73112
// ForWorktree resolves relative paths against this receiver's execution
@@ -76,13 +115,17 @@ func (d *defaultOps) ForWorktree(path string) (Ops, error) {
76115
if path == "" {
77116
return nil, errors.New("worktree path must not be empty")
78117
}
79-
c, err := d.gitClient()
118+
validated, err := d.inspectGitClient()
80119
if err != nil {
81120
return nil, err
82121
}
83-
common, err := d.CommonDir()
84-
if err != nil {
85-
return nil, fmt.Errorf("locating the source repository: %w", err)
122+
c := validated.client
123+
common := validated.commonDir
124+
if common == "" {
125+
common, err = d.CommonDir()
126+
if err != nil {
127+
return nil, fmt.Errorf("locating the source repository: %w", err)
128+
}
86129
}
87130
expectedCommon, err := os.Stat(common)
88131
if err != nil {

‎internal/worktree/context.go‎

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -202,36 +202,40 @@ func CheckClean(ops git.Ops, path string) error {
202202
func (c *Context) Preflight(branches []string) error {
203203
checked := make(map[string]bool)
204204
for _, branch := range branches {
205-
ops, err := c.Ops(branch)
206-
if err != nil {
205+
if _, err := c.preflightBranch(branch, checked); err != nil {
207206
return err
208207
}
209-
location := c.Location(branch)
210-
if !checked[location.Path] {
211-
if err := CheckClean(ops, location.Path); err != nil {
212-
return err
213-
}
214-
checked[location.Path] = true
208+
}
209+
return nil
210+
}
211+
212+
func (c *Context) preflightBranch(branch string, checked map[string]bool) (git.Ops, error) {
213+
ops, err := c.Ops(branch)
214+
if err != nil {
215+
return nil, err
216+
}
217+
location := c.Location(branch)
218+
if !checked[location.Path] {
219+
if err := CheckClean(ops, location.Path); err != nil {
220+
return nil, err
215221
}
216-
if !SamePath(location.Path, c.Origin.Path) {
217-
current, err := ops.CurrentBranch()
218-
if err != nil {
219-
return fmt.Errorf("checking current branch in worktree %s: %w", location.Path, err)
220-
}
221-
if current != branch {
222-
return fmt.Errorf("worktree %s no longer has branch %s checked out", location.Path, branch)
223-
}
222+
checked[location.Path] = true
223+
}
224+
if !SamePath(location.Path, c.Origin.Path) {
225+
current, err := ops.CurrentBranch()
226+
if err != nil {
227+
return nil, fmt.Errorf("checking current branch in worktree %s: %w", location.Path, err)
228+
}
229+
if current != branch {
230+
return nil, fmt.Errorf("worktree %s no longer has branch %s checked out", location.Path, branch)
224231
}
225232
}
226-
return nil
233+
return ops, nil
227234
}
228235

229236
// Prepare may switch the initiating worktree, but never a different worktree.
230237
func (c *Context) Prepare(branch string) (git.Ops, error) {
231-
if err := c.Preflight([]string{branch}); err != nil {
232-
return nil, err
233-
}
234-
ops, err := c.Ops(branch)
238+
ops, err := c.preflightBranch(branch, make(map[string]bool))
235239
if err != nil {
236240
return nil, err
237241
}

‎internal/worktree/context_test.go‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,68 @@ func TestStartRejectsCommitsAfterSnapshot(t *testing.T) {
248248
assert.Empty(t, ctx.Pending)
249249
assert.Empty(t, ctx.Touched)
250250
}
251+
252+
func TestPrepareReusesOnlyTheCurrentPreflightExecutor(t *testing.T) {
253+
common := t.TempDir()
254+
current := "main"
255+
resolutions := 0
256+
scoped := &git.MockOps{
257+
CommonDirFn: func() (string, error) { return common, nil },
258+
GitDirFn: func() (string, error) { return common, nil },
259+
CurrentBranchFn: func() (string, error) { return current, nil },
260+
CheckoutBranchFn: func(branch string) error {
261+
current = branch
262+
return nil
263+
},
264+
}
265+
restore := git.SetOps(&git.MockOps{
266+
CommonDirFn: func() (string, error) { return common, nil },
267+
ForWorktreeFn: func(string) (git.Ops, error) {
268+
resolutions++
269+
return scoped, nil
270+
},
271+
})
272+
defer restore()
273+
ctx := &Context{Origin: Location{Path: common}}
274+
for i := 1; i <= 2; i++ {
275+
ops, err := ctx.Prepare("feature")
276+
require.NoError(t, err)
277+
assert.Same(t, scoped, ops)
278+
assert.Equal(t, i, resolutions, "resolve once per preparation, not once per context")
279+
assert.Equal(t, "feature", current)
280+
}
281+
282+
scoped.HasUncommittedChangesFn = func() (bool, error) { return true, nil }
283+
ops, err := ctx.Prepare("other")
284+
require.ErrorContains(t, err, "uncommitted changes")
285+
assert.Nil(t, ops)
286+
assert.Equal(t, 3, resolutions)
287+
assert.Equal(t, "feature", current)
288+
}
289+
290+
func TestPreflightChecksSharedOwnerOncePerCall(t *testing.T) {
291+
common := t.TempDir()
292+
resolutions, checks := 0, 0
293+
scoped := &git.MockOps{
294+
CommonDirFn: func() (string, error) { return common, nil },
295+
GitDirFn: func() (string, error) { return common, nil },
296+
HasUncommittedChangesFn: func() (bool, error) {
297+
checks++
298+
return false, nil
299+
},
300+
}
301+
restore := git.SetOps(&git.MockOps{
302+
CommonDirFn: func() (string, error) { return common, nil },
303+
ForWorktreeFn: func(string) (git.Ops, error) {
304+
resolutions++
305+
return scoped, nil
306+
},
307+
})
308+
defer restore()
309+
ctx := &Context{Origin: Location{Path: common}}
310+
for i := 1; i <= 2; i++ {
311+
require.NoError(t, ctx.Preflight([]string{"one", "two"}))
312+
assert.Equal(t, 2*i, resolutions, "each branch still resolves its current owner")
313+
assert.Equal(t, i, checks, "cleanliness is shared only within this preflight")
314+
}
315+
}

0 commit comments

Comments
 (0)