From 5b6dd007c6f89e512355730842023f2bbe6c5021 Mon Sep 17 00:00:00 2001 From: Shohruh Ismoilov Date: Fri, 6 Nov 2026 21:26:42 +0500 Subject: [PATCH] fix(vcs): show GitHub commit checks in the Git Log (#1861) --- e2e/gitLogCommitChecks.spec.ts | 148 ++++++++++++++++++++ src/features/github/LogCommitChecks.tsx | 142 +++++++++++++++++++ src/features/github/api/client.ts | 10 +- src/features/github/api/requests.ts | 13 +- src/features/github/data/logCommitChecks.ts | 127 +++++++++++++++++ src/features/github/index.ts | 2 + src/features/vcs/GitLog.tsx | 30 +++- src/features/vcs/LogSettingsPage.tsx | 1 + src/features/vcs/logOptions.ts | 2 + src/platform/locales/text/en.json | 3 + src/platform/locales/text/zh.json | 3 + src/workbench/contributions.ts | 10 ++ 12 files changed, 480 insertions(+), 11 deletions(-) create mode 100644 e2e/gitLogCommitChecks.spec.ts create mode 100644 src/features/github/LogCommitChecks.tsx create mode 100644 src/features/github/data/logCommitChecks.ts diff --git a/e2e/gitLogCommitChecks.spec.ts b/e2e/gitLogCommitChecks.spec.ts new file mode 100644 index 000000000..9a0a65cac --- /dev/null +++ b/e2e/gitLogCommitChecks.spec.ts @@ -0,0 +1,148 @@ +import { expect, ipcCalls, stubDesktop, test } from "./fixtures"; + +test("shows GitHub check statuses for each commit's repository", async ({ workbench: page }) => { + await page.clock.install(); + await stubDesktop(page); + await page.evaluate(async () => { + const primary = "/dev/acme-api"; + const linked = "/dev/acme-web"; + const tip = "1".repeat(40); + const pending = "2".repeat(40); + const missing = "3".repeat(40); + const h = window.__ipc.handlers; + h.vcs_roots = () => + [ + { root: primary, head: "main" }, + { root: linked, head: "release" }, + ].map((repo) => ({ ...repo, operation: null, headCommit: tip })); + h.vcs_branch_info = ({ repo }) => ({ + head: repo === linked ? "release" : "main", + headCommit: tip, + upstream: null, + ahead: 0, + behind: 0, + operation: null, + }); + h.vcs_branches = ({ repo }) => + [repo === linked ? "origin/dev" : "origin/other"].map((name) => ({ + name, + commit: tip, + remote: true, + current: false, + upstream: null, + ahead: 0, + behind: 0, + lastCommitMs: 0, + })); + h.vcs_log = ({ repo }) => { + const rows: [string, string][] = + repo === linked + ? [[tip, "Linked repository commit"]] + : [ + [tip, "Primary repository commit"], + [pending, "Pending commit"], + [missing, "Local-only commit"], + ]; + return rows.map(([id, summary], i) => ({ + id, + shortId: id.slice(0, 8), + parents: [], + refs: i === 0 ? [repo === linked ? "origin/dev" : "origin/other"] : [], + author: "A", + email: "a@example.com", + timeMs: 0, + summary, + message: summary, + })); + }; + h.vcs_log_refs = () => ({}); + h.vcs_log_details_batch = ({ revisions }) => + (revisions as string[]).map(() => ({ + message: "Linked repository commit", + containingBranches: [], + signature: "N", + signer: "", + key: "", + fingerprint: "", + })); + h.vcs_commit_files = () => []; + h.vcs_config_get = () => null; + h.vcs_tags = () => []; + h.vcs_status = () => []; + h.vcs_remotes = ({ repo }) => [ + { name: "origin", url: `git@github.com:example/${repo === linked ? "web" : "api"}.git` }, + ]; + let pendingQueries = 0; + h.gh_api = ({ repo, request }) => { + const { variables } = JSON.parse((request as { body: string }).body) as { variables: Record }; + const linkedRepo = repo === linked; + const state = linkedRepo ? "FAILURE" : "SUCCESS"; + if (variables.oid) + return JSON.stringify({ + data: { + repository: { + object: { + statusCheckRollup: { + contexts: { + nodes: [{ __typename: "StatusContext", context: "Frontend", state, targetUrl: null }], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }, + }, + }, + }, + }); + const statuses = Object.entries(variables) + .filter(([key]) => /^oid\d+$/.test(key)) + .map(([key, revision]) => [ + key.replace("oid", "c"), + revision === missing + ? null + : { statusCheckRollup: { state: revision === pending && ++pendingQueries === 1 ? "PENDING" : state } }, + ]); + return JSON.stringify({ data: { repository: Object.fromEntries(statuses) } }); + }; + const vcsModule = "/src/features/vcs/store.ts"; + const { useVcs, refreshNow } = await import(/* @vite-ignore */ vcsModule); + useVcs.setState({ available: true, root: primary, repo: primary }); + await refreshNow(); + const contextModule = "/src/platform/context.ts"; + const { setContext } = await import(/* @vite-ignore */ contextModule); + setContext("vcsAvailable", true); + const actionsModule = "/src/platform/actions.ts"; + const { executeAction } = await import(/* @vite-ignore */ actionsModule); + await executeAction("vcs.showLog"); + }); + + const primary = page.getByRole("option", { name: /Primary repository commit/ }); + const linked = page.getByRole("option", { name: /Linked repository commit/ }); + await expect(primary).toBeVisible(); + expect(await ipcCalls(page, "gh_api")).toEqual([]); + await page.getByRole("button", { name: "View Options" }).first().click(); + await page.getByRole("menuitem", { name: "Columns", exact: true }).hover(); + await page.getByRole("menuitemcheckbox", { name: "GitHub Commit Checks" }).click({ timeout: 4000 }); + await expect(primary.getByRole("button", { name: "Checks passed" })).toBeVisible(); + await expect(linked.getByRole("button", { name: "Checks failed" })).toBeVisible(); + await expect( + page.getByRole("option", { name: /Pending commit/ }).getByRole("button", { name: "Checks in progress…" }), + ).toBeVisible(); + await expect( + page.getByRole("option", { name: /Local-only commit/ }).getByRole("button", { name: /^Checks / }), + ).toHaveCount(0); + + const calls = await ipcCalls(page, "gh_api"); + expect(calls.map((c) => c.repo).sort()).toEqual(["/dev/acme-api", "/dev/acme-web"]); + expect(calls.map((c) => JSON.parse((c.request as { body: string }).body).variables.repoName).sort()).toEqual([ + "api", + "web", + ]); + await linked.getByRole("button", { name: "Checks failed" }).click(); + await expect(page.getByRole("option", { name: /Frontend/ })).toBeVisible(); + expect((await ipcCalls(page, "gh_api")).at(-1)).toMatchObject({ repo: "/dev/acme-web" }); + + await page.keyboard.press("Escape"); + await page.clock.fastForward(31_000); + await expect( + page.getByRole("option", { name: /Pending commit/ }).getByRole("button", { name: "Checks passed" }), + ).toBeVisible(); +}); diff --git a/src/features/github/LogCommitChecks.tsx b/src/features/github/LogCommitChecks.tsx new file mode 100644 index 000000000..51004f139 --- /dev/null +++ b/src/features/github/LogCommitChecks.tsx @@ -0,0 +1,142 @@ +import { useEffect, useRef, useState, type RefObject } from "react"; +import { isTauri } from "@/platform/ipc"; +import { tx } from "@/platform/i18n"; +import { IconButton } from "@/ui"; +import { commitStatusContextsPage } from "./api/requests"; +import type { GHCommitStatusRollupContext } from "./api/types"; +import { watchLogCommitCheck, type LogCommitCheck } from "./data/logCommitChecks"; +import { openInBrowser } from "./data/local"; +import { STATUS_ICON } from "./details/model"; +import { ChooserPopup } from "./ui/ChooserPopup"; + +const statuses = { + SUCCESS: { label: "Checks passed", icon: "success" }, + FAILURE: { label: "Checks failed", icon: "failed" }, + ERROR: { label: "Checks failed", icon: "failed" }, + PENDING: { label: "Checks in progress…", icon: "pending" }, + EXPECTED: { label: "Checks in progress…", icon: "pending" }, +} as const satisfies Record; + +function checkPresentation(context: GHCommitStatusRollupContext) { + if (context.__typename === "StatusContext") { + return { text: context.context, icon: STATUS_ICON[statuses[context.state].icon] }; + } + const conclusion = context.conclusion; + const icon = + conclusion === "SUCCESS" + ? "success" + : conclusion === "NEUTRAL" || conclusion === "SKIPPED" + ? "skipped" + : conclusion + ? "failed" + : "pending"; + return { text: context.name, detail: context.checkSuite?.workflowRun?.workflow.name, icon: STATUS_ICON[icon] }; +} + +const checkUrl = (context: GHCommitStatusRollupContext) => + context.__typename === "CheckRun" ? context.url : context.targetUrl; + +function CheckDetails({ + check, + revision, + anchor, + onClose, +}: { + check: LogCommitCheck; + revision: string; + anchor: RefObject; + onClose: () => void; +}) { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const { root, owner, name } = check.target; + useEffect(() => { + let active = true; + void (async () => { + let cursor: string | null = null; + do { + const page = await commitStatusContextsPage(revision, cursor, 100, { root, owner, name }); + if (!active || !page) break; + setItems((prev) => [...prev, ...page.nodes]); + cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null; + } while (cursor); + })() + .catch(() => active && setError(true)) + .finally(() => active && setLoading(false)); + return () => { + active = false; + }; + }, [revision, root, owner, name]); + return ( + `${checkPresentation(context).text}\0${checkUrl(context) ?? ""}`} + present={checkPresentation} + onChoose={(context) => { + const url = checkUrl(context); + if (url && /^https?:\/\//i.test(url)) void openInBrowser(url); + }} + loading={loading} + error={error ? tx("Could not load commit checks") : null} + height={Math.min(300, 40 + Math.max(1, items.length) * 28)} + onClose={onClose} + /> + ); +} + +export function LogCommitChecks({ root, revision }: { root: string; revision: string }) { + const [check, setCheck] = useState(null); + const [open, setOpen] = useState(false); + const anchor = useRef(null); + useEffect(() => { + if (!isTauri() || !/^[a-f0-9]{40}$/i.test(revision)) return; + let timer: ReturnType | undefined; + let unsubscribe: (() => void) | undefined; + const load = () => { + unsubscribe?.(); + unsubscribe = watchLogCommitCheck(root, revision, ({ check, expires }) => { + setCheck(check); + if (check) timer = setTimeout(load, Math.max(0, expires - Date.now())); + }); + }; + load(); + return () => { + clearTimeout(timer); + unsubscribe?.(); + }; + }, [root, revision]); + + if (!check) return null; + const status = statuses[check.state]; + return ( + <> + e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === " " || e.key === "Enter") e.stopPropagation(); + }} + onClick={() => setOpen(!open)} + /> + {open && ( + setOpen(false)} + /> + )} + + ); +} diff --git a/src/features/github/api/client.ts b/src/features/github/api/client.ts index 184f45766..2fdef1270 100644 --- a/src/features/github/api/client.ts +++ b/src/features/github/api/client.ts @@ -37,11 +37,13 @@ export function graphqlData(text: string): T { } /** Runs a document from `queries.ts` with `variables` (undefined values are dropped). */ -export async function graphql(doc: string, variables: Record = {}): Promise { +export async function graphql( + doc: string, + variables: Record = {}, + root: string | null = current?.root ?? null, +): Promise { const body = JSON.stringify({ query: withFragments(doc), variables: definedOnly(variables) }); - const text = await call( - commands.ghApi(current?.root ?? null, { method: "POST", endpoint: "graphql", body, paginate: false }), - ); + const text = await call(commands.ghApi(root, { method: "POST", endpoint: "graphql", body, paginate: false })); return graphqlData(text); } diff --git a/src/features/github/api/requests.ts b/src/features/github/api/requests.ts index 4cc75e34f..72637a9e0 100644 --- a/src/features/github/api/requests.ts +++ b/src/features/github/api/requests.ts @@ -181,13 +181,18 @@ export async function pullRequestTemplates(): Promise } | null } | null; - }; - }>(Q.getRepositoryCommitStatusContexts, { ...repoVariables(), oid, pageSize, cursor }); - return data.repository.object?.statusCheckRollup?.contexts ?? null; + } | null; + }>(Q.getRepositoryCommitStatusContexts, { ...repoVariables(target), oid, pageSize, cursor }, target?.root); + return data.repository?.object?.statusCheckRollup?.contexts ?? null; } /** diff --git a/src/features/github/data/logCommitChecks.ts b/src/features/github/data/logCommitChecks.ts new file mode 100644 index 000000000..6850ec479 --- /dev/null +++ b/src/features/github/data/logCommitChecks.ts @@ -0,0 +1,127 @@ +/** Fetch rollups only for mounted log rows. Each GraphQL batch is scoped to one Git root. */ +import { call, commands } from "@/platform/ipc"; +import { graphql, repoVariables } from "../api/client"; +import type { GHCommitStatusContextState, GHRepositoryCoordinates } from "../api/types"; +import { githubRemotes } from "./repository"; + +type Target = GHRepositoryCoordinates & { root: string }; +export interface LogCommitCheck { + state: GHCommitStatusContextState; + target: Target; +} + +const BATCH_SIZE = 20; +const CACHE_SIZE = 2_000; +const REMOTE_TTL = 5 * 60_000; +const remoteCache = new Map(); + +async function targets(root: string): Promise { + const previous = remoteCache.get(root); + if (previous && previous.expires > Date.now()) return previous.targets; + const remotes = githubRemotes(await call(commands.vcsRemotes(root)).catch(() => []), root); + remotes.sort((a, b) => Number(b.remote === "origin") - Number(a.remote === "origin")); + const resolved = remotes.map(({ owner, name }) => ({ owner, name, root })); + if (remoteCache.size >= 128) remoteCache.delete(remoteCache.keys().next().value!); + remoteCache.set(root, { targets: resolved, expires: Date.now() + REMOTE_TTL }); + return resolved; +} + +async function queryBatch(target: Target, revisions: string[]) { + const fields = revisions + .map((_, i) => `c${i}: object(oid: $oid${i}) { ... on Commit { statusCheckRollup { state } } }`) + .join("\n"); + const params = revisions.map((_, i) => `$oid${i}: GitObjectID!`).join(", "); + const doc = `query($repoOwner: String!, $repoName: String!, ${params}) { + repository(owner: $repoOwner, name: $repoName) { ${fields} } + }`; + const variables = Object.fromEntries(revisions.map((oid, i) => [`oid${i}`, oid])); + const data = await graphql<{ + repository: Record | null; + }>(doc, { ...repoVariables(target), ...variables }, target.root); + return revisions.map((_, i) => data.repository?.[`c${i}`]?.statusCheckRollup?.state ?? null); +} + +interface CachedCheck { + check: LogCommitCheck | null; + expires: number; +} + +type Listener = (result: CachedCheck) => void; +interface Request { + key: string; + root: string; + revision: string; + listeners: Set; + started: boolean; +} + +const cache = new Map(); +const requests = new Map(); +let scheduled: ReturnType | undefined; +let running = false; + +function isPendingCheck(check: LogCommitCheck | null): boolean { + return check?.state === "PENDING" || check?.state === "EXPECTED"; +} + +function schedule() { + if (running || scheduled !== undefined || !requests.size) return; + scheduled = setTimeout(() => { + scheduled = undefined; + void flush(); + }, 40); +} + +async function flush() { + const first = requests.values().next().value; + if (!first) return; + const batch = [...requests.values()].filter((r) => r.root === first.root).slice(0, BATCH_SIZE); + batch.forEach((r) => (r.started = true)); + running = true; + const results = new Map(); + try { + // Checks on an upstream can be absent from a fork, even when it contains the same commit. + for (const target of await targets(first.root)) { + const remaining = batch.filter((r) => r.listeners.size && !results.has(r.revision)); + if (!remaining.length) break; + const states = await queryBatch( + target, + remaining.map((r) => r.revision), + ).catch(() => []); + remaining.forEach(({ revision }, i) => { + const state = states[i]; + if (state) results.set(revision, { state, target }); + }); + } + } finally { + for (const r of batch) { + requests.delete(r.key); + const check = results.get(r.revision) ?? null; + if (cache.size >= CACHE_SIZE) cache.delete(cache.keys().next().value!); + const ttl = isPendingCheck(check) ? 30_000 : check ? 5 * 60_000 : 60_000; + const result = { check, expires: Date.now() + ttl }; + cache.set(r.key, result); + r.listeners.forEach((listener) => listener(result)); + } + running = false; + schedule(); + } +} + +export function watchLogCommitCheck(root: string, revision: string, listener: Listener): () => void { + const key = `${root}\0${revision}`; + const previous = cache.get(key); + if (previous && previous.expires > Date.now()) { + listener(previous); + return () => {}; + } + cache.delete(key); + const request = requests.get(key) ?? { key, root, revision, listeners: new Set(), started: false }; + request.listeners.add(listener); + requests.set(key, request); + schedule(); + return () => { + request.listeners.delete(listener); + if (!request.started && !request.listeners.size) requests.delete(key); + }; +} diff --git a/src/features/github/index.ts b/src/features/github/index.ts index 433aa314c..451a2cb3b 100644 --- a/src/features/github/index.ts +++ b/src/features/github/index.ts @@ -17,6 +17,7 @@ import { maybeEditorService, registerCloneSource, registerEditorKind, + registerGitLogChecks, registerOverlay, registerToolWindow, } from "@/workbench/contributions"; @@ -318,6 +319,7 @@ export function activate(): () => void { order: 10, component: lazyComponent(() => import("./clone/GitHubCloneSource").then((m) => m.GitHubCloneSource)), }), + registerGitLogChecks(lazyComponent(() => import("./LogCommitChecks").then((m) => m.LogCommitChecks))), registerToolWindow({ id: WINDOW, title: "Pull Requests", diff --git a/src/features/vcs/GitLog.tsx b/src/features/vcs/GitLog.tsx index d4a6c92e5..0a746dd69 100644 --- a/src/features/vcs/GitLog.tsx +++ b/src/features/vcs/GitLog.tsx @@ -23,7 +23,7 @@ import { useVirtualizer } from "@tanstack/react-virtual"; import type { Branch, ChangedFile, Commit } from "@/bindings"; import { call, commands } from "@/platform/ipc"; import { executeAction } from "@/platform/actions"; -import { maybeEditorService, type EditorInput } from "@/workbench/contributions"; +import { maybeEditorService, useGitLogChecks, type EditorInput } from "@/workbench/contributions"; import { EmptyState, Icon, @@ -81,7 +81,7 @@ import { import { logOptions, refLabels, useLogOptions, type LogOptions } from "./logOptions"; import { KIND_COLOR, baseName, dirName, fileCount, formatTime, relTo, type EffectiveKind } from "./model"; import { clearConsole, useVcsConsole } from "./ops"; -import { currentRepo, loadBranches, repoName, useVcs } from "./store"; +import { currentRepo, currentRepoRoot, loadBranches, repoName, useVcs } from "./store"; import { loadFavoriteBranches, useFavoriteBranches } from "./branchOps"; import { branchMenu, inRepo, tagMenu } from "./branchMenu"; import { @@ -1089,6 +1089,7 @@ export function viewOptionsMenu(o: LogOptions): MenuItem[] { toggle("Date", o, "showDate"), toggle("Hash", o, "showHash"), toggle("Signature", o, "showSignature"), + toggle("GitHub Commit Checks", o, "showGitHubChecks"), { kind: "separator" }, toggle("Column Headers", o, "showColumnHeaders"), ], @@ -1214,6 +1215,7 @@ function CommitList() { }); }, [all]); const opts = useLogOptions(); + const ChecksCell = useGitLogChecks((s) => s.cell); const bookmarks = useGitLog((s) => s.bookmarks); const fragments = useGitLog((s) => s.fragments); const { expanded: expandedFragments, collapsed: collapsedFragments, merges: expandedMerges } = fragments; @@ -1241,10 +1243,32 @@ function CommitList() { ...(opts.showSignature ? [{ id: "signature", title: "Signature", width: 100, render: (c: Commit) => }] : []), + ...(opts.showGitHubChecks && ChecksCell + ? [ + { + id: "github-checks", + title: "GitHub Commit Checks", + width: 44, + render: (c: Commit) => { + const root = commitRepo(c) ?? currentRepoRoot(); + return root && !(c as RepoCommit).readOnly ? : null; + }, + }, + ] + : []), ]; const order = [...new Set([...opts.columnOrder, ...builtins.map((c) => c.id)])]; return builtins.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id)); - }, [opts.showAuthor, opts.showDate, opts.showHash, opts.showSignature, opts.commitTimestamp, opts.columnOrder]); + }, [ + opts.showAuthor, + opts.showDate, + opts.showHash, + opts.showSignature, + opts.showGitHubChecks, + opts.commitTimestamp, + opts.columnOrder, + ChecksCell, + ]); const user = useGitLog((s) => s.user); const headCommit = useVcs((s) => s.branch?.headCommit ?? null); const branches = useVcs((s) => s.branches); diff --git a/src/features/vcs/LogSettingsPage.tsx b/src/features/vcs/LogSettingsPage.tsx index 35ff0ce31..8077a93a8 100644 --- a/src/features/vcs/LogSettingsPage.tsx +++ b/src/features/vcs/LogSettingsPage.tsx @@ -18,6 +18,7 @@ const COLUMNS: { key: Flag; label: string }[] = [ { key: "showAuthor", label: "Author" }, { key: "showHash", label: "Hash" }, { key: "showDate", label: "Date" }, + { key: "showGitHubChecks", label: "GitHub Commit Checks" }, ]; export function LogSettingsPage() { diff --git a/src/features/vcs/logOptions.ts b/src/features/vcs/logOptions.ts index 9d6f40b85..edb316c7f 100644 --- a/src/features/vcs/logOptions.ts +++ b/src/features/vcs/logOptions.ts @@ -22,6 +22,7 @@ export interface LogOptions { showDate: boolean; showHash: boolean; showSignature: boolean; + showGitHubChecks: boolean; /** The row naming the columns, where they are resized and reordered. */ showColumnHeaders: boolean; highlightMine: boolean; @@ -60,6 +61,7 @@ export const DEFAULT_LOG_OPTIONS: LogOptions = { showDate: true, showHash: false, showSignature: false, + showGitHubChecks: false, showColumnHeaders: false, highlightMine: true, highlightMerges: true, diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json index 22f787063..ca9d09aff 100644 --- a/src/platform/locales/text/en.json +++ b/src/platform/locales/text/en.json @@ -2,6 +2,9 @@ "Cannot copy reference": "Cannot copy reference", "Connect through:": "Connect through:", "Connect through": "Connect through", + "GitHub Commit Checks": "GitHub Commit Checks", + "Checks passed": "Checks passed", + "Could not load commit checks": "Could not load commit checks", "WSL distribution:": "WSL distribution:", "WSL distribution": "WSL distribution", "Project runs on {label}": "Project runs on {label}", diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index a19c1bd83..e3c92d6f2 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -5397,6 +5397,9 @@ "Checks in progress, and some already failed…": "检查正在进行,部分已失败…", "Checks in progress…": "检查正在进行…", "Checks failed": "检查失败", + "GitHub Commit Checks": "GitHub 提交检查", + "Checks passed": "检查通过", + "Could not load commit checks": "无法加载提交检查", "{name} approved review": "{name} 已批准审查", "{name} is waiting for updates": "{name} 正在等待更新", "Waiting for review from {name}": "正在等待 {name} 审查", diff --git a/src/workbench/contributions.ts b/src/workbench/contributions.ts index 8ee878f1e..72c159354 100644 --- a/src/workbench/contributions.ts +++ b/src/workbench/contributions.ts @@ -325,6 +325,16 @@ export const registerToolWindow = (c: ToolWindowContribution) => register("toolW export const registerStatusItem = (c: StatusItemContribution) => register("statusItems", c.id, c); export const registerOverlay = (c: OverlayContribution) => register("overlays", c.id, c); +type GitLogChecksCell = ComponentType<{ root: string; revision: string }>; +export const useGitLogChecks = create<{ cell: GitLogChecksCell | null }>(() => ({ cell: null })); + +export function registerGitLogChecks(cell: GitLogChecksCell): () => void { + useGitLogChecks.setState({ cell }); + return () => { + if (useGitLogChecks.getState().cell === cell) useGitLogChecks.setState({ cell: null }); + }; +} + const EditorShown = createContext(true); export const EditorShownProvider = EditorShown.Provider; /** Whether this editor tab is the one its pane shows (a `keepAlive` tab stays mounted when it isn't). */