Skip to content

bug(scm/project): git subprocesses use exec.Command with no context — a hung git blocks the observer poll loop and ignores shutdown #304

@areycruzer

Description

@areycruzer

TL;DR

Several git subprocesses are spawned with exec.Command (no context.Context), so they have no timeout and ignore cancellation. The most serious one runs inside the SCM observer's poll loop: if that git hangs (network filesystem, credential prompt, .git lock, slow disk), it blocks the entire observer tick indefinitely and can't be interrupted by daemon shutdown.

This contradicts the repo convention (AGENTS.md): "Use context.Context as the first argument for functions that do I/O or blocking work." Every other git call in the worktree adapter already uses exec.CommandContext.


Affected call sites

File:line Command Context available at caller?
internal/observe/scm/observer.go:1202 git remote get-url origin Yes — called from discoverSubjects(ctx) (observer.go:447), inside the poll loop
internal/service/project/service.go:242 git remote get-url origin Add request handler (request ctx available)
internal/service/project/service.go:255 git symbolic-ref --short HEAD Add request handler
internal/service/project/service.go:347 git rev-parse --show-toplevel (isGitRepo) Add request handler

Contrast: internal/adapters/workspace/gitworktree/commands.go + workspace.go:530 already thread ctx through exec.CommandContext for every git call.

Why the observer one is the worst

resolveGitOriginURL is invoked per-project inside discoverSubjects, which runs on every SCM observer tick (DefaultTickInterval):

// observer.go:447
if url := resolveGitOriginURL(p.Path); url != "" { ... }

// observer.go:1201
func resolveGitOriginURL(path string) string {
    out, err := exec.Command("git", "-C", path, "remote", "get-url", "origin").Output()
    ...
}

If git blocks on p.Path (a stale NFS/SMB mount, a credential helper prompting on a misconfigured remote, an index.lock left by a crashed git, or just a slow disk), the call never returns. That:

  • Stalls the whole observer tick — no PR/CI facts get refreshed for any project while it's stuck.
  • Ignores shutdown — daemon Stop() cancels the observer's ctx, but the orphaned git process keeps the goroutine parked, so graceful shutdown waits out its ShutdownTimeout and then leaks the child process.

The three project/service.go calls are lower-risk (they run on an interactive project add, not a loop) but share the same defect: a hung git wedges the request past the 60s request timeout with no way to cancel the child.

Impact

  • SCM observer can deadlock indefinitely on a single bad repo path, silently freezing live PR/CI updates for all projects.
  • Orphaned git child processes survive daemon shutdown.
  • project add can hang a request thread on a pathological repo.

Suggested fix (small + surgical)

  1. Change all four to exec.CommandContext(ctx, "git", ...).
  2. Thread ctx into resolveGitOriginURL(ctx, path)discoverSubjects already has it.
  3. For project/service.go, pass the request ctx into resolveGitOriginURL / resolveDefaultBranch / isGitRepo (callers in Add already hold it).
  4. Optionally wrap with a short context.WithTimeout (a few seconds) so even an uncancelled parent ctx can't park on a wedged git.

No behavior change on the happy path; just bounds the unhappy one.

Acceptance criteria

  • All four git calls use exec.CommandContext with a real (cancellable) context.
  • Cancelling the observer/request context terminates the in-flight git child.
  • Test: a fake/slow git that blocks past a deadline causes the call to return promptly with an error/empty result rather than hanging.

Filed after reviewing all open/closed issues and PRs — not tracked elsewhere (closest is #97, which is about isGitRepo case-sensitivity/testability, not cancellation).

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcoreCore Functionalitypriority: mediumFix when convenientscmSCM observer/notifier lane

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions