From 4b5273f4336d64c0d0b98bc902e7792399576483 Mon Sep 17 00:00:00 2001 From: Ali-Al1 Date: Mon, 9 Nov 2026 07:30:10 +0000 Subject: [PATCH] fix(server): treat git 2.56 broken-gitfile stderr as non-repository Git 2.56 reports a worktree whose `.git` gitfile points at a missing directory as "gitfile does not point to a valid repository" instead of "not a git repository". isNonRepositoryGitStderr only matched the old phrase, and StorageCleanup.branchChanged had its own copy of the same check, so on GitHub-hosted runners (git 2.56) storage cleanup reported such worktrees as `failed` rather than keeping them as changed. Export isNonRepositoryGitStderr, match both phrases, and reuse it in storageCleanup so the two checks cannot drift again. Co-Authored-By: Claude Code --- apps/server/src/storageCleanup.ts | 3 ++- apps/server/src/vcs/GitVcsDriverCore.test.ts | 22 ++++++++++++++++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 12 +++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index a5f9cd7eefa12..13fb7288ee057 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -47,6 +47,7 @@ import * as Settings from "./serverSettings.ts"; import * as TerminalManager from "./terminal/Manager.ts"; import { isFilesystemRoot, managedWorktreesDirectories } from "./worktreesDirectory.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; +import { isNonRepositoryGitStderr } from "./vcs/GitVcsDriverCore.ts"; import { removeLinkedWorktree } from "./vcs/worktreeRemoval.ts"; import { withWorkspaceLease } from "./workspace/workspaceLease.ts"; import * as HostProcess from "@t3tools/shared/HostProcess"; @@ -618,7 +619,7 @@ const make = Effect.gen(function* () { ); if (result === null) return null; if (result.exitCode === 0 || result.exitCode === allowedExitCode) return result; - if (result.stderr.toLowerCase().includes("not a git repository")) return null; + if (isNonRepositoryGitStderr(result.stderr)) return null; return yield* failure( result.stderr.trim().split(/\r?\n/)[0]?.slice(0, 1000) || "command failed", ); diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index baf1368054d4e..9dd6ed197b920 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -37,6 +37,7 @@ import { import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import * as ServerConfig from "../config.ts"; import { + isNonRepositoryGitStderr, makeGitVcsDriverCore, parseGitCheckoutProgressLine, splitNullSeparatedGitStdoutPaths, @@ -130,6 +131,27 @@ const makeDriverReleasingLock = ( return { driver, failures }; }); +describe("isNonRepositoryGitStderr", () => { + it.each([ + "fatal: not a git repository (or any of the parent directories): .git", + "fatal: not a git repository: /missing-git-directory", + // Git 2.56+ wording for a `.git` gitfile that points at a missing directory. + "fatal: gitfile does not point to a valid repository: /tmp/worktrees/feature/.git", + "FATAL: GITFILE DOES NOT POINT TO A VALID REPOSITORY: /tmp/x/.git", + ])("classifies %j as a non-repository", (stderr) => { + assert.isTrue(isNonRepositoryGitStderr(stderr)); + }); + + it.each([ + "", + "fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.", + "fatal: Unable to create '/repo/.git/index.lock': File exists.", + "fatal: invalid gitfile format: /repo/.git", + ])("does not classify %j as a non-repository", (stderr) => { + assert.isFalse(isNonRepositoryGitStderr(stderr)); + }); +}); + it.effect("runs background Git in its own lane so user commands never queue behind it", () => Effect.gen(function* () { const gate = yield* Deferred.make(); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 4fcf1b05fe6f5..c0d6f9698a233 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -562,8 +562,16 @@ function isMissingGitCwdError(error: GitCommandError): boolean { ); } -function isNonRepositoryGitStderr(stderr: string): boolean { - return stderr.toLowerCase().includes("not a git repository"); +// Git 2.56+ reports a `.git` gitfile pointing nowhere as "gitfile does not point to a valid +// repository"; older releases said "not a git repository". Both mean the cwd is not a repository. +const NON_REPOSITORY_STDERR_PHRASES = [ + "not a git repository", + "gitfile does not point to a valid repository", +] as const; + +export function isNonRepositoryGitStderr(stderr: string): boolean { + const normalized = stderr.toLowerCase(); + return NON_REPOSITORY_STDERR_PHRASES.some((phrase) => normalized.includes(phrase)); } function isUnbornHeadStderr(stderr: string): boolean { const normalized = stderr.toLowerCase();