Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8229e05
Fix git upstream parsing and refresh timing
juliusmarminge Apr 1, 2026
d5513de
Merge origin/main into t3code/gitcore-race-fix
juliusmarminge Apr 1, 2026
31d0f26
Fix upstream parsing for prefix remotes
juliusmarminge Apr 1, 2026
35b50a1
Stabilize slash-remote GitCore tests
juliusmarminge Apr 1, 2026
dd46b2b
Stabilize overlapping remote tests across Git versions
juliusmarminge Apr 1, 2026
d76318b
Fix slash-remote PR resolution
juliusmarminge Apr 1, 2026
7dd7529
Stabilize cross-repo PR selector tests
juliusmarminge Apr 1, 2026
5351713
Merge branch 'main' into t3code/gitcore-race-fix
juliusmarminge Apr 1, 2026
8e60ea7
Sync sidebar PR state with active branch context
juliusmarminge Apr 1, 2026
e7b7f48
Harden git PR lookup and move success toast server-side
juliusmarminge Apr 1, 2026
54154be
Skip commit and push for ready PR branches
juliusmarminge Apr 1, 2026
7b669d8
Skip duplicate PR lookup after push
juliusmarminge Apr 1, 2026
9653af5
Cache git status refreshes across sibling worktrees
juliusmarminge Apr 1, 2026
6b7c570
Normalize PR summaries before branch matching
juliusmarminge Apr 1, 2026
604bdb8
Skip branch updates on detached HEAD
juliusmarminge Apr 1, 2026
5dc3c21
Split stacked git actions into push and create_pr
juliusmarminge Apr 1, 2026
81eabe2
Fix default branch confirmation dialog for push and create_pr menu ac…
cursoragent Apr 1, 2026
4ebb3d1
Merge branch 'main' into t3code/gitcore-race-fix
juliusmarminge Apr 1, 2026
c0c8c5c
fix: add 'skipped_not_requested' to GitCommitStepStatus for non-commi…
cursoragent Apr 1, 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
284 changes: 216 additions & 68 deletions apps/server/src/git/Layers/GitCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ function git(
});
}

function configureRemote(
cwd: string,
remoteName: string,
remotePath: string,
fetchNamespace: string,
): Effect.Effect<string, GitCommandError, GitCore> {
return Effect.gen(function* () {
yield* git(cwd, ["config", `remote.${remoteName}.url`, remotePath]);
return yield* git(cwd, [
"config",
"--replace-all",
`remote.${remoteName}.fetch`,
`+refs/heads/*:refs/remotes/${fetchNamespace}/*`,
]);
});
}

