Skip to content

fix(desktop): add timeouts to git/GitHub network operations#1346

Closed
Kitenite wants to merge 1 commit into
mainfrom
kitenite/existing-loading-forever
Closed

fix(desktop): add timeouts to git/GitHub network operations#1346
Kitenite wants to merge 1 commit into
mainfrom
kitenite/existing-loading-forever

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Feb 9, 2026

Summary

  • The desktop app hangs indefinitely ("loading forever") when the network is slow or unavailable because simple-git has no timeout support for network operations (fetch, push, pull, ls-remote, remote set-head)
  • Replaces all network-dependent simple-git calls with execFileAsync + explicit timeouts, and adds a default 30s timeout to execWithShellEnv (used by all gh CLI calls)
  • Timeout errors now surface user-friendly messages (e.g. "Push timed out. Check your network connection.") instead of cryptic "process was killed"

Changes

New file: git-timeouts.ts

  • Shared timeout constants: GIT_TIMEOUT_LOCAL (10s), GIT_TIMEOUT_NETWORK (30s), GIT_TIMEOUT_NETWORK_HEAVY (60s), GIT_TIMEOUT_LONG (120s)
  • isTimeoutError() / wrapTimeoutError() helpers for user-friendly error messages

shell-env.ts

  • execWithShellEnv now applies GIT_TIMEOUT_NETWORK (30s) as default timeout — fixes all gh CLI calls in github.ts which previously had zero timeout

git-operations.ts

  • Replaced simpleGit network calls in fetchCurrentBranch, pushWithSetUpstream, push, pull, sync, createPR with execFileAsync + timeout
  • fetchCurrentBranch and pushWithSetUpstream refactored to accept worktreePath: string instead of simpleGit instance

git.ts

  • Replaced simpleGit network calls in fetchDefaultBranch, refreshDefaultBranch, getDefaultBranch (ls-remote fallback), listBranches, checkBranchCheckoutSafety with execFileAsync + timeout

Magic number cleanup

  • All inline timeout values (10_000, 30_000, 120_000) across git.ts, github.ts, shell-env.ts replaced with named constants

Test Plan

  • Push/pull/sync with network available (happy path works as before)
  • Disconnect network → push/pull should error within 60s with "timed out" message
  • Create workspace offline → should fall back to local refs, not hang
  • gh CLI calls offline → should error within 30s
  • Typecheck passes (bun run typecheck)
  • Lint passes (bun run lint:fix)

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling and detection for Git operation timeouts across network operations.
    • Enhanced timeout management for fetch, push, pull, and sync operations with consistent, operation-specific timeouts.
  • Refactor

    • Centralized Git operation timeout configuration for improved consistency and maintainability.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

The changes introduce a centralized timeout management system for Git operations by creating a new utilities module with configurable timeout constants (local, network, and heavy operations). The main git-operations module is refactored to replace direct simpleGit usage with execFileAsync calls using worktreePath, incorporating the new timeout constants and error wrapping. Various utility modules are updated to use the centralized timeouts instead of hard-coded values.

Changes

Cohort / File(s) Summary
Timeout Infrastructure
apps/desktop/src/lib/trpc/routers/workspaces/utils/git-timeouts.ts
New module introducing four timeout constants (GIT_TIMEOUT_LOCAL, GIT_TIMEOUT_NETWORK, GIT_TIMEOUT_NETWORK_HEAVY, GIT_TIMEOUT_LONG) and utility functions isTimeoutError() and wrapTimeoutError() for timeout detection and error wrapping.
Git Operations Refactoring
apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
Replaced simpleGit direct usage with execFileAsync calls using worktreePath workflow. Updated fetchCurrentBranch and pushWithSetUpstream to accept worktreePath and use execFileAsync with appropriate timeouts. Integrated wrapTimeoutError for error handling in fetch, push, and pull operations. Added detached HEAD validation in pushWithSetUpstream.
Git Utilities Updates
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
Replaced hard-coded timeout literals with centralized timeout constants. Added wrapTimeoutError handling for fetch and remote operations. Introduced getGitEnv() usage for environment propagation. Updated several git invocations to use execFileAsync with explicit env and timeout parameters. Enhanced error handling for ENOENT and not-a-repo conditions.
GitHub Utilities
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
Replaced hard-coded 10000 ms timeout values with GIT_TIMEOUT_LOCAL constant in rev-parse HEAD and merge-base checks.
Shell Environment Utilities
apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts
Replaced hard-coded timeout in getShellEnvironment with GIT_TIMEOUT_LOCAL. Added defaultTimeout logic in execWithShellEnv applying GIT_TIMEOUT_NETWORK to both initial and retry execution paths, with enforced utf8 encoding.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A timeout's tale, now centralized and clear,
execFileAsync calls bring workflow dear,
Constants reign where magic milliseconds roamed,
Git operations find their tidy home! 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(desktop): add timeouts to git/GitHub network operations' directly summarizes the main change and is clear, specific, and concise.
Description check ✅ Passed The description comprehensively covers all required template sections: clear summary of the problem and solution, detailed breakdown of changes by file, and a test plan with specific verification steps.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kitenite/existing-loading-forever

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts (1)

36-40: ⚠️ Potential issue | 🟡 Minor

Missing timeout on local rev-parse call.

This execFileAsync call for rev-parse --abbrev-ref HEAD doesn't have a timeout, while the same command in findPRByHeadCommit (Line 149) and sharesAncestry (Line 238) uses GIT_TIMEOUT_LOCAL. Add the timeout for consistency.

