Skip to content
Merged
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
218 changes: 218 additions & 0 deletions apps/server/src/git/Layers/GitCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,18 @@ it.layer(TestLayer)("git integration", (it) => {
}),
);

it.effect("does not include detached HEAD pseudo-refs as branches", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
yield* git(tmp, ["checkout", "--detach", "HEAD"]);

const result = yield* listGitBranches({ cwd: tmp });
expect(result.branches.some((branch) => branch.name.startsWith("("))).toBe(false);
expect(result.branches.some((branch) => branch.current)).toBe(false);
}),
);

it.effect("keeps current branch first and sorts the remaining branches by recency", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
Expand Down Expand Up @@ -370,6 +382,82 @@ it.layer(TestLayer)("git integration", (it) => {
expect(result.branches.every((b) => b.isDefault === false)).toBe(true);
}),
);

it.effect("lists local branches first and remote branches last", () =>
Effect.gen(function* () {
const remote = yield* makeTmpDir();
const tmp = yield* makeTmpDir();

yield* git(remote, ["init", "--bare"]);
yield* initRepoWithCommit(tmp);
const defaultBranch = (yield* listGitBranches({ cwd: tmp })).branches.find(
(branch) => branch.current,
)!.name;

yield* git(tmp, ["remote", "add", "origin", remote]);
yield* git(tmp, ["push", "-u", "origin", defaultBranch]);

yield* createGitBranch({ cwd: tmp, branch: "feature/local-only" });

const remoteOnlyBranch = "feature/remote-only";
yield* checkoutGitBranch({ cwd: tmp, branch: defaultBranch });
yield* git(tmp, ["checkout", "-b", remoteOnlyBranch]);
yield* git(tmp, ["push", "-u", "origin", remoteOnlyBranch]);
yield* git(tmp, ["checkout", defaultBranch]);
yield* git(tmp, ["branch", "-D", remoteOnlyBranch]);

const result = yield* listGitBranches({ cwd: tmp });
const firstRemoteIndex = result.branches.findIndex((branch) => branch.isRemote);

expect(firstRemoteIndex).toBeGreaterThan(0);
expect(result.branches.slice(0, firstRemoteIndex).every((branch) => !branch.isRemote)).toBe(
true,
);
expect(result.branches.slice(firstRemoteIndex).every((branch) => branch.isRemote)).toBe(
true,
);
expect(
result.branches.some((branch) => branch.name === "feature/local-only" && !branch.isRemote),
).toBe(true);
expect(
result.branches.some(
(branch) => branch.name === "origin/feature/remote-only" && branch.isRemote,
),
).toBe(true);
}),
);

it.effect("includes remoteName metadata for remotes with slash in the name", () =>
Effect.gen(function* () {
const remote = yield* makeTmpDir();
const tmp = yield* makeTmpDir();
const remoteName = "my-org/upstream";

yield* git(remote, ["init", "--bare"]);
yield* initRepoWithCommit(tmp);
const defaultBranch = (yield* listGitBranches({ cwd: tmp })).branches.find(
(branch) => branch.current,
)!.name;

yield* git(tmp, ["remote", "add", remoteName, remote]);
yield* git(tmp, ["push", "-u", remoteName, defaultBranch]);

const remoteOnlyBranch = "feature/remote-with-remote-name";
yield* git(tmp, ["checkout", "-b", remoteOnlyBranch]);
yield* git(tmp, ["push", "-u", remoteName, remoteOnlyBranch]);
yield* git(tmp, ["checkout", defaultBranch]);
yield* git(tmp, ["branch", "-D", remoteOnlyBranch]);

const result = yield* listGitBranches({ cwd: tmp });
const remoteBranch = result.branches.find(
(branch) => branch.name === `${remoteName}/${remoteOnlyBranch}`,
);

expect(remoteBranch).toBeDefined();
expect(remoteBranch?.isRemote).toBe(true);
expect(remoteBranch?.remoteName).toBe(remoteName);
}),
);
});

// ── checkoutGitBranch ──
Expand Down Expand Up @@ -592,6 +680,87 @@ it.layer(TestLayer)("git integration", (it) => {
}),
);