function runShellCommand(input: {
command: string;
cwd: string;
Expand Down Expand Up @@ -587,7 +604,7 @@ it.layer(TestLayer)("git integration", (it) => {
}),
);

it.effect("keeps checkout successful when upstream refresh fails", () =>
it.effect("statusDetails remains successful when upstream refresh fails after checkout", () =>
Effect.gen(function* () {
const remote = yield* makeTmpDir();
const source = yield* makeTmpDir();
Expand All @@ -612,7 +629,7 @@ it.layer(TestLayer)("git integration", (it) => {
const realGitCore = yield* GitCore;
let refreshFetchAttempts = 0;
const core = yield* makeIsolatedGitCore((input) => {
if (input.args[0] === "fetch") {
if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
refreshFetchAttempts += 1;
return Effect.fail(
new GitCommandError({
Expand All @@ -626,16 +643,15 @@ it.layer(TestLayer)("git integration", (it) => {
return realGitCore.execute(input);
});
yield* core.checkoutBranch({ cwd: source, branch: featureBranch });
yield* Effect.promise(() =>
vi.waitFor(() => {
expect(refreshFetchAttempts).toBe(1);
}),
);
const status = yield* core.statusDetails(source);
expect(refreshFetchAttempts).toBe(1);
expect(status.branch).toBe(featureBranch);
expect(status.upstreamRef).toBe(`origin/${featureBranch}`);
expect(yield* git(source, ["branch", "--show-current"])).toBe(featureBranch);
}),
);

it.effect("refresh fetch is scoped to the checked out branch upstream refspec", () =>
it.effect("defers upstream refresh until statusDetails is requested", () =>
Effect.gen(function* () {
const remote = yield* makeTmpDir();
const source = yield* makeTmpDir();
Expand All @@ -657,10 +673,10 @@ it.layer(TestLayer)("git integration", (it) => {
yield* git(source, ["checkout", defaultBranch]);

const realGitCore = yield* GitCore;
let fetchArgs: readonly string[] | null = null;
let refreshFetchAttempts = 0;
const core = yield* makeIsolatedGitCore((input) => {
if (input.args[0] === "fetch") {
fetchArgs = [...input.args];
if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
refreshFetchAttempts += 1;
return Effect.succeed({
code: 0,
stdout: "",
Expand All @@ -672,73 +688,131 @@ it.layer(TestLayer)("git integration", (it) => {
return realGitCore.execute(input);
});
yield* core.checkoutBranch({ cwd: source, branch: featureBranch });
yield* Effect.promise(() =>
vi.waitFor(() => {
expect(fetchArgs).not.toBeNull();
}),
);

expect(yield* git(source, ["branch", "--show-current"])).toBe(featureBranch);
expect(fetchArgs).toEqual([
"fetch",
"--quiet",
"--no-tags",
"origin",
`+refs/heads/${featureBranch}:refs/remotes/origin/${featureBranch}`,
]);
yield* Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, 50)));
expect(refreshFetchAttempts).toBe(0);
const status = yield* core.statusDetails(source);
expect(status.branch).toBe(featureBranch);
expect(refreshFetchAttempts).toBe(1);
}),
);

it.effect("returns checkout result before background upstream refresh completes", () =>
it.effect("shares upstream refreshes across worktrees that use the same git common dir", () =>
Effect.gen(function* () {
const remote = yield* makeTmpDir();
const source = yield* makeTmpDir();
yield* git(remote, ["init", "--bare"]);
const ok = (stdout = "") =>
Effect.succeed({
code: 0,
stdout,
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
});

yield* initRepoWithCommit(source);
const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(
(branch) => branch.current,
)!.name;
yield* git(source, ["remote", "add", "origin", remote]);
yield* git(source, ["push", "-u", "origin", defaultBranch]);
let fetchCount = 0;
const core = yield* makeIsolatedGitCore((input) => {
if (
input.args[0] === "rev-parse" &&
input.args[1] === "--abbrev-ref" &&
input.args[2] === "--symbolic-full-name" &&
input.args[3] === "@{upstream}"
) {
return ok("origin/main\n");
}
if (input.args[0] === "remote") {
return ok("origin\n");
}
if (input.args[0] === "rev-parse" && input.args[1] === "--git-common-dir") {
return ok("/repo/.git\n");
}
if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
fetchCount += 1;
expect(input.cwd).toBe("/repo");
return ok();
}
if (input.operation === "GitCore.statusDetails.status") {
return ok("# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n");
}
if (
input.operation === "GitCore.statusDetails.unstagedNumstat" ||
input.operation === "GitCore.statusDetails.stagedNumstat"
) {
return ok();
}
return Effect.fail(
new GitCommandError({
operation: input.operation,
command: `git ${input.args.join(" ")}`,
cwd: input.cwd,
detail: "Unexpected git command in shared refresh cache test.",
}),
);
});

const featureBranch = "feature/background-refresh";
yield* git(source, ["checkout", "-b", featureBranch]);
yield* writeTextFile(path.join(source, "feature.txt"), "feature base\n");
yield* git(source, ["add", "feature.txt"]);
yield* git(source, ["commit", "-m", "feature base"]);
yield* git(source, ["push", "-u", "origin", featureBranch]);
yield* git(source, ["checkout", defaultBranch]);
yield* core.statusDetails("/repo/worktrees/main");
yield* core.statusDetails("/repo/worktrees/pr-123");
expect(fetchCount).toBe(1);
}),
);

const realGitCore = yield* GitCore;
let fetchStarted = false;
let releaseFetch!: () => void;
const waitForReleasePromise = new Promise<void>((resolve) => {
releaseFetch = resolve;
});
it.effect("briefly backs off failed upstream refreshes across sibling worktrees", () =>
Effect.gen(function* () {
const ok = (stdout = "") =>
Effect.succeed({
code: 0,
stdout,
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
});

let fetchCount = 0;
const core = yield* makeIsolatedGitCore((input) => {
if (input.args[0] === "fetch") {
fetchStarted = true;
return Effect.promise(() =>
waitForReleasePromise.then(() => ({
code: 0,
stdout: "",
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
})),
if (
input.args[0] === "rev-parse" &&
input.args[1] === "--abbrev-ref" &&
input.args[2] === "--symbolic-full-name" &&
input.args[3] === "@{upstream}"
) {
return ok("origin/main\n");
}
if (input.args[0] === "remote") {
return ok("origin\n");
}
if (input.args[0] === "rev-parse" && input.args[1] === "--git-common-dir") {
return ok("/repo/.git\n");
}
if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
fetchCount += 1;
return Effect.fail(
new GitCommandError({
operation: input.operation,
command: `git ${input.args.join(" ")}`,
cwd: input.cwd,
detail: "simulated fetch timeout",
}),
);
}
return realGitCore.execute(input);
if (input.operation === "GitCore.statusDetails.status") {
return ok("# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n");
}
if (
input.operation === "GitCore.statusDetails.unstagedNumstat" ||
input.operation === "GitCore.statusDetails.stagedNumstat"
) {
return ok();
}
return Effect.fail(
new GitCommandError({
operation: input.operation,
command: `git ${input.args.join(" ")}`,
cwd: input.cwd,
detail: "Unexpected git command in refresh failure cooldown test.",
}),
);
});
yield* core.checkoutBranch({ cwd: source, branch: featureBranch });
yield* Effect.promise(() =>
vi.waitFor(() => {
expect(fetchStarted).toBe(true);
}),
);
expect(yield* git(source, ["branch", "--show-current"])).toBe(featureBranch);
releaseFetch();

yield* core.statusDetails("/repo/worktrees/main");
yield* core.statusDetails("/repo/worktrees/pr-123");
expect(fetchCount).toBe(1);
}),
);

Expand Down Expand Up @@ -779,16 +853,21 @@ it.layer(TestLayer)("git integration", (it) => {
it.effect("checks out a remote tracking branch when remote name contains slashes", () =>
Effect.gen(function* () {
const remote = yield* makeTmpDir();
const prefixRemote = yield* makeTmpDir();
const source = yield* makeTmpDir();
const prefixFetchNamespace = "prefix-my-org";
const prefixRemoteName = "my-org";
const remoteName = "my-org/upstream";
const featureBranch = "feature";
yield* git(remote, ["init", "--bare"]);
yield* git(prefixRemote, ["init", "--bare"]);

yield* initRepoWithCommit(source);
const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(
(branch) => branch.current,
)!.name;
yield* git(source, ["remote", "add", remoteName, remote]);
yield* configureRemote(source, prefixRemoteName, prefixRemote, prefixFetchNamespace);
yield* configureRemote(source, remoteName, remote, remoteName);
yield* git(source, ["push", "-u", remoteName, defaultBranch]);

yield* git(source, ["checkout", "-b", featureBranch]);
Expand All @@ -805,6 +884,34 @@ it.layer(TestLayer)("git integration", (it) => {
});

expect(yield* git(source, ["branch", "--show-current"])).toBe("upstream/feature");
const realGitCore = yield* GitCore;
let fetchArgs: readonly string[] | null = null;
const core = yield* makeIsolatedGitCore((input) => {
if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
fetchArgs = [...input.args];
return Effect.succeed({
code: 0,
stdout: "",
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
});
}
return realGitCore.execute(input);
});

const status = yield* core.statusDetails(source);
expect(status.branch).toBe("upstream/feature");
expect(status.upstreamRef).toBe(`${remoteName}/${featureBranch}`);
expect(fetchArgs).toEqual([
"--git-dir",
path.join(source, ".git"),
"fetch",
"--quiet",
"--no-tags",
remoteName,
`+refs/heads/${featureBranch}:refs/remotes/${remoteName}/${featureBranch}`,
]);
}),
);

Expand Down Expand Up @@ -1691,6 +1798,47 @@ it.layer(TestLayer)("git integration", (it) => {
}),
);

it.effect("pushes to the tracked upstream when the remote name contains slashes", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
const remote = yield* makeTmpDir();
const prefixRemote = yield* makeTmpDir();
const prefixFetchNamespace = "prefix-my-org";
const prefixRemoteName = "my-org";
const remoteName = "my-org/upstream";
const featureBranch = "feature/slash-remote-push";
yield* git(remote, ["init", "--bare"]);
yield* git(prefixRemote, ["init", "--bare"]);

const { initialBranch } = yield* initRepoWithCommit(tmp);
yield* configureRemote(tmp, prefixRemoteName, prefixRemote, prefixFetchNamespace);
yield* configureRemote(tmp, remoteName, remote, remoteName);
yield* git(tmp, ["push", "-u", remoteName, initialBranch]);

yield* git(tmp, ["checkout", "-b", featureBranch]);
yield* writeTextFile(path.join(tmp, "feature.txt"), "first revision\n");
yield* git(tmp, ["add", "feature.txt"]);
yield* git(tmp, ["commit", "-m", "feature base"]);
yield* git(tmp, ["push", "-u", remoteName, featureBranch]);

yield* writeTextFile(path.join(tmp, "feature.txt"), "second revision\n");
yield* git(tmp, ["add", "feature.txt"]);
yield* git(tmp, ["commit", "-m", "feature update"]);

const core = yield* GitCore;
const pushed = yield* core.pushCurrentBranch(tmp, null);
expect(pushed.status).toBe("pushed");
expect(pushed.setUpstream).toBe(false);
expect(pushed.upstreamBranch).toBe(`${remoteName}/${featureBranch}`);
expect(yield* git(tmp, ["rev-parse", "--abbrev-ref", "@{upstream}"])).toBe(
`${remoteName}/${featureBranch}`,
);
expect(yield* git(tmp, ["ls-remote", "--heads", remoteName, featureBranch])).toContain(
featureBranch,
);
}),
);

it.effect("includes command context when worktree removal fails", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
Expand Down
Loading
Loading