perf(vcs): keep multi-root Git Log refresh bounded (#1554)

Refreshing the combined Git Log of several repositories merged every repository's prefetched candidates into the result, so 300 loaded rows could grow into thousands and the graph layout took seconds. A refresh now keeps the loaded window through its last surviving commit (plus new arrivals); older history comes in with paging as before. Commit selection no longer rebuilds the layout when linear-history collapsing is off, and current-branch highlighting uses the backend's reachability, falling back to a single traversal from all heads.

Co-authored-by: shohruh-genius <[email protected]>
Цей коміт міститься в:
Shohruh Ismoilovтаshohruh-genius authored and GitHub committed 2026-11-05 20:59:28 +08:00
1 джерело 7071fd9107
коміт 24cf1a52d5
5 files changed
+58 -13

No files matched your search

+11 -6
Переглянути файл
@@ -1263,16 +1263,20 @@ function CommitList() {
),
[all, heads, headCommit],
);
const keep = useMemo(
() =>
opts.collapseLinear || fragments.collapsed.size
? new Set([...headKeys, ...selection, ...Object.keys(bookmarks)])
: null,
[opts.collapseLinear, fragments.collapsed, headKeys, selection, bookmarks],
);
const {
commits,
rows,
linear,
collapsed,
graph: displayedGraph,
} = useMemo(
() => presentLogGraph(all, opts, fragments, new Set([...headKeys, ...selection, ...Object.keys(bookmarks)])),
[all, opts, fragments, headKeys, selection, bookmarks],
);
} = useMemo(() => presentLogGraph(all, opts, fragments, keep ?? new Set()), [all, opts, fragments, keep]);
const { edges: highlighted, nodes: highlightedNodes } = useMemo(() => {
const empty = { edges: new Set<string>(), nodes: new Set<string>() };
if (!hoverColor) return empty;
@@ -1321,8 +1325,9 @@ function CommitList() {
const onlyCurrent = filterBranches.length > 0 && filterBranches.every((b) => b === "HEAD" || b === currentBranch);
const onBranch = useMemo(() => {
if (!opts.highlightCurrentBranch || onlyCurrent) return null;
const reach = new Set<string>();
for (const head of headKeys) for (const id of reachableFrom(all.map(graphCommit), head)) reach.add(id);
const reach = all.some((c) => c.onCurrentBranch == null)
? reachableFrom(all.map(graphCommit), headKeys)
: new Set<string>();
for (const c of all) {
if (c.onCurrentBranch === true) reach.add(commitKey(c));
else if (c.onCurrentBranch === false) reach.delete(commitKey(c));
+3 -2
Переглянути файл
@@ -90,8 +90,9 @@ describe("long edges", () => {
describe("highlighting", () => {
it("finds the loaded commits of the current branch", () => {
const commits = [c("s", ["a"]), c("h", ["a"]), c("a", ["r"]), c("r")];
expect([...reachableFrom(commits, "h")].sort()).toEqual(["a", "h", "r"]);
expect(reachableFrom(commits, null).size).toBe(0);
expect([...reachableFrom(commits, ["h"])].sort()).toEqual(["a", "h", "r"]);
expect([...reachableFrom(commits, ["h", "s"])].sort()).toEqual(["a", "h", "r", "s"]);
expect(reachableFrom(commits, []).size).toBe(0);
});
it("matches my commits by email or name", () => {
+33
Переглянути файл
@@ -249,6 +249,39 @@ describe("Git log background refresh", () => {
expect(useGitLog.getState().commits.map((c) => c.id)).toEqual(history.slice(0, PAGE * 3).map((c) => c.id));
});
it("keeps the loaded multi-root window on refresh without emitting older candidates", async () => {
const roots = useVcs.getState().roots;
const history = Object.fromEntries(
roots.map((repo, root) => [
repo,
Array.from({ length: 180 }, (_, i) => commit(`${root}-${i}`, 1000 - i * 2 - root)),
]),
);
log.mockImplementation(async (repo: string, q: LogQuery) => ({
status: "ok",
data: history[repo]!.slice(q.skip, q.skip + q.limit).map((c, i) => ({
...c,
logPosition: q.skip + i,
logTotal: history[repo]!.length,
})),
}));
await reloadLog();
const loaded = useGitLog.getState().commits.map((c) => c.id);
expect(loaded).toHaveLength(PAGE);
history[roots[0]!]!.unshift(commit("new", 2000));
await reloadLog({ preserve: true });
expect(useGitLog.getState().commits.map((c) => c.id)).toEqual(["new", ...loaded]);
expect(useGitLog.getState().done).toBe(false);
await loadMore();
const expected = Object.values(history)
.flat()
.sort((a, b) => (b.timeMs ?? 0) - (a.timeMs ?? 0));
expect(useGitLog.getState().commits.map((c) => c.id)).toEqual(expected.map((c) => c.id));
expect(useGitLog.getState().done).toBe(true);
});
it("retains the current history if refreshing fails", async () => {
useVcs.setState({ roots: ["/project/front"] });
log.mockResolvedValue({ status: "ok", data: [commit("old")] });
+4 -5
Переглянути файл
@@ -264,14 +264,13 @@ export function collapseLinear<
return { commits: visible, graph };
}
/** Ids of the loaded commits reachable from `head` (log order: children before parents). */
/** Ids reachable from the given heads (log order: children before parents). */
export function reachableFrom(
commits: ReadonlyArray<{ id: string; parents: readonly string[] }>,
head: string | null,
heads: Iterable<string>,
): Set<string> {
const out = new Set<string>();
if (!head) return out;
out.add(head);
const out = new Set(heads);
if (!out.size) return out;
for (const c of commits) if (out.has(c.id)) for (const p of c.parents) out.add(p);
return out;
}
+7
Переглянути файл
@@ -994,6 +994,13 @@ function loadPage(replace: boolean, id: string, before = false): Promise<void> {
before ? streams.map((stream, i) => [...stream, ...(currentStreams[i] ?? [])]) : streams,
before || joined || s.seek || (replace && s.commits.length) ? Number.MAX_SAFE_INTEGER : limit,
);
if (replace && roots.length > 1 && s.commits.length && !s.seek) {
// Keep the loaded window through its last surviving commit. Older candidates
// from each repository are fetched again when paging.
const loadedKeys = new Set(s.commits.map(commitKey));
const last = page.findLastIndex((c) => loadedKeys.has(commitKey(c)));
page.length = Math.min(page.length, Math.max(limit, last + 1));
}
const cur = logState(id);
if (cur.generation !== gen) return;
if (replace && results.length > 0 && failures.length === results.length && cur.commits.length) {