Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c9ecb88
fix(server): publish local git status before remote lookup
JustYannicc Oct 17, 2026
2f13197
fix(server): keep branch status retries accurate
JustYannicc Oct 18, 2026
7e31306
Merge remote-tracking branch 'origin/main' into fix/local-git-status-…
juliusmarminge Oct 19, 2026
91d904d
fix(server): publish local status before remote refresh
juliusmarminge Oct 19, 2026
e16df94
Merge branch 'main' into fix/local-git-status-upstream
JustYannicc Oct 19, 2026
9824d38
fix(server): assemble git status from one cache snapshot
JustYannicc Oct 19, 2026
696a3d4
Merge branch 'main' into fix/local-git-status-upstream
JustYannicc Oct 19, 2026
468c21a
Merge branch 'main' into fix/local-git-status-upstream
JustYannicc Oct 19, 2026
038c7cb
chore: merge upstream main into local git status fix
JustYannicc Oct 20, 2026
e2a5da5
Merge branch 'main' into fix/local-git-status-upstream
JustYannicc Oct 20, 2026
7df11f1
Merge branch 'main' into fix/local-git-status-upstream
JustYannicc Oct 20, 2026
43aff74
Merge branch 'main' into fix/local-git-status-upstream
JustYannicc Oct 20, 2026
cd7bd82
Merge branch 'main' into fix/local-git-status-upstream
JustYannicc Oct 20, 2026
b35f6d6
Merge branch 'main' into fix/local-git-status-upstream
JustYannicc Oct 20, 2026
3845014
merge: resolve Git status tests against upstream main
JustYannicc Oct 21, 2026
91eefb4
Merge commit '955b4cc80166fd0bfd01fe2ec40285bd755f986f' into babysit/…
JustYannicc Oct 21, 2026
6581bf9
merge: adapt Git status regression to source-control provider fake
JustYannicc Oct 24, 2026
15561ab
merge: retain local-first status through upstream comment cleanup
JustYannicc Oct 25, 2026
0659864
merge: preserve local-first status with branch picker refresh
JustYannicc Oct 25, 2026
8b39687
merge: preserve Git branch lookup with upstream Effect tracing
JustYannicc Oct 26, 2026
1a6fcb8
test(server): use tap for Git status fixture side effects
JustYannicc Oct 26, 2026
8fe2afc
merge: sync upstream agent context budget baseline
JustYannicc Oct 28, 2026
8be315b
test(server): remove obsolete background demand fixture
JustYannicc Oct 28, 2026
bb35b94
merge: sync reduced upstream agent context budget
JustYannicc Oct 28, 2026
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
115 changes: 115 additions & 0 deletions apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
import { SourceControlProviderKind } from "@t3tools/contracts";
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFS from "node:fs";
Expand Down Expand Up @@ -69,6 +71,10 @@ type FakeChangeRequest = Omit<ChangeRequest, "provider" | "state" | "updatedAt">
};

