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
136 changes: 136 additions & 0 deletions apps/api/src/git-provider/workspace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, expect, it } from "bun:test";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { MockProvider } from "../../mock/provider.js";
import type { Workspace } from "./workspace.js";

async function git(dir: string, argList: string[]): Promise<string> {
const proc = Bun.spawn(["git", ...argList], { cwd: dir, stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, code] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
if (code !== 0) {
throw new Error(stderr || stdout);
}
return stdout.trim();
}

// Isolate the real logger and service dependency from module mocks in other test files
if (process.env.PROVAL_WORKSPACE_TEST_CHILD !== "1") {
it("passes workspace integration in an isolated process", () => {
const result = Bun.spawnSync([process.execPath, "test", import.meta.path], {
env: { ...process.env, PROVAL_WORKSPACE_TEST_CHILD: "1", DB_FILE_NAME: ":memory:" },
stdout: "pipe",
stderr: "pipe",
});
if (result.exitCode !== 0) {
throw new Error(result.stdout.toString() + result.stderr.toString());
}
expect(result.exitCode).toBe(0);
}, 30000);
} else {
const { Workspace } = await import("./workspace.js");

async function withFixture(
branch: string | null,
prRef: string,
run: (fixture: {
dir: string;
targetBranch: string;
workspace: Workspace;
startSha: string;
previousSha: string;
headSha: string;
}) => Promise<void>,
): Promise<void> {
const dir = await mkdtemp(join(tmpdir(), "provalworkspace"));
const provider = new MockProvider({
detail: {
title: "Test",
description: null,
sourceBranch: "feature",
targetBranch: branch ?? "master",
author: "dev",
state: "opened",
},
diffs: [],
});
provider.fetchGitRepositoryUrl = async () => pathToFileURL(dir).href;
provider.getPullRequestHeadFetchRef = () => prRef;
const workspace = new Workspace(provider);

try {
await git(dir, ["init"]);
const targetBranch = branch ?? (await git(dir, ["symbolic-ref", "--short", "HEAD"]));
await git(dir, ["symbolic-ref", "HEAD", `refs/heads/${targetBranch}`]);
await git(dir, ["config", "user.name", "Test"]);
await git(dir, ["config", "user.email", "test@example.com"]);
await git(dir, ["config", "commit.gpgsign", "false"]);
await writeFile(join(dir, "sample.txt"), "start\n");
await git(dir, ["add", "sample.txt"]);
await git(dir, ["commit", "-m", "start"]);
const startSha = await git(dir, ["rev-parse", "HEAD"]);
await git(dir, ["checkout", "-b", "feature"]);
await writeFile(join(dir, "sample.txt"), "previous\n");
await git(dir, ["commit", "-am", "previous"]);
const previousSha = await git(dir, ["rev-parse", "HEAD"]);
await git(dir, ["update-ref", "refs/heads/previous", previousSha]);
await writeFile(join(dir, "sample.txt"), "head\n");
await git(dir, ["commit", "-am", "head"]);
const headSha = await git(dir, ["rev-parse", "HEAD"]);
await git(dir, ["update-ref", prRef, headSha]);

await run({ dir, targetBranch, workspace, startSha, previousSha, headSha });
} finally {
await workspace.clean();
await rm(dir, { recursive: true, force: true });
}
}

describe("Workspace fetch", () => {
for (const prRef of ["refs/pull/21/head", "refs/merge-requests/21/head"]) {
it(`loads ${prRef} when the target matches the initial branch`, async () => {
await withFixture(null, prRef, async ({ targetBranch, workspace, startSha, previousSha, headSha }) => {
await workspace.loadFromPullRequest({
prIid: 21,
targetBranch,
headSha,
startSha,
baseSha: startSha,
previousSha,
});

expect((await workspace.read("sample.txt")).trim()).toBe("head");
expect((await workspace.getFileDiff("sample.txt")).diff).toContain("-start");
expect((await workspace.getPushFileDiff("sample.txt")).diff).toContain("-previous");
});
});
}

for (const branch of [null, "master", "main", "release/stable"]) {
it(`loads branch ${branch ?? "matching the initial default"}`, async () => {
await withFixture(branch, "refs/pull/21/head", async ({ targetBranch, workspace }) => {
await workspace.loadFromBranch(targetBranch);
expect((await workspace.read("sample.txt")).trim()).toBe("start");
});
});
}

it("loads the local branch in an adopted repository and preserves the directory", async () => {
await withFixture("master", "refs/pull/21/head", async ({ dir, workspace, startSha }) => {
await workspace.adopt(dir);
await workspace.loadFromBranch("master");

expect((await workspace.read("sample.txt")).trim()).toBe("start");
expect(await git(dir, ["rev-parse", "HEAD"])).toBe(startSha);
expect(await git(dir, ["rev-parse", "--abbrev-ref", "HEAD"])).toBe("HEAD");
await workspace.clean();
expect((await readFile(join(dir, "sample.txt"), "utf8")).trim()).toBe("start");
});
});
});
}
14 changes: 8 additions & 6 deletions apps/api/src/git-provider/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class Workspace {
this.authHeader = null;
}

public async fetch(ref: string, options: { depth?: number } = {}): Promise<void> {
public async fetch(ref: string, options: { depth?: number } = {}): Promise<string> {
if (!this.rootDir || !this.authHeader) {
throw new Error("Workspace is not initialized. Call init() first.");
}
Expand All @@ -111,14 +111,18 @@ export class Workspace {
}

const depth = options.depth ?? 1;
const spec = SHA_PATTERN.test(want) ? want : `${want}:${want}`;
const target = want.replace(/^refs\/heads\//, "refs/remotes/origin/");
const spec = SHA_PATTERN.test(want) ? want : `${want}:${target}`;
await runCommand(
["git", "-c", `http.extraHeader=${this.authHeader}`, "fetch", `--depth=${depth}`, "origin", spec],
{
cwd: this.rootDir!,
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
},
);

// Returns the fetched remote ref name (refs/remotes/origin/...)
return target;
}

public async checkout(sha: string): Promise<void> {
Expand Down Expand Up @@ -180,10 +184,8 @@ export class Workspace {
await this.init();
}
const branchRef = this.provider.getBranchFetchRef(branch);
if (this.authHeader) {
await this.fetch(branchRef);
}
await this.checkout(branchRef);
const target = this.authHeader ? await this.fetch(branchRef) : branchRef;
await this.checkout(target);
}

public setVersion(version: WorkspaceVersion): void {
Expand Down
Loading