Suggested fix
 		const { stdout: branchOutput } = await execFileAsync(
 			"git",
 			["rev-parse", "--abbrev-ref", "HEAD"],
-			{ cwd: worktreePath },
+			{ cwd: worktreePath, timeout: GIT_TIMEOUT_LOCAL },
 		);
apps/desktop/src/lib/trpc/routers/changes/git-operations.ts (1)

32-65: ⚠️ Potential issue | 🟠 Major

Missing env propagation in execFileAsync calls — inconsistent with git.ts.

All execFileAsync("git", ...) calls in this file omit the env option, while git.ts consistently passes { env: await getGitEnv(), timeout: ... } for the same types of operations. On macOS GUI apps (launched from Finder/Dock), process.env.PATH may not include /usr/local/bin or Homebrew paths until the shell-env PATH fix has been triggered.

This affects fetchCurrentBranch (Lines 36-47), pushWithSetUpstream (Line 86), and every execFileAsync call in the tRPC procedures (push, pull, sync, createPR).

Example fix for fetchCurrentBranch

You'd need to import getGitEnv (or getShellEnvironment) and pass env:

+import { getShellEnvironment } from "../workspaces/utils/shell-env";
+
+async function getGitEnv(): Promise<Record<string, string>> {
+	// Or import the one from git.ts if it can be shared
+	const shellEnv = await getShellEnvironment();
+	// ...
+}
+
 async function fetchCurrentBranch(worktreePath: string): Promise<void> {
 	const git = simpleGit(worktreePath);
+	const env = await getGitEnv();
 	const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
 	try {
 		await execFileAsync(
 			"git",
 			["-C", worktreePath, "fetch", "origin", branch],
-			{ timeout: GIT_TIMEOUT_NETWORK },
+			{ env, timeout: GIT_TIMEOUT_NETWORK },
 		);

To verify whether getGitEnv is exported from git.ts or could be shared:

#!/bin/bash
# Check if getGitEnv is exported or only used internally in git.ts
rg -n 'getGitEnv' --type=ts -C2
🤖 Fix all issues with AI agents
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts`:
- Around line 337-341: The timeout for the GitHub API call using
execFileAsync("gh", ["api", "user", "--jq", ".login"], { env, timeout:
GIT_TIMEOUT_LOCAL }) is too short; change the options to use GIT_TIMEOUT_NETWORK
instead of GIT_TIMEOUT_LOCAL so the execFileAsync call (the "gh api user"
invocation) uses the 30s network timeout constant; ensure GIT_TIMEOUT_NETWORK is
in scope where execFileAsync is used.
🧹 Nitpick comments (2)
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts (1)

73-86: git status uses GIT_TIMEOUT_NETWORK but is a local operation.

git status --porcelain does not hit the network. The GIT_TIMEOUT_NETWORK constant is documented as "30s — fetch single branch, ls-remote, gh API calls." While 30s may be a reasonable budget for large repos with expensive working-tree scans, GIT_TIMEOUT_LOCAL is the semantically correct tier. If you want a longer local budget for status specifically, consider a dedicated constant or a comment explaining the choice.

apps/desktop/src/lib/trpc/routers/changes/git-operations.ts (1)

208-247: sync procedure: push error after pull success throws "Push" — consider "Sync (push)" for disambiguation.

When sync fails during the push phase (Line 243), the error says "Push timed out." The user initiated a "Sync" operation, so they might find it confusing. Minor UX consideration — could use "Sync" consistently or "Sync (push phase)" for clarity.

Comment on lines 337 to 341
const { stdout } = await execFileAsync(
"gh",
["api", "user", "--jq", ".login"],
{ env, timeout: 10_000 },
{ env, timeout: GIT_TIMEOUT_LOCAL },
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

gh api user is a network call but uses GIT_TIMEOUT_LOCAL (10s).

gh api user --jq .login hits the GitHub API over the network. Using GIT_TIMEOUT_LOCAL (10s) risks premature timeouts on slow connections. This should use GIT_TIMEOUT_NETWORK (30s), which is documented as the timeout for "gh API calls."

Suggested fix
 		const { stdout } = await execFileAsync(
 			"gh",
 			["api", "user", "--jq", ".login"],
-			{ env, timeout: GIT_TIMEOUT_LOCAL },
+			{ env, timeout: GIT_TIMEOUT_NETWORK },
 		);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { stdout } = await execFileAsync(
"gh",
["api", "user", "--jq", ".login"],
{ env, timeout: 10_000 },
{ env, timeout: GIT_TIMEOUT_LOCAL },
);
const { stdout } = await execFileAsync(
"gh",
["api", "user", "--jq", ".login"],
{ env, timeout: GIT_TIMEOUT_NETWORK },
);
🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts` around lines 337 -
341, The timeout for the GitHub API call using execFileAsync("gh", ["api",
"user", "--jq", ".login"], { env, timeout: GIT_TIMEOUT_LOCAL }) is too short;
change the options to use GIT_TIMEOUT_NETWORK instead of GIT_TIMEOUT_LOCAL so
the execFileAsync call (the "gh api user" invocation) uses the 30s network
timeout constant; ensure GIT_TIMEOUT_NETWORK is in scope where execFileAsync is
used.

@Kitenite
Copy link
Copy Markdown
Collaborator Author

Kitenite commented May 6, 2026

Closing as stale: created in Jan-Mar with no recent activity. If still relevant, re-open or re-create from a fresh branch. Bulk audit 2026-05-06.

@Kitenite Kitenite closed this May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant