mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(vcs): Keep submodule entries out of Rollback and refresh the log of another repository
- Rollback no longer offers a submodule's own entry: it cannot be reverted from the enclosing repository (it did nothing while reporting "Rolled back 1 file"). Rolling back only that entry explains how to revert it instead. - A selected change is rolled back alone; only folders take the changes below them, so selecting a submodule's entry no longer pulls in the uncommitted files inside it. - The Git log loads the branches of the newly selected repository, so it reloads when that repository's refs move (a new branch showed up only after a manual refresh).
This commit is contained in:
@@ -111,14 +111,16 @@ function LogView() {
|
||||
const commits = useGitLog((s) => s.commits);
|
||||
const selected = useGitLog((s) => s.selected);
|
||||
const root = useVcs((s) => s.root);
|
||||
const repo = useVcs((s) => s.repo);
|
||||
const lastRefs = useRef<string | null>(null);
|
||||
|
||||
// On mount and when the window switches to another repository.
|
||||
// On mount, when the window switches to another project and when another of its
|
||||
// repositories is selected (its branches are needed to notice its refs moving).
|
||||
useEffect(() => {
|
||||
lastRefs.current = null;
|
||||
if (!useGitLog.getState().commits.length) void reloadLog();
|
||||
void loadBranches().catch(() => undefined);
|
||||
}, [root]);
|
||||
}, [root, repo]);
|
||||
|
||||
// HEAD or a branch tip moved (commit, undo, reset, rebase, fetch…): reload (keeps the selection).
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { FileStatus } from "@/bindings";
|
||||
import { isNestedRepoEntry, rollbackCandidates } from "../changeOps";
|
||||
|
||||
const fs = (rel: string, unstaged: FileStatus["unstaged"]): FileStatus => ({
|
||||
path: `/r/${rel}`,
|
||||
relPath: rel,
|
||||
staged: null,
|
||||
unstaged,
|
||||
renamedFrom: null,
|
||||
});
|
||||
|
||||
describe("rollback candidates with a submodule", () => {
|
||||
const roots = ["/r", "/r/libs/core"];
|
||||
const status = [
|
||||
fs("app.txt", "modified"),
|
||||
fs("libs/core", "modified"),
|
||||
fs("libs/core/core.txt", "modified"),
|
||||
fs("libs/core/wip.txt", "untracked"),
|
||||
];
|
||||
const paths = (files: FileStatus[]) => files.map((f) => f.path);
|
||||
|
||||
it("never offers the submodule's own entry", () => {
|
||||
expect(isNestedRepoEntry("/r/libs/core", roots)).toBe(true);
|
||||
expect(isNestedRepoEntry("/r", roots)).toBe(false);
|
||||
expect(paths(rollbackCandidates(undefined, status, roots))).toEqual(["/r/app.txt", "/r/libs/core/core.txt"]);
|
||||
});
|
||||
|
||||
it("takes a selected change alone, not the submodule's changes below its path", () => {
|
||||
expect(rollbackCandidates(["/r/libs/core"], status, roots)).toEqual([]);
|
||||
expect(paths(rollbackCandidates(["/r/app.txt", "/r/libs/core"], status, roots))).toEqual(["/r/app.txt"]);
|
||||
});
|
||||
|
||||
it("takes the changes below a selected folder", () => {
|
||||
expect(paths(rollbackCandidates(["/r/libs"], status, roots))).toEqual(["/r/libs/core/core.txt"]);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { editorService, maybeEditorService, notifications, toolWindowService } f
|
||||
import { annotationsShown, toggleAnnotations } from "./blame";
|
||||
import { showBranchesPopup } from "./BranchWidget";
|
||||
import { checkoutRevision, newBranch } from "./branchOps";
|
||||
import { addToVcs, rollbackCandidates, showRollbackDialog, stashChanges } from "./changeOps";
|
||||
import { addToVcs, isNestedRepoEntry, rollbackCandidates, showRollbackDialog, stashChanges } from "./changeOps";
|
||||
import { useCommit } from "./commitStore";
|
||||
import { gotoConflict, resolveAtCaret, showConflictsDialog } from "./conflictsUi";
|
||||
import { focusedDiffNavigator } from "./diffNavigator";
|
||||
@@ -20,7 +20,7 @@ import { allRepos, runRemote } from "./ops";
|
||||
import { showPushDialog, showUpdateDialog, type PushTarget } from "./remoteUi";
|
||||
import { chooseRevision } from "./RevisionChooser";
|
||||
import { showCloneDialog } from "./clone";
|
||||
import { refreshNow } from "./store";
|
||||
import { refreshNow, repoName } from "./store";
|
||||
|
||||
const VCS = "vcsAvailable";
|
||||
|
||||
@@ -171,7 +171,18 @@ export function registerVcsActions(): () => void {
|
||||
const explicit = argPaths(arg);
|
||||
const sel = useCommit.getState().selection;
|
||||
const paths = explicit?.length ? explicit : commitWindowFocused() && sel.length ? sel : undefined;
|
||||
if (paths) return showRollbackDialog(rollbackCandidates(paths));
|
||||
if (paths) {
|
||||
const candidates = rollbackCandidates(paths);
|
||||
const submodule = paths.find((p) => isNestedRepoEntry(p));
|
||||
if (!candidates.length && submodule)
|
||||
return notifications().notify({
|
||||
group: "vcs",
|
||||
level: "info",
|
||||
title: "Submodule changes cannot be rolled back here",
|
||||
body: `${repoName(submodule)} points to another commit. Check out the recorded commit in the submodule (git submodule update) to revert it.`,
|
||||
});
|
||||
return showRollbackDialog(candidates);
|
||||
}
|
||||
const active = maybeEditorService()?.activePath();
|
||||
const activeChanged = active ? rollbackCandidates([active]) : [];
|
||||
showRollbackDialog(activeChanged.length ? activeChanged : rollbackCandidates());
|
||||
|
||||
@@ -77,20 +77,32 @@ export async function commitChanges(): Promise<string | undefined> {
|
||||
}
|
||||
|
||||
/** Files the Rollback action applies to by default. */
|
||||
export function rollbackCandidates(paths?: readonly string[]): FileStatus[] {
|
||||
const status = useVcs.getState().status.filter((f) => {
|
||||
export function rollbackCandidates(
|
||||
paths?: readonly string[],
|
||||
status: readonly FileStatus[] = useVcs.getState().status,
|
||||
roots: readonly string[] = useVcs.getState().roots,
|
||||
): FileStatus[] {
|
||||
// A nested repository's own entry (a submodule's checked-out commit) is not rolled back
|
||||
// from the enclosing repository: it is changed by checking out a commit in the submodule.
|
||||
const changed = status.filter((f) => {
|
||||
const g = groupOf(f);
|
||||
return g === "changes" || g === "conflicts";
|
||||
return (g === "changes" || g === "conflicts") && !isNestedRepoEntry(f.path, roots);
|
||||
});
|
||||
if (!paths?.length) return status;
|
||||
if (!paths?.length) return changed;
|
||||
const set = new Set(paths.map(stripSlash));
|
||||
const picked = status.filter((f) => {
|
||||
// A selected change is just that change; other paths are folders and take the changes below them.
|
||||
const entries = new Set(status.map((f) => stripSlash(f.path)));
|
||||
const folders = [...set].filter((s) => !entries.has(s));
|
||||
return changed.filter((f) => {
|
||||
const p = stripSlash(f.path);
|
||||
if (set.has(p)) return true;
|
||||
for (const s of set) if (p.startsWith(s + "/")) return true;
|
||||
return false;
|
||||
return set.has(p) || folders.some((s) => p.startsWith(s + "/"));
|
||||
});
|
||||
return picked;
|
||||
}
|
||||
|
||||
/** `path` is the root of a repository nested in another one (its submodule / untracked-folder entry there). */
|
||||
export function isNestedRepoEntry(path: string, roots: readonly string[] = useVcs.getState().roots): boolean {
|
||||
const p = stripSlash(path);
|
||||
return roots.includes(p) && roots.some((r) => p.startsWith(r + "/"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1945,6 +1945,7 @@
|
||||
"Structure unavailable": "结构不可用",
|
||||
"Subject line length": "主题行长度",
|
||||
"Submit": "提交",
|
||||
"Submodule changes cannot be rolled back here": "子模块的变更无法在此回滚",
|
||||
"Subpixel": "子像素",
|
||||
"Suggested names": "建议的名称",
|
||||
"Super Method": "超方法",
|
||||
|
||||
Reference in New Issue
Block a user