it.effect(
"does not silently checkout a local branch when a remote ref no longer exists",
() =>
Effect.gen(function* () {
const remote = yield* makeTmpDir();
const source = yield* makeTmpDir();
yield* git(remote, ["init", "--bare"]);

yield* initRepoWithCommit(source);
const defaultBranch = (yield* listGitBranches({ cwd: source })).branches.find(
(branch) => branch.current,
)!.name;
yield* git(source, ["remote", "add", "origin", remote]);
yield* git(source, ["push", "-u", "origin", defaultBranch]);

yield* createGitBranch({ cwd: source, branch: "feature" });

const checkoutResult = yield* Effect.result(
checkoutGitBranch({ cwd: source, branch: "origin/feature" }),
);
expect(checkoutResult._tag).toBe("Failure");
expect(yield* git(source, ["branch", "--show-current"])).toBe(defaultBranch);
}),
);

it.effect("checks out a remote tracking branch when remote name contains slashes", () =>
Effect.gen(function* () {
const remote = yield* makeTmpDir();
const source = yield* makeTmpDir();
const remoteName = "my-org/upstream";
const featureBranch = "feature";
yield* git(remote, ["init", "--bare"]);

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

yield* git(source, ["checkout", "-b", featureBranch]);
yield* writeTextFile(path.join(source, "feature.txt"), "feature content\n");
yield* git(source, ["add", "feature.txt"]);
yield* git(source, ["commit", "-m", "feature commit"]);
yield* git(source, ["push", "-u", remoteName, featureBranch]);
yield* git(source, ["checkout", defaultBranch]);
yield* git(source, ["branch", "-D", featureBranch]);

yield* checkoutGitBranch({ cwd: source, branch: `${remoteName}/${featureBranch}` });

expect(yield* git(source, ["branch", "--show-current"])).toBe("upstream/feature");
}),
);

it.effect(
"falls back to detached checkout when --track would conflict with an existing local branch",
() =>
Effect.gen(function* () {
const remote = yield* makeTmpDir();
const source = yield* makeTmpDir();
yield* git(remote, ["init", "--bare"]);

yield* initRepoWithCommit(source);
const defaultBranch = (yield* listGitBranches({ cwd: source })).branches.find(
(branch) => branch.current,
)!.name;
yield* git(source, ["remote", "add", "origin", remote]);
yield* git(source, ["push", "-u", "origin", defaultBranch]);

// Keep local branch but remove tracking so `--track origin/<branch>`
// would attempt to create an already-existing local branch.
yield* git(source, ["branch", "--unset-upstream"]);

yield* checkoutGitBranch({ cwd: source, branch: `origin/${defaultBranch}` });

const core = yield* GitCore;
const status = yield* core.statusDetails(source);
expect(status.branch).toBeNull();
}),
);

it.effect("throws when checkout would overwrite uncommitted changes", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
Expand Down Expand Up @@ -1153,5 +1322,54 @@ it.layer(TestLayer)("git integration", (it) => {
expect(didFailRecency).toBe(true);
}),
);

it.effect("falls back to empty remote branch data when remote lookups fail", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
const remote = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
yield* git(remote, ["init", "--bare"]);
yield* git(tmp, ["remote", "add", "origin", remote]);

const realGitService = yield* GitService;
let didFailRemoteBranches = false;
let didFailRemoteNames = false;
const core = yield* makeIsolatedGitCore({
execute: (input) => {
if (input.args.join(" ") === "branch --no-color --remotes") {
didFailRemoteBranches = true;
return Effect.fail(
new GitCommandError({
operation: "git.test.listBranchesRemoteBranches",
command: `git ${input.args.join(" ")}`,
cwd: input.cwd,
detail: "remote unavailable",
}),
);
}
if (input.args.join(" ") === "remote") {
didFailRemoteNames = true;
return Effect.fail(
new GitCommandError({
operation: "git.test.listBranchesRemoteNames",
command: `git ${input.args.join(" ")}`,
cwd: input.cwd,
detail: "remote unavailable",
}),
);
}
return realGitService.execute(input);
},
});

const result = yield* core.listBranches({ cwd: tmp });

expect(result.isRepo).toBe(true);
expect(result.branches.length).toBeGreaterThan(0);
expect(result.branches.every((branch) => !branch.isRemote)).toBe(true);
expect(didFailRemoteBranches).toBe(true);
expect(didFailRemoteNames).toBe(true);
}),
);
});
});
Loading