дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(vcs): keep a deleted-upstream branch pushable in the multi-repo Push dialog (#1004)
`@{u}..HEAD` fatally fails when the tracking config remains but the
remote-tracking ref is gone (the upstream branch was deleted on the
remote and the fetch pruned): the repository was dropped from the
multi-repository Push dialog, or, when every repository failed, the
dialog showed the raw fatal and disabled Push. Fall back to recent
history like the single-repository dialog does, so the branch stays
listed and pushable (a plain push recreates the remote branch).
Цей коміт міститься в:
1 джерело
82130d37e5
коміт
a15712b670
2 files changed
+74
-6
No files matched your search
@@ -1,10 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { BranchInfo } from "@/bindings";
|
||||
import { messageProblems, wrapAtMargin } from "../CommitView";
|
||||
import { linkifyIssues } from "../issueLinks";
|
||||
import { defaultPushChecked, isProtectedBranch, isPushRejected, pushCandidates, type RepoPush } from "../remoteUi";
|
||||
import {
|
||||
defaultPushChecked,
|
||||
isProtectedBranch,
|
||||
isPushRejected,
|
||||
pushCandidates,
|
||||
pushRepoCommits,
|
||||
type RepoPush,
|
||||
} from "../remoteUi";
|
||||
import { attentionRepo } from "../store";
|
||||
import { statusDecorations } from "../model";
|
||||
|
||||
const vcsLog = vi.fn();
|
||||
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...real,
|
||||
commands: {
|
||||
...real.commands,
|
||||
vcsLog: (repo: unknown, query: unknown) => vcsLog(repo, query),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const cfg = { blankLineAfterSubject: true, limitSubject: true, subjectLimit: 10, limitBody: true, bodyLimit: 8 };
|
||||
|
||||
describe("commit message inspections", () => {
|
||||
@@ -93,3 +114,39 @@ describe("several repositories", () => {
|
||||
expect([...defaultPushChecked(candidates, "/p/b")]).toEqual(["/p", "/p/b"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pushRepoCommits", () => {
|
||||
const branch = (head: string | null, upstream: string | null): BranchInfo => ({
|
||||
head,
|
||||
headCommit: "c1",
|
||||
upstream,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
operation: null,
|
||||
});
|
||||
|
||||
it("falls back to recent history when the upstream no longer resolves", async () => {
|
||||
vcsLog.mockImplementation((_repo: unknown, q: { range: string | null }) =>
|
||||
q.range === "@{u}..HEAD"
|
||||
? Promise.resolve({
|
||||
status: "error",
|
||||
error: "fatal: ambiguous argument '@{u}..HEAD': unknown revision or path not in the working tree.",
|
||||
})
|
||||
: Promise.resolve({ status: "ok", data: [{ id: "c1" }] }),
|
||||
);
|
||||
await expect(pushRepoCommits("/p", branch("feature", "origin/feature"))).resolves.toEqual([{ id: "c1" }]);
|
||||
expect(vcsLog).toHaveBeenLastCalledWith("/p", expect.objectContaining({ branch: "HEAD", range: null, limit: 50 }));
|
||||
});
|
||||
|
||||
it("passes the upstream range through when it resolves", async () => {
|
||||
vcsLog.mockReset().mockResolvedValue({ status: "ok", data: [] });
|
||||
await pushRepoCommits("/p", branch("main", "origin/main"));
|
||||
expect(vcsLog).toHaveBeenCalledWith("/p", expect.objectContaining({ range: "@{u}..HEAD", limit: 1000 }));
|
||||
});
|
||||
|
||||
it("lists no commits for a detached HEAD", async () => {
|
||||
vcsLog.mockClear();
|
||||
await expect(pushRepoCommits("/p", branch(null, null))).resolves.toEqual([]);
|
||||
expect(vcsLog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -95,6 +95,18 @@ export function outgoingQuery(upstream: string | null): { branch: string; range:
|
||||
return upstream ? { branch: "HEAD", range: "@{u}..HEAD", limit: 1000 } : { branch: "HEAD", range: null, limit: 50 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Outgoing commits of one repository in the multi-repository Push dialog. `@{u}` no
|
||||
* longer resolves once the upstream branch was deleted on the remote (the tracking
|
||||
* config remains, `upstream_of` still reports it), so the range query falls back to
|
||||
* recent history and the repository stays in the list, pushable.
|
||||
*/
|
||||
export async function pushRepoCommits(root: string, branch: BranchInfo): Promise<Commit[]> {
|
||||
if (!branch.head) return [];
|
||||
const q = outgoingQuery(branch.upstream);
|
||||
return logOf(q.branch, q.limit, q.range, root).catch(() => logOf(q.branch, 50, null, root));
|
||||
}
|
||||
|
||||
export interface PushTarget {
|
||||
/** Local branch to push (default: the current one). */
|
||||
branch?: string;
|
||||
@@ -148,13 +160,12 @@ function MultiPushDialog({ onClose }: { onClose: () => void }) {
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
// A repository that cannot be read (e.g. its upstream branch was deleted) is left out.
|
||||
// A repository whose branch info cannot be read is left out; a deleted upstream
|
||||
// branch falls back to recent history (pushRepoCommits) and stays pushable.
|
||||
Promise.allSettled(
|
||||
roots.map(async (root) => {
|
||||
const branch = await call(commands.vcsBranchInfo(root));
|
||||
const q = outgoingQuery(branch.upstream);
|
||||
const commits = branch.head ? await logOf(q.branch, q.limit, q.range, root) : [];
|
||||
return { root, branch, commits };
|
||||
return { root, branch, commits: await pushRepoCommits(root, branch) };
|
||||
}),
|
||||
).then((results) => {
|
||||
if (!alive) return;
|
||||
|
||||
Reference in new issue
Заблокувати користувача