import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, it, describe } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as PlatformError from "effect/PlatformError";import * as Scope from "effect/Scope"; import { GitCommandError } from "@t3tools/contracts";import { ServerConfig } from "../config.ts";import { splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts";import * as GitVcsDriver from "./GitVcsDriver.ts"; const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-vcs-driver-test-",});const TestLayer = GitVcsDriver.layer.pipe( Layer.provide(ServerConfigLayer), Layer.provideMerge(NodeServices.layer),); const makeTmpDir = ( prefix = "git-vcs-driver-test-",): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; return yield* fileSystem.makeTempDirectoryScoped({ prefix }); }); const writeTextFile = ( cwd: string, relativePath: string, contents: string,): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const filePath = pathService.join(cwd, relativePath); yield* fileSystem.makeDirectory(pathService.dirname(filePath), { recursive: true }); yield* fileSystem.writeFileString(filePath, contents); }); const git = ( cwd: string, args: ReadonlyArray<string>, env?: NodeJS.ProcessEnv,): Effect.Effect<string, GitCommandError, GitVcsDriver.GitVcsDriver> => Effect.gen(function* () { const driver = yield* GitVcsDriver.GitVcsDriver; const result = yield* driver.execute({ operation: "GitVcsDriver.test.git", cwd, args, ...(env ? { env } : {}), timeoutMs: 10_000, }); return result.stdout.trim(); }); const initRepoWithCommit = ( cwd: string,): Effect.Effect< { readonly initialBranch: string }, GitCommandError | PlatformError.PlatformError, GitVcsDriver.GitVcsDriver | FileSystem.FileSystem | Path.Path> => Effect.gen(function* () { const driver = yield* GitVcsDriver.GitVcsDriver; yield* driver.initRepo({ cwd }); yield* git(cwd, ["config", "user.email", "test@test.com"]); yield* git(cwd, ["config", "user.name", "Test"]); yield* writeTextFile(cwd, "README.md", "# test\n"); yield* git(cwd, ["add", "."]); yield* git(cwd, ["commit", "-m", "initial commit"]); const initialBranch = yield* git(cwd, ["branch", "--show-current"]); return { initialBranch }; }); it.layer(TestLayer)("GitVcsDriver core integration", (it) => { describe("review diff previews", () => { it.effect("drops an unterminated path from truncated NUL-separated git output", () => Effect.sync(() => { const paths = splitNullSeparatedGitStdoutPaths({ stdout: "complete.txt\0partial", stdoutTruncated: true, }); assert.deepStrictEqual(paths, ["complete.txt"]); }), ); it.effect("keeps the final path when NUL-separated git output is complete", () => Effect.sync(() => { const paths = splitNullSeparatedGitStdoutPaths({ stdout: "complete.txt\0final.txt", stdoutTruncated: false, }); assert.deepStrictEqual(paths, ["complete.txt", "final.txt"]); }), ); }); describe("repository status", () => { it.effect("reports non-repository directories without failing", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); const driver = yield* GitVcsDriver.GitVcsDriver; const refs = yield* driver.listRefs({ cwd }); assert.equal(refs.isRepo, false); assert.deepStrictEqual(refs.refs, []); }), ); it.effect("reports refName and dirty state for a repository", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); const { initialBranch } = yield* initRepoWithCommit(cwd); yield* writeTextFile(cwd, "feature.ts", "export const value = 1;\n"); const status = yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd); assert.equal(status.isRepo, true); assert.equal(status.branch, initialBranch); assert.equal(status.hasWorkingTreeChanges, true); assert.include( status.workingTree.files.map((file) => file.path), "feature.ts", ); }), ); it.effect("reports default-branch delta separately from upstream delta", () => 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* git(cwd, ["checkout", "-b", "feature/synced"]); yield* writeTextFile(cwd, "feature.txt", "feature\n"); yield* git(cwd, ["add", "feature.txt"]); yield* git(cwd, ["commit", "-m", "feature commit"]); yield* git(cwd, ["push", "-u", "origin", "feature/synced"]); const status = yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd); assert.equal(status.hasUpstream, true); assert.equal(status.aheadCount, 0); assert.equal(status.behindCount, 0); assert.equal(status.aheadOfDefaultCount, 1); }), ); it.effect("disables SSH askpass for background upstream status fetches", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); const tempDir = yield* makeTmpDir("git-vcs-driver-ssh-env-"); const { initialBranch } = yield* initRepoWithCommit(cwd); const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const sshLogPath = pathService.join(tempDir, "ssh-env.txt"); const sshWrapperPath = pathService.join(tempDir, "ssh-wrapper.sh"); const previousGitSsh = process.env.GIT_SSH; const previousAskpassRequire = process.env.SSH_ASKPASS_REQUIRE; const previousAskpassLog = process.env.T3_TEST_SSH_ASKPASS_LOG; yield* fileSystem.writeFileString( sshWrapperPath, [ "#!/bin/sh", 'printf "%s\\n" "${SSH_ASKPASS_REQUIRE:-}" > "$T3_TEST_SSH_ASKPASS_LOG"', "exit 1", "", ].join("\n"), ); yield* fileSystem.chmod(sshWrapperPath, 0o755); yield* git(cwd, ["remote", "add", "origin", "ssh://example.invalid/repo.git"]); yield* git(cwd, ["update-ref", `refs/remotes/origin/${initialBranch}`, "HEAD"]); yield* git(cwd, ["branch", "--set-upstream-to", `origin/${initialBranch}`]); yield* Effect.gen(function* () { process.env.GIT_SSH = sshWrapperPath; process.env.SSH_ASKPASS_REQUIRE = "force"; process.env.T3_TEST_SSH_ASKPASS_LOG = sshLogPath; yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd); assert.equal((yield* fileSystem.readFileString(sshLogPath)).trim(), "never"); }).pipe( Effect.ensuring( Effect.sync(() => { if (previousGitSsh === undefined) { delete process.env.GIT_SSH; } else { process.env.GIT_SSH = previousGitSsh; } if (previousAskpassRequire === undefined) { delete process.env.SSH_ASKPASS_REQUIRE; } else { process.env.SSH_ASKPASS_REQUIRE = previousAskpassRequire; } if (previousAskpassLog === undefined) { delete process.env.T3_TEST_SSH_ASKPASS_LOG; } else { process.env.T3_TEST_SSH_ASKPASS_LOG = previousAskpassLog; } }), ), ); }), ); it.effect("reuses the no-upstream fallback ahead count for default-branch delta", () => 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* git(cwd, ["checkout", "-b", "feature/no-upstream"]); yield* writeTextFile(cwd, "feature.txt", "feature\n"); yield* git(cwd, ["add", "feature.txt"]); yield* git(cwd, ["commit", "-m", "feature commit"]); const status = yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd); assert.equal(status.hasUpstream, false); assert.equal(status.aheadCount, 1); assert.equal(status.behindCount, 0); assert.equal(status.aheadOfDefaultCount, 1); }), ); }); describe("refName operations", () => { it.effect("creates, checks out, renames, and lists refs", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); yield* initRepoWithCommit(cwd); const driver = yield* GitVcsDriver.GitVcsDriver; yield* driver.createRef({ cwd, refName: "feature/original" }); const switchRef = yield* driver.switchRef({ cwd, refName: "feature/original" }); assert.equal(switchRef.refName, "feature/original"); const renamed = yield* driver.renameBranch({ cwd, oldBranch: "feature/original", newBranch: "feature/renamed", }); assert.equal(renamed.branch, "feature/renamed"); assert.equal(yield* git(cwd, ["branch", "--show-current"]), "feature/renamed"); const refs = yield* driver.listRefs({ cwd }); assert.equal( refs.refs.find((refName) => refName.name === "feature/renamed")?.current, true, ); }), ); it.effect("returns the existing refName when rename source and target match", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); yield* initRepoWithCommit(cwd); const driver = yield* GitVcsDriver.GitVcsDriver; const current = yield* git(cwd, ["branch", "--show-current"]); const result = yield* driver.renameBranch({ cwd, oldBranch: current, newBranch: current, }); assert.equal(result.branch, current); }), ); }); describe("worktree operations", () => { it.effect("creates and removes a worktree for a new refName", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); const { initialBranch } = yield* initRepoWithCommit(cwd); const pathService = yield* Path.Path; const worktreePath = pathService.join( yield* makeTmpDir("git-worktrees-"), "feature-worktree", ); const driver = yield* GitVcsDriver.GitVcsDriver; const created = yield* driver.createWorktree({ cwd, path: worktreePath, refName: initialBranch, newRefName: "feature/worktree", }); assert.equal(created.worktree.path, worktreePath); assert.equal(created.worktree.refName, "feature/worktree"); assert.equal(yield* git(worktreePath, ["branch", "--show-current"]), "feature/worktree"); yield* driver.removeWorktree({ cwd, path: worktreePath }); const fileSystem = yield* FileSystem.FileSystem; assert.equal(yield* fileSystem.exists(worktreePath), false); }), ); }); describe("commit context", () => { it.effect("stages selected files and commits only those files", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); yield* initRepoWithCommit(cwd); const driver = yield* GitVcsDriver.GitVcsDriver; yield* writeTextFile(cwd, "a.txt", "a\n"); yield* writeTextFile(cwd, "b.txt", "b\n"); const context = yield* driver.prepareCommitContext(cwd, ["a.txt"]); assert.include(context?.stagedSummary ?? "", "a.txt"); assert.notInclude(context?.stagedSummary ?? "", "b.txt"); const commit = yield* driver.commit(cwd, "Add a", ""); assert.match(commit.commitSha, /^[a-f0-9]{40}$/); assert.equal(yield* git(cwd, ["log", "-1", "--pretty=%s"]), "Add a"); const status = yield* git(cwd, ["status", "--porcelain"]); assert.include(status, "?? b.txt"); assert.notInclude(status, "a.txt"); }), ); }); describe("remote operations", () => { it.effect("pushes with upstream setup and skips when already up to date", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); const remote = yield* makeTmpDir("git-remote-"); yield* initRepoWithCommit(cwd); yield* git(remote, ["init", "--bare"]); yield* git(cwd, ["remote", "add", "origin", remote]); yield* (yield* GitVcsDriver.GitVcsDriver).createRef({ cwd, refName: "feature/push", }); yield* (yield* GitVcsDriver.GitVcsDriver).switchRef({ cwd, refName: "feature/push", }); yield* writeTextFile(cwd, "feature.txt", "feature\n"); yield* (yield* GitVcsDriver.GitVcsDriver).prepareCommitContext(cwd); yield* (yield* GitVcsDriver.GitVcsDriver).commit(cwd, "Add feature", ""); const pushed = yield* (yield* GitVcsDriver.GitVcsDriver).pushCurrentBranch(cwd, null); assert.deepInclude(pushed, { status: "pushed", branch: "feature/push", setUpstream: true, }); assert.equal( yield* git(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"]), "origin/feature/push", ); const skipped = yield* (yield* GitVcsDriver.GitVcsDriver).pushCurrentBranch(cwd, null); assert.deepInclude(skipped, { status: "skipped_up_to_date", branch: "feature/push", }); }), ); it.effect( "pushes upstream branches to the remote branch name, not the upstream shorthand", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); const remote = yield* makeTmpDir("git-remote-"); yield* initRepoWithCommit(cwd); const driver = yield* GitVcsDriver.GitVcsDriver; yield* git(cwd, ["branch", "-M", "main"]); yield* git(remote, ["init", "--bare"]); yield* git(cwd, ["remote", "add", "origin", remote]); yield* git(cwd, ["push", "-u", "origin", "main"]); yield* writeTextFile(cwd, "upstream.txt", "upstream\n"); yield* driver.prepareCommitContext(cwd); yield* driver.commit(cwd, "Add upstream update", ""); const pushed = yield* driver.pushCurrentBranch(cwd, null); assert.deepInclude(pushed, { status: "pushed", branch: "main", upstreamBranch: "origin/main", setUpstream: false, }); assert.equal( yield* git(remote, ["log", "-1", "--pretty=%s", "main"]), "Add upstream update", ); const badBranch = yield* driver.execute({ operation: "GitVcsDriver.test.showBadRemoteBranch", cwd: remote, args: ["show-ref", "--verify", "--quiet", "refs/heads/origin/main"], allowNonZeroExit: true, timeoutMs: 10_000, }); assert.notEqual(badBranch.exitCode, 0); }), ); it.effect("pushes to the requested remote instead of the primary remote", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); const originRemote = yield* makeTmpDir("git-origin-remote-"); const publishRemote = yield* makeTmpDir("git-publish-remote-"); yield* initRepoWithCommit(cwd); const driver = yield* GitVcsDriver.GitVcsDriver; yield* git(cwd, ["branch", "-M", "main"]); yield* git(originRemote, ["init", "--bare"]); yield* git(publishRemote, ["init", "--bare"]); yield* git(cwd, ["remote", "add", "origin", originRemote]); yield* git(cwd, ["remote", "add", "origin-1", publishRemote]); const pushed = yield* driver.pushCurrentBranch(cwd, null, { remoteName: "origin-1" }); assert.deepInclude(pushed, { status: "pushed", branch: "main", upstreamBranch: "origin-1/main", setUpstream: true, }); assert.equal( yield* git(publishRemote, ["log", "-1", "--pretty=%s", "main"]), "initial commit", ); const originMain = yield* driver.execute({ operation: "GitVcsDriver.test.originMainMissing", cwd: originRemote, args: ["show-ref", "--verify", "--quiet", "refs/heads/main"], allowNonZeroExit: true, timeoutMs: 10_000, }); assert.notEqual(originMain.exitCode, 0); }), ); });});
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it, describe } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import * as Scope from "effect/Scope";
import { GitCommandError } from "@t3tools/contracts";
import { ServerConfig } from "../config.ts";
import { splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts";
import * as GitVcsDriver from "./GitVcsDriver.ts";
const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-git-vcs-driver-test-",
});
const TestLayer = GitVcsDriver.layer.pipe(
Layer.provide(ServerConfigLayer),
Layer.provideMerge(NodeServices.layer),
);
const makeTmpDir = (
prefix = "git-vcs-driver-test-",
): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
return yield* fileSystem.makeTempDirectoryScoped({ prefix });
});
const writeTextFile = (
cwd: string,
relativePath: string,
contents: string,
): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;
const filePath = pathService.join(cwd, relativePath);
yield* fileSystem.makeDirectory(pathService.dirname(filePath), { recursive: true });
yield* fileSystem.writeFileString(filePath, contents);
});
const git = (
cwd: string,
args: ReadonlyArray<string>,
env?: NodeJS.ProcessEnv,
): Effect.Effect<string, GitCommandError, GitVcsDriver.GitVcsDriver> =>
Effect.gen(function* () {
const driver = yield* GitVcsDriver.GitVcsDriver;
const result = yield* driver.execute({
operation: "GitVcsDriver.test.git",
cwd,
args,
...(env ? { env } : {}),
timeoutMs: 10_000,
});
return result.stdout.trim();
});
const initRepoWithCommit = (
cwd: string,
): Effect.Effect<
{ readonly initialBranch: string },
GitCommandError | PlatformError.PlatformError,
GitVcsDriver.GitVcsDriver | FileSystem.FileSystem | Path.Path
> =>
Effect.gen(function* () {
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* driver.initRepo({ cwd });
yield* git(cwd, ["config", "user.email", "test@test.com"]);
yield* git(cwd, ["config", "user.name", "Test"]);
yield* writeTextFile(cwd, "README.md", "# test\n");
yield* git(cwd, ["add", "."]);
yield* git(cwd, ["commit", "-m", "initial commit"]);
const initialBranch = yield* git(cwd, ["branch", "--show-current"]);
return { initialBranch };
});
it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
describe("review diff previews", () => {
it.effect("drops an unterminated path from truncated NUL-separated git output", () =>
Effect.sync(() => {
const paths = splitNullSeparatedGitStdoutPaths({
stdout: "complete.txt\0partial",
stdoutTruncated: true,
});
assert.deepStrictEqual(paths, ["complete.txt"]);
}),
);
it.effect("keeps the final path when NUL-separated git output is complete", () =>
Effect.sync(() => {
const paths = splitNullSeparatedGitStdoutPaths({
stdout: "complete.txt\0final.txt",
stdoutTruncated: false,
});
assert.deepStrictEqual(paths, ["complete.txt", "final.txt"]);
}),
);
});
describe("repository status", () => {
it.effect("reports non-repository directories without failing", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const driver = yield* GitVcsDriver.GitVcsDriver;
const refs = yield* driver.listRefs({ cwd });
assert.equal(refs.isRepo, false);
assert.deepStrictEqual(refs.refs, []);
}),
);
it.effect("reports refName and dirty state for a repository", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const { initialBranch } = yield* initRepoWithCommit(cwd);
yield* writeTextFile(cwd, "feature.ts", "export const value = 1;\n");
const status = yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd);
assert.equal(status.isRepo, true);
assert.equal(status.branch, initialBranch);
assert.equal(status.hasWorkingTreeChanges, true);
assert.include(
status.workingTree.files.map((file) => file.path),
"feature.ts",
);
}),
);
it.effect("reports default-branch delta separately from upstream delta", () =>
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* git(cwd, ["checkout", "-b", "feature/synced"]);
yield* writeTextFile(cwd, "feature.txt", "feature\n");
yield* git(cwd, ["add", "feature.txt"]);
yield* git(cwd, ["commit", "-m", "feature commit"]);
yield* git(cwd, ["push", "-u", "origin", "feature/synced"]);
const status = yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd);
assert.equal(status.hasUpstream, true);
assert.equal(status.aheadCount, 0);
assert.equal(status.behindCount, 0);
assert.equal(status.aheadOfDefaultCount, 1);
}),
);
it.effect("disables SSH askpass for background upstream status fetches", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const tempDir = yield* makeTmpDir("git-vcs-driver-ssh-env-");
const { initialBranch } = yield* initRepoWithCommit(cwd);
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;
const sshLogPath = pathService.join(tempDir, "ssh-env.txt");
const sshWrapperPath = pathService.join(tempDir, "ssh-wrapper.sh");
const previousGitSsh = process.env.GIT_SSH;
const previousAskpassRequire = process.env.SSH_ASKPASS_REQUIRE;
const previousAskpassLog = process.env.T3_TEST_SSH_ASKPASS_LOG;
yield* fileSystem.writeFileString(
sshWrapperPath,
[
"#!/bin/sh",
'printf "%s\\n" "${SSH_ASKPASS_REQUIRE:-}" > "$T3_TEST_SSH_ASKPASS_LOG"',
"exit 1",
"",
].join("\n"),
);
yield* fileSystem.chmod(sshWrapperPath, 0o755);
yield* git(cwd, ["remote", "add", "origin", "ssh://example.invalid/repo.git"]);
yield* git(cwd, ["update-ref", `refs/remotes/origin/${initialBranch}`, "HEAD"]);
yield* git(cwd, ["branch", "--set-upstream-to", `origin/${initialBranch}`]);
yield* Effect.gen(function* () {
process.env.GIT_SSH = sshWrapperPath;
process.env.SSH_ASKPASS_REQUIRE = "force";
process.env.T3_TEST_SSH_ASKPASS_LOG = sshLogPath;
yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd);
assert.equal((yield* fileSystem.readFileString(sshLogPath)).trim(), "never");
}).pipe(
Effect.ensuring(
Effect.sync(() => {
if (previousGitSsh === undefined) {
delete process.env.GIT_SSH;
} else {
process.env.GIT_SSH = previousGitSsh;
}
if (previousAskpassRequire === undefined) {
delete process.env.SSH_ASKPASS_REQUIRE;
} else {
process.env.SSH_ASKPASS_REQUIRE = previousAskpassRequire;
}
if (previousAskpassLog === undefined) {
delete process.env.T3_TEST_SSH_ASKPASS_LOG;
} else {
process.env.T3_TEST_SSH_ASKPASS_LOG = previousAskpassLog;
}
}),
),
);
}),
);
it.effect("reuses the no-upstream fallback ahead count for default-branch delta", () =>
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* git(cwd, ["checkout", "-b", "feature/no-upstream"]);
yield* writeTextFile(cwd, "feature.txt", "feature\n");
yield* git(cwd, ["add", "feature.txt"]);
yield* git(cwd, ["commit", "-m", "feature commit"]);
const status = yield* (yield* GitVcsDriver.GitVcsDriver).statusDetails(cwd);
assert.equal(status.hasUpstream, false);
assert.equal(status.aheadCount, 1);
assert.equal(status.behindCount, 0);
assert.equal(status.aheadOfDefaultCount, 1);
}),
);
});
describe("refName operations", () => {
it.effect("creates, checks out, renames, and lists refs", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* driver.createRef({ cwd, refName: "feature/original" });
const switchRef = yield* driver.switchRef({ cwd, refName: "feature/original" });
assert.equal(switchRef.refName, "feature/original");
const renamed = yield* driver.renameBranch({
cwd,
oldBranch: "feature/original",
newBranch: "feature/renamed",
});
assert.equal(renamed.branch, "feature/renamed");
assert.equal(yield* git(cwd, ["branch", "--show-current"]), "feature/renamed");
const refs = yield* driver.listRefs({ cwd });
assert.equal(
refs.refs.find((refName) => refName.name === "feature/renamed")?.current,
true,
);
}),
);
it.effect("returns the existing refName when rename source and target match", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
const current = yield* git(cwd, ["branch", "--show-current"]);
const result = yield* driver.renameBranch({
cwd,
oldBranch: current,
newBranch: current,
});
assert.equal(result.branch, current);
}),
);
});
describe("worktree operations", () => {
it.effect("creates and removes a worktree for a new refName", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const { initialBranch } = yield* initRepoWithCommit(cwd);
const pathService = yield* Path.Path;
const worktreePath = pathService.join(
yield* makeTmpDir("git-worktrees-"),
"feature-worktree",
);
const driver = yield* GitVcsDriver.GitVcsDriver;
const created = yield* driver.createWorktree({
cwd,
path: worktreePath,
refName: initialBranch,
newRefName: "feature/worktree",
});
assert.equal(created.worktree.path, worktreePath);
assert.equal(created.worktree.refName, "feature/worktree");
assert.equal(yield* git(worktreePath, ["branch", "--show-current"]), "feature/worktree");
yield* driver.removeWorktree({ cwd, path: worktreePath });
const fileSystem = yield* FileSystem.FileSystem;
assert.equal(yield* fileSystem.exists(worktreePath), false);
}),
);
});
describe("commit context", () => {
it.effect("stages selected files and commits only those files", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* writeTextFile(cwd, "a.txt", "a\n");
yield* writeTextFile(cwd, "b.txt", "b\n");
const context = yield* driver.prepareCommitContext(cwd, ["a.txt"]);
assert.include(context?.stagedSummary ?? "", "a.txt");
assert.notInclude(context?.stagedSummary ?? "", "b.txt");
const commit = yield* driver.commit(cwd, "Add a", "");
assert.match(commit.commitSha, /^[a-f0-9]{40}$/);
assert.equal(yield* git(cwd, ["log", "-1", "--pretty=%s"]), "Add a");
const status = yield* git(cwd, ["status", "--porcelain"]);
assert.include(status, "?? b.txt");
assert.notInclude(status, "a.txt");
}),
);
});
describe("remote operations", () => {
it.effect("pushes with upstream setup and skips when already up to date", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const remote = yield* makeTmpDir("git-remote-");
yield* initRepoWithCommit(cwd);
yield* git(remote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "origin", remote]);
yield* (yield* GitVcsDriver.GitVcsDriver).createRef({
cwd,
refName: "feature/push",
});
yield* (yield* GitVcsDriver.GitVcsDriver).switchRef({
cwd,
refName: "feature/push",
});
yield* writeTextFile(cwd, "feature.txt", "feature\n");
yield* (yield* GitVcsDriver.GitVcsDriver).prepareCommitContext(cwd);
yield* (yield* GitVcsDriver.GitVcsDriver).commit(cwd, "Add feature", "");
const pushed = yield* (yield* GitVcsDriver.GitVcsDriver).pushCurrentBranch(cwd, null);
assert.deepInclude(pushed, {
status: "pushed",
branch: "feature/push",
setUpstream: true,
});
assert.equal(
yield* git(cwd, ["rev-parse", "--abbrev-ref", "@{upstream}"]),
"origin/feature/push",
);
const skipped = yield* (yield* GitVcsDriver.GitVcsDriver).pushCurrentBranch(cwd, null);
assert.deepInclude(skipped, {
status: "skipped_up_to_date",
branch: "feature/push",
});
}),
);
it.effect(
"pushes upstream branches to the remote branch name, not the upstream shorthand",
() =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const remote = yield* makeTmpDir("git-remote-");
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(cwd, ["branch", "-M", "main"]);
yield* git(remote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "origin", remote]);
yield* git(cwd, ["push", "-u", "origin", "main"]);
yield* writeTextFile(cwd, "upstream.txt", "upstream\n");
yield* driver.prepareCommitContext(cwd);
yield* driver.commit(cwd, "Add upstream update", "");
const pushed = yield* driver.pushCurrentBranch(cwd, null);
assert.deepInclude(pushed, {
status: "pushed",
branch: "main",
upstreamBranch: "origin/main",
setUpstream: false,
});
assert.equal(
yield* git(remote, ["log", "-1", "--pretty=%s", "main"]),
"Add upstream update",
);
const badBranch = yield* driver.execute({
operation: "GitVcsDriver.test.showBadRemoteBranch",
cwd: remote,
args: ["show-ref", "--verify", "--quiet", "refs/heads/origin/main"],
allowNonZeroExit: true,
timeoutMs: 10_000,
});
assert.notEqual(badBranch.exitCode, 0);
}),
);
it.effect("pushes to the requested remote instead of the primary remote", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const originRemote = yield* makeTmpDir("git-origin-remote-");
const publishRemote = yield* makeTmpDir("git-publish-remote-");
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(cwd, ["branch", "-M", "main"]);
yield* git(originRemote, ["init", "--bare"]);
yield* git(publishRemote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "origin", originRemote]);
yield* git(cwd, ["remote", "add", "origin-1", publishRemote]);
const pushed = yield* driver.pushCurrentBranch(cwd, null, { remoteName: "origin-1" });
assert.deepInclude(pushed, {
status: "pushed",
branch: "main",
upstreamBranch: "origin-1/main",
setUpstream: true,
});
assert.equal(
yield* git(publishRemote, ["log", "-1", "--pretty=%s", "main"]),
"initial commit",
);
const originMain = yield* driver.execute({
operation: "GitVcsDriver.test.originMainMissing",
cwd: originRemote,
args: ["show-ref", "--verify", "--quiet", "refs/heads/main"],
allowNonZeroExit: true,
timeoutMs: 10_000,
});
assert.notEqual(originMain.exitCode, 0);
}),
);
});
});