Compare commits

...
Author SHA1 Message Date
l0ng-ai f25740f14d fix(vcs): handle repositories nested below a non-Git project root
- Project view: the Git submenu was gated on the project root being a
  repository, so it never appeared when the repositories are found later
  below a non-Git root. It now also follows the VCS availability that the
  Commit tool window uses.
- Commit: when only one repository had included changes, every staged but
  excluded file of every repository was passed to it, and the commit failed
  with "<path> is outside the repository". Paths that belong to another known
  repository are now left to that repository.

Fixes #2517
Fixes #2518
2026-11-10 23:07:22 +08:00
4 changed files with 37 additions and 3 deletions

No files matched your search

@@ -1,3 +1,4 @@
import { clearContext, setContext } from "@/platform/context";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { registerActions } from "@/platform/actions";
import { useProject } from "@/platform/project";
@@ -88,6 +89,17 @@ describe("project view context menu", () => {
expect(labels(contextMenuItems([node("/p/a.txt"), node("/p/b.txt")]))).toContain("Compare Files");
});
it("offers Git for repositories found below a non-Git project root", () => {
useProject.setState({ project: { name: "p", root: ROOT, trusted: true, isGit: false, settings: {} } as never });
expect(sub(contextMenuItems([node("/p/a.py")]), "Git")).toBeUndefined();
setContext("vcsAvailable", true);
try {
expect(sub(contextMenuItems([node("/p/a.py")]), "Git")).toContain("Commit File…");
} finally {
clearContext("vcsAvailable");
}
});
it("follows IDEA's order for a file", () => {
const items = contextMenuItems([node(`${ROOT}/src/a.py`)]);
expect(labels(items)).toEqual([
+3 -2
View File
@@ -1,5 +1,6 @@
/** Context menu and "New" popup of the project view. */
import { executeAction, getAction } from "@/platform/actions";
import { getContext } from "@/platform/context";
import { canRevealInFileManager, fileManagerName } from "@/platform/fileManager";
import { formatBinding, hostBinding, isMacHost, shortcutLabel } from "@/platform/keymap";
import { relativePath, useProject } from "@/platform/project";
@@ -332,8 +333,8 @@ export function contextMenuItems(nodes: ProjectNode[]): MenuItem[] {
{ kind: "separator" },
);
// Git ›
if (inProject && project?.isGit) {
// Git › (`vcsAvailable` also covers repositories nested below a non-Git project root)
if (inProject && (project?.isGit || getContext().vcsAvailable === true)) {
const arg = vcsArg(nodes);
const dirs = nodes.some((n) => n.dir);
push(
@@ -76,6 +76,23 @@ describe("the commit plugins see", () => {
expect(draftRevision()).toBeGreaterThan(before);
});
it("leaves another repository's staged files out of the only repository committed", async () => {
const staged: FileStatus = {
path: "/w/b/new.yml",
relPath: "new.yml",
staged: "added",
unstaged: null,
renamedFrom: null,
};
const changed: FileStatus = { ...files[0]!, path: "/w/a/Foo.java", relPath: "Foo.java" };
useVcs.setState({ root: "/w", repo: "/w/a", roots: ["/w/a", "/w/b"], status: [changed, staged] });
useCommit.setState({ checks: new Map([["/w/b/new.yml", "none" as const]]) });
const selection = (await maybeCommitService()!.selection())!;
expect(selection.repositories).toEqual([
{ root: "/w/a", options: expect.objectContaining({ stage: ["/w/a/Foo.java"], exclude: [] }) },
]);
});
it("is null without a repository", async () => {
useVcs.setState({ root: null });
expect(await maybeCommitService()!.selection()).toBeNull();
+5 -1
View File
@@ -86,7 +86,11 @@ export async function selectionOf(plan: CommitPlan): Promise<CommitSelection> {
const stage = [...plan.stage, ...partial.whole];
const fallback = useVcs.getState().root ?? "";
const repositories = repos.map((repo) => {
const inRepo = (path: string) => repos.length === 1 || (rootOf(path, roots) ?? null) === repo;
// A path in another known repository never goes to this one, even when it is the only one committed.
const inRepo = (path: string) => {
const root = rootOf(path, roots);
return root === null ? repos.length === 1 : root === repo;
};
const options: CommitOptions = {
amend,
signOff,