Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,57 @@ it.effect("backs off failed upstream refreshes across linked worktrees", () =>
).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),
);

it.effect("cools down a failed remote default branch probe across mutations", () =>
Effect.scoped(
Effect.gen(function* () {
const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
const probeAttempts = yield* Ref.make(0);
const failingProbeSpawner = ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
if (!ChildProcess.isStandardCommand(command)) {
return yield* Effect.die("expected a standard Git command");
}
if (!command.args.includes("ls-remote")) {
return yield* delegate.spawn(command);
}
yield* Ref.update(probeAttempts, (count) => count + 1);
// A probe that cannot spawn fails its cache entry instead of resolving to
// null, the same channel a timed-out probe fails through.
return yield* PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
});
}),
);
const driver = yield* makeGitVcsDriverCore().pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, failingProbeSpawner),
);

yield* Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const remote = yield* makeTmpDir("git-vcs-driver-remote-");
const { initialBranch } = yield* initRepoWithCommit(cwd);
yield* git(remote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "origin", remote]);
yield* git(cwd, ["push", "-u", "origin", initialBranch]);
yield* Ref.set(probeAttempts, 0);

yield* driver.statusDetailsRemote(cwd);
// Ordinary mutations cannot move the remote's HEAD, so they must not drop the
// entry and send the next status read back through the failing probe.
yield* driver.fetchRemote({ cwd, remoteName: "origin" });
yield* driver.statusDetailsRemote(cwd);
assert.equal(yield* Ref.get(probeAttempts), 1);

yield* TestClock.adjust("6 minutes");
yield* driver.statusDetailsRemote(cwd);
assert.equal(yield* Ref.get(probeAttempts), 2);
}).pipe(Effect.provideService(GitVcsDriver.GitVcsDriver, driver));
}),
).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))),
);

it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
describe("process environment", () => {
it.effect("preserves the caller locale for general Git subprocesses", () =>
Expand Down Expand Up @@ -1101,6 +1152,55 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
}),
);

it.effect("prefers the remote default branch over a stale local origin HEAD", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const remote = yield* makeTmpDir("git-vcs-driver-remote-");
yield* initRepoWithCommit(cwd);
yield* git(remote, ["init", "--bare"]);
yield* git(cwd, ["branch", "-M", "main"]);
yield* git(cwd, ["remote", "add", "origin", remote]);
yield* git(cwd, ["push", "origin", "main"]);
yield* git(cwd, ["checkout", "-b", "dev"]);
yield* git(cwd, ["push", "-u", "origin", "dev"]);
// The project switched its default branch to `dev` after this clone was
// made. Git never rewrites `refs/remotes/origin/HEAD` on fetch or pull, so
// the local snapshot is left pointing at the branch that used to be default.
yield* git(remote, ["symbolic-ref", "HEAD", "refs/heads/dev"]);
yield* git(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"]);

const status = yield* (yield* GitVcsDriver.GitVcsDriver).statusDetailsRemote(cwd);

assert.equal(status.branch, "dev");
assert.equal(status.isDefaultBranch, true);
}),
);

it.effect("falls back to the local origin HEAD when the remote is unreachable", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const remote = yield* makeTmpDir("git-vcs-driver-remote-");
const pathService = yield* Path.Path;
yield* initRepoWithCommit(cwd);
yield* git(remote, ["init", "--bare"]);
yield* git(cwd, ["branch", "-M", "main"]);
yield* git(cwd, ["remote", "add", "origin", remote]);
yield* git(cwd, ["push", "origin", "main"]);
yield* git(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"]);
yield* git(cwd, [
"remote",
"set-url",
"origin",
pathService.join(remote, "missing-remote.git"),
]);

const status = yield* (yield* GitVcsDriver.GitVcsDriver).statusDetailsRemote(cwd);

assert.equal(status.branch, "main");
assert.equal(status.isDefaultBranch, true);
}),
);

