mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Archive merged PR workspaces from settings (#1313)
* Add workspace settings for merged PR cleanup * Fix worktree branch ahead status * Handle PR worktree upstream status * Remove stale checkout tracking field * Update settings E2E host section slugs * Treat unknown upstream as unsafe for auto-archive
This commit is contained in:
@@ -233,6 +233,17 @@ describe("archiveIfSafe", () => {
|
||||
expect(harness.deps.archivePaseoWorktree).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does nothing when the upstream status is unknown", async () => {
|
||||
const harness = createHarness({
|
||||
getSnapshot: async () => createSnapshot({ git: { aheadOfOrigin: null } }),
|
||||
});
|
||||
|
||||
await runArchiveIfSafe(harness);
|
||||
|
||||
expect(harness.deps.isPaseoOwnedWorktreeCwd).not.toHaveBeenCalled();
|
||||
expect(harness.deps.archivePaseoWorktree).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does nothing when the cwd is not a Paseo-owned worktree", async () => {
|
||||
const harness = createHarness({
|
||||
isPaseoOwnedWorktreeCwd: async () => ({ allowed: false, worktreePath: CWD }),
|
||||
|
||||
@@ -78,7 +78,10 @@ export async function archiveIfSafe(input: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapshot.git.isDirty === true || (snapshot.git.aheadOfOrigin ?? 0) > 0) {
|
||||
if (snapshot.git.isDirty === true || snapshot.git.aheadOfOrigin === null) {
|
||||
return;
|
||||
}
|
||||
if (snapshot.git.aheadOfOrigin > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ function createCheckoutFacts(
|
||||
comparisonBaseRef: null,
|
||||
branchRemoteName: null,
|
||||
branchMergeRef: null,
|
||||
trackedOriginBranch: null,
|
||||
pullRequestLookupTarget: { headRef: "main" },
|
||||
...overrides,
|
||||
};
|
||||
@@ -890,7 +889,6 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
currentBranch: "fork-owner/open-button-targets-active-file",
|
||||
branchRemoteName: "paseo-pr-1285",
|
||||
branchMergeRef: "refs/heads/open-button-targets-active-file",
|
||||
trackedOriginBranch: "paseo-pr-1285/open-button-targets-active-file",
|
||||
pullRequestLookupTarget: {
|
||||
headRef: "open-button-targets-active-file",
|
||||
headRepositoryOwner: "fork-owner",
|
||||
|
||||
@@ -121,7 +121,6 @@ function createCheckoutSnapshotFacts(cwd: string): CheckoutSnapshotFacts {
|
||||
comparisonBaseRef: null,
|
||||
branchRemoteName: "origin",
|
||||
branchMergeRef: "refs/heads/main",
|
||||
trackedOriginBranch: "main",
|
||||
pullRequestLookupTarget: { headRef: "main" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -522,6 +522,92 @@ const x = 1;
|
||||
expect(divergedStatus.behindOfOrigin).toBe(1);
|
||||
});
|
||||
|
||||
it("reports a PR worktree as not ahead when its branch is pushed to the configured PR remote", async () => {
|
||||
setupRemoteTrackingMain(repoDir, tempDir);
|
||||
const prRemoteDir = join(tempDir, "pr-remote.git");
|
||||
execFileSync("git", ["init", "--bare", "-b", "main", prRemoteDir]);
|
||||
execFileSync("git", ["checkout", "-b", "aaronzhongg/open-button-targets-active-file"], {
|
||||
cwd: repoDir,
|
||||
});
|
||||
commitFile(repoDir, "feature.txt", "feature\n", "feature commit");
|
||||
execFileSync("git", ["remote", "add", "paseo-pr-1285", prRemoteDir], { cwd: repoDir });
|
||||
execFileSync(
|
||||
"git",
|
||||
["push", "paseo-pr-1285", "HEAD:refs/heads/open-button-targets-active-file"],
|
||||
{ cwd: repoDir },
|
||||
);
|
||||
execFileSync(
|
||||
"git",
|
||||
["config", "branch.aaronzhongg/open-button-targets-active-file.remote", "paseo-pr-1285"],
|
||||
{
|
||||
cwd: repoDir,
|
||||
},
|
||||
);
|
||||
execFileSync(
|
||||
"git",
|
||||
[
|
||||
"config",
|
||||
"branch.aaronzhongg/open-button-targets-active-file.merge",
|
||||
"refs/heads/open-button-targets-active-file",
|
||||
],
|
||||
{ cwd: repoDir },
|
||||
);
|
||||
|
||||
const status = await getCheckoutStatus(repoDir);
|
||||
|
||||
expect(status).toMatchObject({
|
||||
isGit: true,
|
||||
currentBranch: "aaronzhongg/open-button-targets-active-file",
|
||||
aheadOfOrigin: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a PR worktree as behind when its configured PR remote has newer commits", async () => {
|
||||
setupRemoteTrackingMain(repoDir, tempDir);
|
||||
const prRemoteDir = join(tempDir, "pr-remote.git");
|
||||
const prCloneDir = join(tempDir, "pr-clone");
|
||||
execFileSync("git", ["init", "--bare", "-b", "main", prRemoteDir]);
|
||||
execFileSync("git", ["checkout", "-b", "aaronzhongg/open-button-targets-active-file"], {
|
||||
cwd: repoDir,
|
||||
});
|
||||
commitFile(repoDir, "feature.txt", "feature\n", "feature commit");
|
||||
execFileSync("git", ["remote", "add", "paseo-pr-1285", prRemoteDir], { cwd: repoDir });
|
||||
execFileSync(
|
||||
"git",
|
||||
["push", "paseo-pr-1285", "HEAD:refs/heads/open-button-targets-active-file"],
|
||||
{ cwd: repoDir },
|
||||
);
|
||||
execFileSync(
|
||||
"git",
|
||||
["config", "branch.aaronzhongg/open-button-targets-active-file.remote", "paseo-pr-1285"],
|
||||
{ cwd: repoDir },
|
||||
);
|
||||
execFileSync(
|
||||
"git",
|
||||
[
|
||||
"config",
|
||||
"branch.aaronzhongg/open-button-targets-active-file.merge",
|
||||
"refs/heads/open-button-targets-active-file",
|
||||
],
|
||||
{ cwd: repoDir },
|
||||
);
|
||||
execFileSync("git", ["clone", prRemoteDir, prCloneDir]);
|
||||
execFileSync("git", ["checkout", "open-button-targets-active-file"], { cwd: prCloneDir });
|
||||
execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: prCloneDir });
|
||||
execFileSync("git", ["config", "user.name", "Test"], { cwd: prCloneDir });
|
||||
commitFile(prCloneDir, "remote.txt", "remote\n", "remote update");
|
||||
execFileSync("git", ["push"], { cwd: prCloneDir });
|
||||
execFileSync("git", ["fetch", "paseo-pr-1285"], { cwd: repoDir });
|
||||
|
||||
const status = await getCheckoutStatus(repoDir);
|
||||
|
||||
expect(status).toMatchObject({
|
||||
isGit: true,
|
||||
currentBranch: "aaronzhongg/open-button-targets-active-file",
|
||||
behindOfOrigin: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not report the full branch history as ahead when the current branch remote is gone", async () => {
|
||||
setupRemoteTrackingMain(repoDir, tempDir);
|
||||
execFileSync("git", ["checkout", "-b", "feature"], { cwd: repoDir });
|
||||
@@ -557,11 +643,11 @@ const x = 1;
|
||||
isPaseoOwnedWorktree: true,
|
||||
baseRef: "main",
|
||||
aheadBehind: { ahead: 0, behind: 0 },
|
||||
aheadOfOrigin: 0,
|
||||
aheadOfOrigin: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports local-only worktree commits as unpushed relative to base", async () => {
|
||||
it("does not report local-only no-track worktree commits as ahead of origin", async () => {
|
||||
setupRemoteTrackingMain(repoDir, tempDir);
|
||||
commitFile(repoDir, "second.txt", "second\n", "second commit");
|
||||
execFileSync("git", ["push"], { cwd: repoDir });
|
||||
@@ -581,7 +667,7 @@ const x = 1;
|
||||
isPaseoOwnedWorktree: true,
|
||||
baseRef: "main",
|
||||
aheadBehind: { ahead: 1, behind: 0 },
|
||||
aheadOfOrigin: 1,
|
||||
aheadOfOrigin: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -802,7 +802,6 @@ export type CheckoutSnapshotFacts =
|
||||
comparisonBaseRef: string | null;
|
||||
branchRemoteName: string | null;
|
||||
branchMergeRef: string | null;
|
||||
trackedOriginBranch: string | null;
|
||||
pullRequestLookupTarget: PullRequestStatusLookupTarget | null;
|
||||
};
|
||||
|
||||
@@ -1407,57 +1406,46 @@ async function getAheadBehind(
|
||||
async function getAheadOfOrigin(
|
||||
cwd: string,
|
||||
currentBranch: string,
|
||||
baseRef: string | null,
|
||||
context?: CheckoutContext,
|
||||
): Promise<number | null> {
|
||||
if (!currentBranch) {
|
||||
return null;
|
||||
}
|
||||
const trackedOriginBranch = await getTrackedOriginBranch(cwd, currentBranch, context);
|
||||
const originBranch = trackedOriginBranch ?? currentBranch;
|
||||
const upstreamRef = await getConfiguredUpstreamRef(cwd, currentBranch, context);
|
||||
if (!upstreamRef) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const { stdout } = await runGitCommand(
|
||||
["rev-list", "--count", `origin/${originBranch}..${currentBranch}`],
|
||||
["rev-list", "--count", `${upstreamRef}..${currentBranch}`],
|
||||
{ cwd, envOverlay: READ_ONLY_GIT_ENV, logger: context?.logger },
|
||||
);
|
||||
const count = Number.parseInt(stdout.trim(), 10);
|
||||
return Number.isNaN(count) ? null : count;
|
||||
} catch {
|
||||
if (trackedOriginBranch) {
|
||||
return null;
|
||||
}
|
||||
if (!baseRef || normalizeLocalBranchRefName(baseRef) === currentBranch) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, baseRef, context);
|
||||
const { stdout } = await runGitCommand(
|
||||
["rev-list", "--count", `${comparisonBaseRef}..${currentBranch}`],
|
||||
{ cwd, envOverlay: READ_ONLY_GIT_ENV, logger: context?.logger },
|
||||
);
|
||||
const count = Number.parseInt(stdout.trim(), 10);
|
||||
return Number.isNaN(count) ? null : count;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getTrackedOriginBranch(
|
||||
async function getConfiguredUpstreamRef(
|
||||
cwd: string,
|
||||
currentBranch: string,
|
||||
context?: CheckoutContext,
|
||||
): Promise<string | null> {
|
||||
if (context?.facts?.isGit && context.facts.currentBranch === currentBranch) {
|
||||
return context.facts.trackedOriginBranch;
|
||||
}
|
||||
const remoteName = await getGitConfigValue(cwd, `branch.${currentBranch}.remote`, context);
|
||||
if (remoteName !== "origin") {
|
||||
const remoteName =
|
||||
context?.facts?.isGit && context.facts.currentBranch === currentBranch
|
||||
? context.facts.branchRemoteName
|
||||
: await getGitConfigValue(cwd, `branch.${currentBranch}.remote`, context);
|
||||
if (!remoteName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mergeRef = await getGitConfigValue(cwd, `branch.${currentBranch}.merge`, context);
|
||||
return parseBranchMergeHeadRef(mergeRef);
|
||||
const mergeRef =
|
||||
context?.facts?.isGit && context.facts.currentBranch === currentBranch
|
||||
? context.facts.branchMergeRef
|
||||
: await getGitConfigValue(cwd, `branch.${currentBranch}.merge`, context);
|
||||
const upstreamBranch = parseBranchMergeHeadRef(mergeRef);
|
||||
return upstreamBranch ? `${remoteName}/${upstreamBranch}` : null;
|
||||
}
|
||||
|
||||
async function getBehindOfOrigin(
|
||||
@@ -1468,9 +1456,13 @@ async function getBehindOfOrigin(
|
||||
if (!currentBranch) {
|
||||
return null;
|
||||
}
|
||||
const upstreamRef = await getConfiguredUpstreamRef(cwd, currentBranch, context);
|
||||
if (!upstreamRef) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const { stdout } = await runGitCommand(
|
||||
["rev-list", "--count", `${currentBranch}..origin/${currentBranch}`],
|
||||
["rev-list", "--count", `${currentBranch}..${upstreamRef}`],
|
||||
{ cwd, envOverlay: READ_ONLY_GIT_ENV, logger: context?.logger },
|
||||
);
|
||||
const count = Number.parseInt(stdout.trim(), 10);
|
||||
@@ -1602,8 +1594,6 @@ export async function getCheckoutSnapshotFacts(
|
||||
}
|
||||
}
|
||||
}
|
||||
const trackedOriginBranch =
|
||||
branchRemoteName === "origin" ? parseBranchMergeHeadRef(branchMergeRef) : null;
|
||||
const pullRequestLookupTarget = inspected.currentBranch
|
||||
? buildPullRequestLookupTargetFromBranchConfig({
|
||||
currentBranch: inspected.currentBranch,
|
||||
@@ -1627,7 +1617,6 @@ export async function getCheckoutSnapshotFacts(
|
||||
comparisonBaseRef,
|
||||
branchRemoteName,
|
||||
branchMergeRef,
|
||||
trackedOriginBranch,
|
||||
pullRequestLookupTarget,
|
||||
};
|
||||
}
|
||||
@@ -1765,7 +1754,7 @@ export async function getCheckoutStatus(
|
||||
? getAheadBehind(cwd, baseRef, currentBranch, factsContext)
|
||||
: Promise.resolve(null),
|
||||
hasRemote && currentBranch
|
||||
? getAheadOfOrigin(cwd, currentBranch, baseRef, factsContext)
|
||||
? getAheadOfOrigin(cwd, currentBranch, factsContext)
|
||||
: Promise.resolve(null),
|
||||
hasRemote && currentBranch
|
||||
? getBehindOfOrigin(cwd, currentBranch, factsContext)
|
||||
|
||||
Reference in New Issue
Block a user