mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 02:02:42 +00:00
* fix(server): bound shared-worktree fetch refreshes * fix(server): preserve buffered git metadata events * fix(server): cover remote ref selection changes * fix(server): preserve narrow fetch echo routing * fix(server): preserve ref updates during refresh Keep normal refresh requeue semantics for watcher events that arrive while a ref-derived calculation is active, so a second same-ref movement cannot be mistaken for the original fetch echo. * fix(server): narrow external fetch ref updates Use authoritative fetch boundaries to separate watcher echoes from outside events, and retain remote ref names so packed-to-loose materialization from other Git clients stays narrow without weakening add/delete fallbacks.
186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { afterEach, expect, test } from "vitest";
|
|
import { type CheckoutSnapshotFacts, getCheckoutRefDerivedState } from "./checkout-git.js";
|
|
import {
|
|
startGitCommandMetrics,
|
|
stopGitCommandMetrics,
|
|
waitForGitCommandMetricsIdle,
|
|
} from "./run-git-command.js";
|
|
|
|
const cleanupPaths: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const path of cleanupPaths.splice(0)) {
|
|
rmSync(path, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function git(cwd: string, args: string[]): string {
|
|
return execFileSync("git", args, {
|
|
cwd,
|
|
encoding: "utf8",
|
|
env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1" },
|
|
}).trim();
|
|
}
|
|
|
|
function seedFeatureCheckout(): {
|
|
cwd: string;
|
|
facts: Extract<CheckoutSnapshotFacts, { isGit: true }>;
|
|
} {
|
|
const cwd = mkdtempSync(join(tmpdir(), "paseo-ref-derived-"));
|
|
cleanupPaths.push(cwd);
|
|
git(cwd, ["init", "-b", "main"]);
|
|
git(cwd, ["config", "user.email", "repro@example.com"]);
|
|
git(cwd, ["config", "user.name", "Repro"]);
|
|
writeFileSync(join(cwd, "base.txt"), "base\n");
|
|
git(cwd, ["add", "base.txt"]);
|
|
git(cwd, ["commit", "-m", "base"]);
|
|
const base = git(cwd, ["rev-parse", "HEAD"]);
|
|
git(cwd, ["update-ref", "refs/remotes/origin/main", base]);
|
|
git(cwd, ["switch", "--quiet", "-c", "feature"]);
|
|
writeFileSync(join(cwd, "feature.txt"), "feature\n");
|
|
git(cwd, ["add", "feature.txt"]);
|
|
git(cwd, ["commit", "-m", "feature"]);
|
|
|
|
return {
|
|
cwd,
|
|
facts: {
|
|
isGit: true,
|
|
worktreeRoot: cwd,
|
|
currentBranch: "feature",
|
|
remoteUrl: "https://example.com/repo.git",
|
|
absoluteGitDir: join(cwd, ".git"),
|
|
gitCommonDir: join(cwd, ".git"),
|
|
paseoWorktree: { isPaseoOwnedWorktree: false },
|
|
storedBaseRef: null,
|
|
resolvedBaseRef: "main",
|
|
mainRepoRoot: null,
|
|
comparisonBaseRef: "origin/main",
|
|
branchRemoteName: "origin",
|
|
branchMergeRef: "refs/heads/main",
|
|
upstreamStatus: {
|
|
ref: "refs/remotes/origin/main",
|
|
aheadBehind: { ahead: 0, behind: 0 },
|
|
},
|
|
pullRequestLookupTarget: { headRef: "feature" },
|
|
},
|
|
};
|
|
}
|
|
|
|
function seedMainCheckout(): {
|
|
cwd: string;
|
|
facts: Extract<CheckoutSnapshotFacts, { isGit: true }>;
|
|
} {
|
|
const cwd = mkdtempSync(join(tmpdir(), "paseo-ref-derived-main-"));
|
|
cleanupPaths.push(cwd);
|
|
git(cwd, ["init", "-b", "main"]);
|
|
git(cwd, ["config", "user.email", "repro@example.com"]);
|
|
git(cwd, ["config", "user.name", "Repro"]);
|
|
writeFileSync(join(cwd, "base.txt"), "base\n");
|
|
git(cwd, ["add", "base.txt"]);
|
|
git(cwd, ["commit", "-m", "base"]);
|
|
git(cwd, ["update-ref", "refs/remotes/origin/main", git(cwd, ["rev-parse", "HEAD"])]);
|
|
writeFileSync(join(cwd, "local.txt"), "local\n");
|
|
git(cwd, ["add", "local.txt"]);
|
|
git(cwd, ["commit", "-m", "local"]);
|
|
|
|
return {
|
|
cwd,
|
|
facts: {
|
|
isGit: true,
|
|
worktreeRoot: cwd,
|
|
currentBranch: "main",
|
|
remoteUrl: "https://example.com/repo.git",
|
|
absoluteGitDir: join(cwd, ".git"),
|
|
gitCommonDir: join(cwd, ".git"),
|
|
paseoWorktree: { isPaseoOwnedWorktree: false },
|
|
storedBaseRef: null,
|
|
resolvedBaseRef: "main",
|
|
mainRepoRoot: null,
|
|
comparisonBaseRef: null,
|
|
branchRemoteName: "origin",
|
|
branchMergeRef: "refs/heads/main",
|
|
upstreamStatus: {
|
|
ref: "refs/remotes/origin/main",
|
|
aheadBehind: { ahead: 1, behind: 0 },
|
|
},
|
|
pullRequestLookupTarget: { headRef: "main" },
|
|
},
|
|
};
|
|
}
|
|
|
|
test("one comparison refresh updates a shared base and upstream ref", async () => {
|
|
const { cwd, facts } = seedFeatureCheckout();
|
|
startGitCommandMetrics();
|
|
const result = await getCheckoutRefDerivedState(
|
|
cwd,
|
|
facts,
|
|
{ aheadBehind: { ahead: 0, behind: 0 }, diffStat: null },
|
|
new Set(["origin/main"]),
|
|
);
|
|
await waitForGitCommandMetricsIdle({ quietMs: 50, timeoutMs: 5_000 });
|
|
const metrics = stopGitCommandMetrics();
|
|
|
|
expect(result.aheadBehind).toEqual({ ahead: 1, behind: 0 });
|
|
expect(result.upstreamStatus).toEqual({
|
|
ref: "refs/remotes/origin/main",
|
|
aheadBehind: { ahead: 1, behind: 0 },
|
|
});
|
|
expect(result.diffStat).toEqual({ additions: 1, deletions: 0 });
|
|
expect(metrics.submissions.map((command) => command.args[0]).sort()).toEqual([
|
|
"diff",
|
|
"ls-files",
|
|
"merge-base",
|
|
"rev-list",
|
|
]);
|
|
});
|
|
|
|
test("an upstream move refreshes the main checkout shortstat", async () => {
|
|
const { cwd, facts } = seedMainCheckout();
|
|
git(cwd, ["update-ref", "refs/remotes/origin/main", git(cwd, ["rev-parse", "HEAD"])]);
|
|
|
|
const result = await getCheckoutRefDerivedState(
|
|
cwd,
|
|
facts,
|
|
{ aheadBehind: null, diffStat: { additions: 1, deletions: 0 } },
|
|
new Set(["origin/main"]),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
aheadBehind: null,
|
|
diffStat: null,
|
|
upstreamStatus: {
|
|
ref: "refs/remotes/origin/main",
|
|
aheadBehind: { ahead: 0, behind: 0 },
|
|
},
|
|
});
|
|
});
|
|
|
|
test("an origin move refreshes an untracked main checkout shortstat", async () => {
|
|
const seeded = seedMainCheckout();
|
|
const facts = {
|
|
...seeded.facts,
|
|
branchRemoteName: null,
|
|
branchMergeRef: null,
|
|
upstreamStatus: null,
|
|
};
|
|
git(seeded.cwd, [
|
|
"update-ref",
|
|
"refs/remotes/origin/main",
|
|
git(seeded.cwd, ["rev-parse", "HEAD"]),
|
|
]);
|
|
|
|
expect(
|
|
await getCheckoutRefDerivedState(
|
|
seeded.cwd,
|
|
facts,
|
|
{ aheadBehind: null, diffStat: { additions: 1, deletions: 0 } },
|
|
new Set(["origin/main"]),
|
|
),
|
|
).toEqual({ aheadBehind: null, diffStat: null, upstreamStatus: null });
|
|
});
|