interface FakeProviderScenario {
prListGate?: {
readonly started: Deferred.Deferred<void>;
readonly release: Deferred.Deferred<void>;
};
/** The kind the fake host reports itself as. */
kind?: string;
/** Answers to successive change request listings, whatever their head selector. */
Expand Down Expand Up @@ -452,6 +458,13 @@ function createFakeSourceControlProvider(scenario: FakeProviderScenario = {}): {
[]
).map(toChangeRequest),
),
Effect.tap(() =>
scenario.prListGate
? Deferred.succeed(scenario.prListGate.started, undefined).pipe(
Effect.andThen(Deferred.await(scenario.prListGate.release)),
)
: Effect.void,
),
),
createChangeRequest: (input) =>
record(input.cwd, {
Expand Down Expand Up @@ -723,6 +736,108 @@ const layerGitManagerTest = GitVcsDriver.layer.pipe(
);

it.layer(layerGitManagerTest)("GitManager", (it) => {
it.effect(
"publishes local Git changes during a held PR lookup without caching its old branch",
() =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-refresh-status-gh-gate-");
yield* initRepo(repoDir);
yield* runGit(repoDir, ["checkout", "-b", "release"]);
const remoteDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
yield* runGit(repoDir, ["push", "-u", "origin", "release"]);
yield* runGit(repoDir, ["tag", "release"]);
const oldPr = {
number: 613,
title: "Release PR",
url: "https://github.com/pingdotgg/codething-mvp/pull/613",
baseRefName: "main",
headRefName: "release",
};
const newPr = { ...oldPr, number: 614, headRefName: "heads/release" };
const providerScenario: FakeProviderScenario = {
prListSequence: [oldPr, oldPr, newPr].map((pr) => [pr]),
};
const { manager } = yield* makeManager({ providerScenario });
const context = yield* Layer.build(
VcsStatusBroadcaster.layer.pipe(
Layer.provide(
GitWorkflowService.layer.pipe(
Layer.provide(Layer.succeed(GitManager.GitManager, manager)),
Layer.provide(
VcsDriverRegistry.layer.pipe(
Layer.provide(VcsProjectConfig.layer),
Layer.provide(VcsProcess.layer),
),
),
),
),
Layer.provide(
Layer.mock(BackgroundPolicy.BackgroundPolicy)({
shouldRunScopeWork: () => Effect.succeed(true),
}),
),
),
);
const broadcaster = Context.get(context, VcsStatusBroadcaster.VcsStatusBroadcaster);
const initial = yield* broadcaster.getStatus({ cwd: repoDir });
expect(initial.refName).toBe("release");
expect(initial.pr?.number).toBe(613);

const subscribed = yield* Deferred.make<void>();
const published = yield* Deferred.make<void>();
yield* broadcaster.streamStatus({ cwd: repoDir, includeRemote: false }).pipe(
Stream.runForEach((event) => {
if (event._tag === "snapshot") return Deferred.succeed(subscribed, undefined);
if (event._tag === "localUpdated" && event.local.hasWorkingTreeChanges) {
return Deferred.succeed(published, undefined);
}
return Effect.void;
}),
Effect.forkScoped,
);
yield* Deferred.await(subscribed);
const gate = {
started: yield* Deferred.make<void>(),
release: yield* Deferred.make<void>(),
};
providerScenario.prListGate = gate;
const fs = yield* FileSystem.FileSystem;
yield* fs.writeFileString(NodePath.join(repoDir, "README.md"), "changed before refresh\n");
const completed = yield* Deferred.make<void>();
const refresh = yield* broadcaster
.refreshStatus(repoDir)
.pipe(Effect.ensuring(Deferred.succeed(completed, undefined)), Effect.forkScoped);
yield* Deferred.await(gate.started);
const pending = Option.getOrThrow(
yield* Stream.runHead(broadcaster.streamStatus({ cwd: repoDir, includeRemote: false })),
);
expect(pending._tag).toBe("snapshot");
if (pending._tag === "snapshot") expect(pending.local.hasWorkingTreeChanges).toBe(true);
yield* Deferred.await(published);
expect(yield* Deferred.isDone(completed)).toBe(false);

yield* runGit(repoDir, ["checkout", "-b", "heads/release"]);
yield* runGit(repoDir, ["push", "-u", "origin", "refs/heads/heads/release"]);
yield* Deferred.succeed(gate.release, undefined);
const refreshed = yield* Fiber.join(refresh);
expect(refreshed.refName).toBe("heads/release");
expect(refreshed.hasWorkingTreeChanges).toBe(true);
expect(refreshed.pr).toBeNull();
const snapshot = Option.getOrThrow(
yield* Stream.runHead(broadcaster.streamStatus({ cwd: repoDir, includeRemote: false })),
);
expect(snapshot._tag).toBe("snapshot");
if (snapshot._tag === "snapshot") expect(snapshot.remote).toBeNull();

delete providerScenario.prListGate;
const recovered = yield* broadcaster.getStatus({ cwd: repoDir });
expect(recovered.refName).toBe("heads/release");
expect(recovered.pr?.number).toBe(614);
expect((yield* broadcaster.getStatus({ cwd: repoDir })).pr?.number).toBe(614);
}),
);

it.effect("status reports the listed PR whose head is the branch", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down
27 changes: 27 additions & 0 deletions apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class GitWorkflowService extends Context.Service<
readonly localStatus: (
input: VcsStatusInput,
) => Effect.Effect<VcsStatusLocalResult, GitManagerServiceError>;
readonly currentBranch: (cwd: string) => Effect.Effect<string | null, GitManagerServiceError>;
readonly remoteStatus: (
input: VcsStatusInput,
options?: GitManager.GitRemoteStatusOptions,
Expand Down Expand Up @@ -332,6 +333,32 @@ export const make = Effect.gen(function* () {
),
);
}),
currentBranch: Effect.fn("GitWorkflowService.currentBranch")(function* (cwd) {
if (!(yield* detectGitRepositoryForStatus("GitWorkflowService.currentBranch", cwd))) {
return null;
}
const result = yield* git.execute({
operation: "GitWorkflowService.currentBranch",
cwd,
args: ["symbolic-ref", "--quiet", "HEAD"],
allowNonZeroExit: true,
timeoutMs: 5_000,
});
if (result.exitCode === 0) {
return result.stdout.trim().replace(/^refs\/heads\//, "") || null;
}
if (result.exitCode === 1) return null;
return yield* new GitCommandError({
operation: "GitWorkflowService.currentBranch",
command: "git symbolic-ref",
cwd,
argumentCount: 3,
...(result.exitCode === null ? {} : { exitCode: result.exitCode }),
stdoutLength: result.stdout.length,
stderrLength: result.stderr.length,
detail: "Failed to read the checked-out branch.",
});
}),
remoteStatus: Effect.fn("GitWorkflowService.remoteStatus")(function (input, options) {
return detectGitRepositoryForStatus("GitWorkflowService.remoteStatus", input.cwd).pipe(
Effect.flatMap((isGitRepository) =>
Expand Down
Loading
Loading