it.effect("makes background upstream status fetches non-interactive", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand Down
100 changes: 87 additions & 13 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ const LIST_REFS_SNAPSHOT_CACHE_TTL = Duration.minutes(2);
const LIST_REFS_REFRESH_COALESCE_TTL = Duration.seconds(5);
const LIST_REFS_REFRESH_FAILURE_COOLDOWN = Duration.seconds(30);
const STATUS_DEFAULT_BRANCH_CACHE_TTL = Duration.minutes(5);
// A project's default branch changes far more rarely than the local snapshot it
// corrects, so a successful probe is cached well past that snapshot.
const STATUS_REMOTE_DEFAULT_BRANCH_CACHE_TTL = Duration.minutes(30);
const STATUS_ORIGIN_EXISTS_CACHE_TTL = Duration.minutes(5);
const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({
GCM_INTERACTIVE: "never",
Expand Down Expand Up @@ -116,6 +119,11 @@ function statusUpstreamRefreshFailureCooldown(consecutiveFailures: number): Dura
return Duration.min(Duration.millis(cooldownMs), STATUS_UPSTREAM_REFRESH_FAILURE_MAX_COOLDOWN);
}

class RemoteDefaultBranchCacheKey extends Data.Class<{
gitCommonDir: string;
remoteName: string;
}> {}

class GitRefsSnapshotCacheKey extends Data.Class<{
gitCommonDir: string;
epoch: number;
Expand Down Expand Up @@ -397,6 +405,14 @@ function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string):
return refName.length > 0 ? refName : null;
}

// `git ls-remote --symref <remote> HEAD` answers with the remote's own HEAD symref
// followed by the resolved object, for example:
// ref: refs/heads/dev\tHEAD
// 1a2b3cd\tHEAD
function parseDefaultBranchFromLsRemoteSymref(value: string): string | null {
return /^ref:\s+refs\/heads\/(\S+)\s+HEAD\s*$/m.exec(value)?.[1] ?? null;
}

function isMissingGitCwdError(error: GitCommandError): boolean {
if (!(error.cause instanceof PlatformError.PlatformError)) {
return false;
Expand Down Expand Up @@ -1129,10 +1145,60 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
return Cache.get(refresh ? repositoryPathsRefreshCache : repositoryPathsCache, cacheKey);
};

// `refs/remotes/<remote>/HEAD` is written once, when the repository is cloned, and
// neither fetch nor pull ever moves it again. A project that changes its default
// branch afterwards leaves every existing clone pointing at the old one, so the real
// default branch gets classified as an ordinary topic branch. Ask the remote itself,
// and keep the local snapshot as the offline fallback. No mutation this driver
// performs can move the remote's own HEAD, so the entry is left to expire on its own
// rather than being dropped by `invalidateStatusStaticCaches` after every commit.
const remoteDefaultBranchCache = yield* Cache.makeWith(
({ gitCommonDir, remoteName }: RemoteDefaultBranchCacheKey) =>
executeGit(
"GitVcsDriver.statusDetails.remoteDefaultBranch",
path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir,
["--git-dir", gitCommonDir, "ls-remote", "--symref", remoteName, "HEAD"],
{
allowNonZeroExit: true,
env: STATUS_UPSTREAM_REFRESH_ENV,
timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT),
},
).pipe(
Effect.map((result) =>
result.exitCode === 0 ? parseDefaultBranchFromLsRemoteSymref(result.stdout) : null,
),
),
{
capacity: 2_048,
timeToLive: Exit.match({
// An unreachable remote resolves to null. Retry that on the shorter local
// cadence instead of pinning the fallback in place for the full window.
onSuccess: (defaultBranch) =>
defaultBranch === null
? STATUS_DEFAULT_BRANCH_CACHE_TTL
: STATUS_REMOTE_DEFAULT_BRANCH_CACHE_TTL,
// A timed-out or unspawnable probe carries the same answer as an unreachable
// remote — none — and costs the full timeout to learn it, so it cools down on
// that same cadence instead of re-probing on every miss.
onFailure: () => STATUS_DEFAULT_BRANCH_CACHE_TTL,
}),
Comment thread
cursor[bot] marked this conversation as resolved.
},
);

const remoteDefaultBranchFor = (gitCommonDir: string, remoteName: string) =>
Cache.get(
remoteDefaultBranchCache,
new RemoteDefaultBranchCacheKey({ gitCommonDir, remoteName }),
).pipe(Effect.orElseSucceed(() => null));

const defaultBranchCache = yield* Cache.makeWith(
(gitCommonDir: string) =>
Effect.gen(function* () {
const path = yield* Path.Path;
const remoteDefaultBranch = yield* remoteDefaultBranchFor(gitCommonDir, "origin");
if (remoteDefaultBranch !== null) {
return remoteDefaultBranch;
}
const fetchCwd =
path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
return yield* executeGit(
Expand Down Expand Up @@ -1262,19 +1328,27 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
cwd: string,
remoteName: string,
): Effect.Effect<string | null, GitCommandError> =>
executeGit(
"GitVcsDriver.resolveDefaultBranchName",
cwd,
["symbolic-ref", `refs/remotes/${remoteName}/HEAD`],
{ allowNonZeroExit: true },
).pipe(
Effect.map((result) => {
if (result.exitCode !== 0) {
return null;
}
return parseDefaultBranchFromRemoteHeadRef(result.stdout, remoteName);
}),
);
Effect.gen(function* () {
const repositoryPaths = yield* resolveRepositoryPaths(cwd).pipe(
Effect.orElseSucceed(() => null),
);
const remoteDefaultBranch =
repositoryPaths === null
? null
: yield* remoteDefaultBranchFor(repositoryPaths.gitCommonDir, remoteName);
if (remoteDefaultBranch !== null) {
return remoteDefaultBranch;
}
const result = yield* executeGit(
"GitVcsDriver.resolveDefaultBranchName",
cwd,
["symbolic-ref", `refs/remotes/${remoteName}/HEAD`],
{ allowNonZeroExit: true },
);
return result.exitCode === 0
? parseDefaultBranchFromRemoteHeadRef(result.stdout, remoteName)
: null;
});

const remoteBranchExists = (
cwd: string,
Expand Down
Loading