Skip to content

Commit 25d2c42

Browse files
authored
[fix] Thread context through the git package (#283) (#299)
* [fix] Thread context through git package; drop non-cancellable Run/RunInDir Thread ctx context.Context (first param) through all git package functions so SIGINT and MCP per-request deadlines cancel in-flight git subprocesses. Remove Run/RunInDir from the Runner interface, ExecRunner, and the debug decorator — the context.Background() shim can no longer be reintroduced. Recovery/rollback paths (AbortRebase, MergeAbort, StashApply, StashDrop, MoveWorktree) deliberately stay non-cancellable, each with a doc comment explaining why: completing them is required to avoid SIGKILL-mid-restore data loss or stranded worktrees. Closes #283 * [chore] Rename RunContext→Run, RunInDirContext→RunInDir; fix review issues Since the context.Background() shims are gone, the Context suffix is no longer needed to distinguish variants. Rename to the cleaner form. Also fix code review findings: - Remove dead Run/RunInDir shim methods from ctxMockRunner in sync_ctx_test.go - Add non-cancellable doc comment to stashRefBySHA (called only from StashDrop) * [chore] Address review feedback: drop misleading ctx params, rename stale test fields - Remove ctx from restoreStash/applyStashToWorktree (non-cancellable recovery paths should not accept ctx at all — matches StashApply/StashDrop/MoveWorktree pattern) - Rename stubRunner fields runContextCalled→runCalled, runInDirCtxCalled→runInDirCalled in debug_test.go to match the renamed Run/RunInDir methods * [chore] Put ctx first in runInitFresh signature (Go convention)
1 parent 0aca609 commit 25d2c42

94 files changed

Lines changed: 616 additions & 701 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ main repo and is not the default branch. --source is not valid in branch: mode.`
5656

5757
r := newRunner()
5858

59-
repoRoot, err := git.MainRepoRoot(r)
59+
repoRoot, err := git.MainRepoRoot(cmd.Context(), r)
6060
if err != nil {
6161
return err
6262
}

cmd/archive.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ var archiveCmd = &cobra.Command{
3030
task := args[0]
3131
r := newRunner()
3232

33-
wt, err := findWorktree(r, task)
33+
wt, err := findWorktree(cmd.Context(), r, task)
3434
if err != nil {
3535
return err
3636
}
@@ -47,7 +47,7 @@ var archiveCmd = &cobra.Command{
4747
defer s.Stop()
4848

4949
s.Start("Archiving worktree...")
50-
result, err := operations.ArchiveWorktree(r, operations.ArchiveParams{
50+
result, err := operations.ArchiveWorktree(cmd.Context(), r, operations.ArchiveParams{
5151
Path: wt.Path,
5252
Branch: wt.Branch,
5353
Force: force,

cmd/clean.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func cleanPrune(ctx context.Context, cmd *cobra.Command, r git.Runner) error {
7474
defer s.Stop()
7575

7676
s.Start("Pruning stale references...")
77-
out, err := git.Prune(r, dryRun)
77+
out, err := git.Prune(ctx, r, dryRun)
7878
if err != nil {
7979
return err
8080
}
@@ -96,7 +96,7 @@ func cleanPrune(ctx context.Context, cmd *cobra.Command, r git.Runner) error {
9696
// Skips gracefully when there are no remotes; warns and continues on per-remote failure.
9797
func cleanRemotePrune(ctx context.Context, cmd *cobra.Command, r git.Runner, s *spinner.Spinner, dryRun bool) error {
9898
s.Start("Pruning remote-tracking refs...")
99-
remotes, err := git.ListRemotes(r)
99+
remotes, err := git.ListRemotes(ctx, r)
100100
if err != nil {
101101
s.Stop()
102102
return err
@@ -133,8 +133,8 @@ type hintEntry struct {
133133
}
134134

135135
// cleanResolveAndHint resolves the main branch and shows relevant flag hints.
136-
func cleanResolveAndHint(cmd *cobra.Command, r git.Runner, entries []hintEntry) (string, error) {
137-
mainBranch, err := resolveMainBranch(r)
136+
func cleanResolveAndHint(ctx context.Context, cmd *cobra.Command, r git.Runner, entries []hintEntry) (string, error) {
137+
mainBranch, err := resolveMainBranch(ctx, r)
138138
if err != nil {
139139
return "", err
140140
}
@@ -207,15 +207,15 @@ func runClean(ctx context.Context, cmd *cobra.Command, r git.Runner, s cleanStra
207207
}
208208

209209
func cleanMerged(ctx context.Context, cmd *cobra.Command, r git.Runner) error {
210-
mainBranch, err := cleanResolveAndHint(cmd, r, []hintEntry{
210+
mainBranch, err := cleanResolveAndHint(ctx, cmd, r, []hintEntry{
211211
{flagDryRun, hintDryRunClean},
212212
{flagForce, hintForce},
213213
})
214214
if err != nil {
215215
return err
216216
}
217217

218-
remotePresent := git.RemoteExists(r, git.DefaultRemote)
218+
remotePresent := git.RemoteExists(ctx, r, git.DefaultRemote)
219219
var mergeRef string
220220
return runClean(ctx, cmd, r, cleanStrategy{
221221
label: "merged",
@@ -229,7 +229,7 @@ func cleanMerged(ctx context.Context, cmd *cobra.Command, r git.Runner) error {
229229
return err
230230
},
231231
find: func(rr git.Runner) ([]operations.CleanCandidate, []string, error) {
232-
result, err := operations.FindMergedCandidates(rr, mergeRef, mainBranch)
232+
result, err := operations.FindMergedCandidates(ctx, rr, mergeRef, mainBranch)
233233
if err != nil {
234234
return nil, nil, err
235235
}
@@ -242,7 +242,7 @@ func cleanMerged(ctx context.Context, cmd *cobra.Command, r git.Runner) error {
242242
}
243243

244244
func cleanStale(ctx context.Context, cmd *cobra.Command, r git.Runner) error {
245-
mainBranch, err := cleanResolveAndHint(cmd, r, []hintEntry{
245+
mainBranch, err := cleanResolveAndHint(ctx, cmd, r, []hintEntry{
246246
{flagDryRun, hintDryRunClean},
247247
{flagForce, hintForce},
248248
{flagStaleDays, "Customize the staleness threshold (default: 14 days)"},
@@ -260,7 +260,7 @@ func cleanStale(ctx context.Context, cmd *cobra.Command, r git.Runner) error {
260260
summaryFmt: "Cleaned %d stale worktree(s).\n",
261261
originPresent: false, // stale mode is local-only
262262
find: func(rr git.Runner) ([]operations.CleanCandidate, []string, error) {
263-
result, err := operations.FindStaleCandidates(rr, mainBranch, staleDays)
263+
result, err := operations.FindStaleCandidates(ctx, rr, mainBranch, staleDays)
264264
if err != nil {
265265
return nil, nil, err
266266
}

cmd/completions.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"context"
45
"fmt"
56
"sort"
67
"strings"
@@ -47,7 +48,7 @@ func init() {
4748
// completeWorktreeTasks returns task names for shell completion.
4849
func completeWorktreeTasks(_ *cobra.Command, toComplete string) []string {
4950
r := newRunner()
50-
entries, err := git.ListWorktrees(r)
51+
entries, err := git.ListWorktrees(context.Background(), r)
5152
if err != nil {
5253
return nil
5354
}
@@ -86,10 +87,11 @@ func completeOpenShortcuts(cmd *cobra.Command, toComplete string) []string {
8687
// completeArchivedTasks returns task names from archived branches (branches not in any active worktree).
8788
func completeArchivedTasks(_ *cobra.Command, toComplete string) []string {
8889
r := newRunner()
90+
ctx := context.Background()
8991

90-
mainBranch, _ := resolveMainBranch(r)
92+
mainBranch, _ := resolveMainBranch(ctx, r)
9193

92-
archived, err := operations.ListArchivedBranches(r, mainBranch)
94+
archived, err := operations.ListArchivedBranches(ctx, r, mainBranch)
9395
if err != nil {
9496
return nil
9597
}
@@ -108,7 +110,7 @@ func completeArchivedTasks(_ *cobra.Command, toComplete string) []string {
108110
// completeBranchNames returns branch names for shell completion.
109111
func completeBranchNames(_ *cobra.Command, toComplete string) []string {
110112
r := newRunner()
111-
out, err := r.Run("branch", "--format=%(refname:short)")
113+
out, err := r.Run(context.Background(), "branch", "--format=%(refname:short)")
112114
if err != nil {
113115
return nil
114116
}

cmd/conflict_check.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ var conflictCheckCmd = &cobra.Command{
3939

4040
r := newRunner()
4141

42-
worktrees, err := listWorktreeInfos(r)
42+
worktrees, err := listWorktreeInfos(cmd.Context(), r)
4343
if err != nil {
4444
return err
4545
}
@@ -68,7 +68,7 @@ var conflictCheckCmd = &cobra.Command{
6868
defer s.Stop()
6969
s.Start("Collecting file changes...")
7070

71-
diffs, err := conflict.CollectDiffs(r, cfg.DefaultSource, eligible)
71+
diffs, err := conflict.CollectDiffs(cmd.Context(), r, cfg.DefaultSource, eligible)
7272
if err != nil {
7373
return err
7474
}
@@ -79,7 +79,7 @@ var conflictCheckCmd = &cobra.Command{
7979
var dryResults []conflict.DryMergeResult
8080
if dryMerge {
8181
s.Update("Running dry merges...")
82-
dryResults, err = conflict.DryMergeAll(r, eligible)
82+
dryResults, err = conflict.DryMergeAll(cmd.Context(), r, eligible)
8383
if err != nil {
8484
return err
8585
}

cmd/deps.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ var depsStatusCmd = &cobra.Command{
3535
cfg := config.FromContext(cmd.Context())
3636

3737
r := newRunner()
38-
worktrees, err := listWorktreeInfos(r)
38+
worktrees, err := listWorktreeInfos(cmd.Context(), r)
3939
if err != nil {
4040
return err
4141
}
@@ -141,12 +141,12 @@ var depsInstallCmd = &cobra.Command{
141141

142142
r := newRunner()
143143

144-
repoRoot, err := git.MainRepoRoot(r)
144+
repoRoot, err := git.MainRepoRoot(cmd.Context(), r)
145145
if err != nil {
146146
return err
147147
}
148148

149-
worktrees, err := listWorktreeInfos(r)
149+
worktrees, err := listWorktreeInfos(cmd.Context(), r)
150150
if err != nil {
151151
return err
152152
}

cmd/duplicate.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@ var duplicateCmd = &cobra.Command{
4141
cfg := config.FromContext(cmd.Context())
4242

4343
r := newRunner()
44+
ctx := cmd.Context()
4445

45-
repoRoot, err := git.MainRepoRoot(r)
46+
repoRoot, err := git.MainRepoRoot(ctx, r)
4647
if err != nil {
4748
return err
4849
}
4950

50-
wt, err := findWorktree(r, task)
51+
wt, err := findWorktree(ctx, r, task)
5152
if err != nil {
5253
return err
5354
}
@@ -75,7 +76,7 @@ var duplicateCmd = &cobra.Command{
7576
for i := 1; i <= maxDuplicateSuffix; i++ {
7677
candidate := fmt.Sprintf("%s-%d", task, i)
7778
candidateBranch := resolver.FullBranchName(svc, matchedPrefix, candidate)
78-
if !git.BranchExists(r, candidateBranch) {
79+
if !git.BranchExists(ctx, r, candidateBranch) {
7980
newTask = candidate
8081
break
8182
}
@@ -90,7 +91,7 @@ var duplicateCmd = &cobra.Command{
9091
wtPath := resolver.WorktreePath(wtDir, newBranch)
9192

9293
// Validate
93-
if git.BranchExists(r, newBranch) {
94+
if git.BranchExists(ctx, r, newBranch) {
9495
return fmt.Errorf("branch %q already exists", newBranch)
9596
}
9697
if _, err := os.Stat(wtPath); err == nil {
@@ -132,7 +133,7 @@ var duplicateCmd = &cobra.Command{
132133

133134
// Create worktree from source branch
134135
s.Start("Creating worktree...")
135-
if err := git.AddWorktree(r, wtPath, newBranch, wt.Branch); err != nil {
136+
if err := git.AddWorktree(ctx, r, wtPath, newBranch, wt.Branch); err != nil {
136137
return err
137138
}
138139

cmd/exec.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"io"
@@ -146,8 +147,9 @@ func execShowHints(cmd *cobra.Command) {
146147
}
147148

148149
func execSelectWorktrees(cmd *cobra.Command, r git.Runner, s *spinner.Spinner, opts execOpts, prefixes []string) ([]resolver.WorktreeInfo, error) {
149-
cfg := config.FromContext(cmd.Context())
150-
worktrees, err := listWorktreeInfos(r)
150+
ctx := cmd.Context()
151+
cfg := config.FromContext(ctx)
152+
worktrees, err := listWorktreeInfos(ctx, r)
151153
if err != nil {
152154
return nil, err
153155
}
@@ -161,7 +163,7 @@ func execSelectWorktrees(cmd *cobra.Command, r git.Runner, s *spinner.Spinner, o
161163
}
162164

163165
if opts.dirty {
164-
filtered = filterDirtyWorktrees(cmd, r, s, filtered)
166+
filtered = filterDirtyWorktrees(ctx, cmd, r, s, filtered)
165167
}
166168
return filtered, nil
167169
}
@@ -233,13 +235,13 @@ func init() {
233235
// filterDirtyWorktrees filters worktrees to only those with uncommitted changes.
234236
// If IsDirty returns an error for a worktree, it is treated as dirty (included)
235237
// and a warning is emitted to cmd.ErrOrStderr() so the error is visible.
236-
func filterDirtyWorktrees(cmd *cobra.Command, r git.Runner, s *spinner.Spinner, worktrees []resolver.WorktreeInfo) []resolver.WorktreeInfo {
238+
func filterDirtyWorktrees(ctx context.Context, cmd *cobra.Command, r git.Runner, s *spinner.Spinner, worktrees []resolver.WorktreeInfo) []resolver.WorktreeInfo {
237239
n := len(worktrees)
238240
var done atomic.Int32
239241

240242
results := parallel.Collect[dirtyResult](n, 8, func(i int) dirtyResult {
241243
path := worktrees[i].Path
242-
dirty, err := git.IsDirty(r, path)
244+
dirty, err := git.IsDirty(ctx, r, path)
243245
count := done.Add(1)
244246
s.Update(fmt.Sprintf("Checking dirty status... (%d/%d)", count, n))
245247
if err != nil {

cmd/exec_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ func TestFilterDirtyWorktrees(t *testing.T) {
234234
s := testExecSpinner(cmd)
235235
defer s.Stop()
236236

237-
result := filterDirtyWorktrees(cmd, r, s, worktrees)
237+
result := filterDirtyWorktrees(context.Background(), cmd, r, s, worktrees)
238238
if len(result) != 1 {
239239
t.Fatalf("expected 1 dirty worktree, got %d", len(result))
240240
}
@@ -262,7 +262,7 @@ func TestFilterDirtyWorktreesIsDirtyErrorIncludedAndWarned(t *testing.T) {
262262
s := testExecSpinner(cmd)
263263
defer s.Stop()
264264

265-
result := filterDirtyWorktrees(cmd, r, s, worktrees)
265+
result := filterDirtyWorktrees(context.Background(), cmd, r, s, worktrees)
266266

267267
if len(result) != 1 {
268268
t.Fatalf("expected erroring worktree to be included (treated as dirty), got %d", len(result))

cmd/helpers.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"context"
45
"io"
56

67
"github.com/lugassawan/rimba/internal/config"
@@ -44,8 +45,8 @@ func spinnerOpts(cmd *cobra.Command) spinner.Options {
4445
}
4546

4647
// resolveMainBranch tries to get the main branch from config, falling back to DefaultBranch.
47-
func resolveMainBranch(r git.Runner) (string, error) {
48-
repoRoot, err := git.MainRepoRoot(r)
48+
func resolveMainBranch(ctx context.Context, r git.Runner) (string, error) {
49+
repoRoot, err := git.MainRepoRoot(ctx, r)
4950
if err != nil {
5051
return "", err
5152
}
@@ -55,21 +56,21 @@ func resolveMainBranch(r git.Runner) (string, error) {
5556
configDefault = cfg.DefaultSource
5657
}
5758

58-
return operations.ResolveMainBranch(r, configDefault)
59+
return operations.ResolveMainBranch(ctx, r, configDefault)
5960
}
6061

6162
// listWorktreeInfos converts git worktree entries to resolver-compatible WorktreeInfo slice.
62-
func listWorktreeInfos(r git.Runner) ([]resolver.WorktreeInfo, error) {
63-
return operations.ListWorktreeInfos(r)
63+
func listWorktreeInfos(ctx context.Context, r git.Runner) ([]resolver.WorktreeInfo, error) {
64+
return operations.ListWorktreeInfos(ctx, r)
6465
}
6566

6667
// findWorktree looks up a worktree by user input (task or service/task).
6768
// It resolves the input to detect monorepo service names.
68-
func findWorktree(r git.Runner, input string) (resolver.WorktreeInfo, error) {
69-
repoRoot, err := git.MainRepoRoot(r)
69+
func findWorktree(ctx context.Context, r git.Runner, input string) (resolver.WorktreeInfo, error) {
70+
repoRoot, err := git.MainRepoRoot(ctx, r)
7071
if err != nil {
71-
return operations.FindWorktree(r, "", input)
72+
return operations.FindWorktree(ctx, r, "", input)
7273
}
7374
service, task := operations.ResolveTaskInput(input, repoRoot)
74-
return operations.FindWorktree(r, service, task)
75+
return operations.FindWorktree(ctx, r, service, task)
7576
}

0 commit comments

Comments
 (0)