mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Preserve base worktree paths when merging (#6668)
* fix(git): preserve the base worktree path when merging * test(git): cover merging into whitespace worktree paths * test(git): follow the server object parameter convention * test(git): keep merge fixture line endings deterministic Disable automatic CRLF checkout in the fixture repository so the exact-content assertion works with Windows Git defaults. Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com> --------- Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -89,7 +89,7 @@
|
||||
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
|
||||
"test": "npm run test:unit && npm run test:integration",
|
||||
"test:unit": "vitest run --fileParallelism --exclude \"**/*.e2e.test.ts\"",
|
||||
"test:integration": "vitest run --maxWorkers=1 src/server/daemon-e2e/filesystem.e2e.test.ts src/server/daemon-e2e/file-download.e2e.test.ts src/server/daemon-e2e/cli-logs-follow.e2e.test.ts src/server/creation/creation.e2e.test.ts src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts src/server/plugins/plugin-paseo-api.e2e.test.ts && vitest run --maxWorkers=1 src/server/session.create-agent-worktree-autoarchive.e2e.test.ts -t \"creates a worktree and auto-archives both\" && vitest run --maxWorkers=1 src/server/agent/providers/opencode-bridge.local.e2e.test.ts -t \"versioned runtime\"",
|
||||
"test:integration": "vitest run --maxWorkers=1 src/server/daemon-e2e/filesystem.e2e.test.ts src/server/daemon-e2e/file-download.e2e.test.ts src/server/daemon-e2e/cli-logs-follow.e2e.test.ts src/server/creation/creation.e2e.test.ts src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts src/server/plugins/plugin-paseo-api.e2e.test.ts && vitest run --maxWorkers=1 src/server/daemon-e2e/git-operations.e2e.test.ts -t \"merges into\" && vitest run --maxWorkers=1 src/server/session.create-agent-worktree-autoarchive.e2e.test.ts -t \"creates a worktree and auto-archives both\" && vitest run --maxWorkers=1 src/server/agent/providers/opencode-bridge.local.e2e.test.ts -t \"versioned runtime\"",
|
||||
"test:hub-cli-contract": "vitest run --maxWorkers=1 src/server/hub/hub-cli-contract.test.ts",
|
||||
"test:integration:all": "npm run test:e2e",
|
||||
"test:integration:real": "vitest run real.e2e.test.ts",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { test, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, writeFileSync, existsSync, rmSync, readFileSync, realpathSync } from "fs";
|
||||
import {
|
||||
mkdtempSync,
|
||||
writeFileSync,
|
||||
existsSync,
|
||||
rmSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
mkdirSync,
|
||||
} from "fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { tmpdir } from "os";
|
||||
import path from "path";
|
||||
import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js";
|
||||
@@ -152,6 +161,78 @@ afterEach(async () => {
|
||||
await ctx.cleanup();
|
||||
}, 60000);
|
||||
|
||||
interface GitCommandOptions {
|
||||
cwd: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
async function mergeFeatureIntoBaseWorktree(baseName: string): Promise<void> {
|
||||
const directory = realpathSync(tmpCwd());
|
||||
const base = path.join(directory, baseName);
|
||||
const feature = path.join(directory, "feature");
|
||||
function git({ cwd, args }: GitCommandOptions): string {
|
||||
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: "pipe" });
|
||||
}
|
||||
try {
|
||||
mkdirSync(base);
|
||||
git({ cwd: base, args: ["init", "-b", "main"] });
|
||||
git({ cwd: base, args: ["config", "user.name", "Paseo Test"] });
|
||||
git({ cwd: base, args: ["config", "user.email", "test@paseo.local"] });
|
||||
git({ cwd: base, args: ["config", "commit.gpgsign", "false"] });
|
||||
git({ cwd: base, args: ["config", "core.autocrlf", "false"] });
|
||||
git({ cwd: base, args: ["commit", "--allow-empty", "-m", "base"] });
|
||||
git({ cwd: base, args: ["worktree", "add", "-b", "feature", feature] });
|
||||
writeFileSync(path.join(feature, "feature.txt"), "feature payload\n");
|
||||
git({ cwd: feature, args: ["add", "feature.txt"] });
|
||||
git({ cwd: feature, args: ["commit", "-m", "feature"] });
|
||||
const featureCommit = git({ cwd: feature, args: ["rev-parse", "HEAD"] }).trim();
|
||||
|
||||
const merged = await ctx.client.checkoutMerge(feature, { baseRef: "main" });
|
||||
expect(merged).toMatchObject({ success: true, error: null });
|
||||
expect(git({ cwd: base, args: ["rev-parse", "HEAD"] }).trim()).toBe(featureCommit);
|
||||
expect(readFileSync(path.join(base, "feature.txt"), "utf8")).toBe("feature payload\n");
|
||||
expect(git({ cwd: feature, args: ["symbolic-ref", "--short", "HEAD"] }).trim()).toBe("feature");
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test.skipIf(isPlatform("win32")).each(["base\ncheckout", "base "])(
|
||||
"merges into a base worktree whose path preserves %j",
|
||||
mergeFeatureIntoBaseWorktree,
|
||||
);
|
||||
|
||||
test.each(["base checkout", "中文 checkout"])(
|
||||
"merges into a base worktree named %j",
|
||||
mergeFeatureIntoBaseWorktree,
|
||||
);
|
||||
|
||||
test("merges into a local base branch without another worktree", async () => {
|
||||
const cwd = realpathSync(tmpCwd());
|
||||
function git(args: string[]): string {
|
||||
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: "pipe" });
|
||||
}
|
||||
try {
|
||||
git(["init", "-b", "main"]);
|
||||
git(["config", "user.name", "Paseo Test"]);
|
||||
git(["config", "user.email", "test@paseo.local"]);
|
||||
git(["config", "commit.gpgsign", "false"]);
|
||||
git(["commit", "--allow-empty", "-m", "base"]);
|
||||
git(["checkout", "-b", "feature"]);
|
||||
writeFileSync(path.join(cwd, "feature.txt"), "feature payload\n");
|
||||
git(["add", "feature.txt"]);
|
||||
git(["commit", "-m", "feature"]);
|
||||
const featureCommit = git(["rev-parse", "HEAD"]).trim();
|
||||
|
||||
const merged = await ctx.client.checkoutMerge(cwd, { baseRef: "main" });
|
||||
expect(merged).toMatchObject({ success: true, error: null });
|
||||
expect(git(["rev-parse", "main"]).trim()).toBe(featureCommit);
|
||||
expect(git(["symbolic-ref", "--short", "HEAD"]).trim()).toBe("feature");
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("returns diff for modified file in git repo", async () => {
|
||||
const cwd = tmpCwd();
|
||||
|
||||
|
||||
@@ -1187,13 +1187,16 @@ export function parseWorktreeList(output: string): GitWorktreeEntry[] {
|
||||
|
||||
async function getWorktreePathForBranch(cwd: string, branchName: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await runGitCommand(["worktree", "list", "--porcelain"], {
|
||||
cwd,
|
||||
envOverlay: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
const entries = parseWorktreeList(stdout);
|
||||
const ref = branchName.startsWith("refs/heads/") ? branchName : `refs/heads/${branchName}`;
|
||||
return entries.find((entry) => entry.branchRef === ref)?.path ?? null;
|
||||
const { stdout } = await runGitCommand(
|
||||
["for-each-ref", "--format=%(refname)%00%(worktreepath)%00", ref],
|
||||
{ cwd, envOverlay: READ_ONLY_GIT_ENV },
|
||||
);
|
||||
for (const record of stdout.split("\0\n")) {
|
||||
const [reference, worktreePath] = record.split("\0");
|
||||
if (reference === ref) return worktreePath || null;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user