дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(vcs): show GitHub commit checks in the Git Log (#1861)
Цей коміт міститься в:
1 джерело
0ac3f82dfb
коміт
5b6dd007c6
12 files changed
+480
-11
No files matched your search
@@ -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: "[email protected]",
|
||||
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: `[email protected]: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<string, string> };
|
||||
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();
|
||||
});
|
||||
@@ -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<LogCommitCheck["state"], { label: string; icon: keyof typeof STATUS_ICON }>;
|
||||
|
||||
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<HTMLButtonElement | null>;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [items, setItems] = useState<GHCommitStatusRollupContext[]>([]);
|
||||
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 (
|
||||
<ChooserPopup
|
||||
mode="single"
|
||||
aria-label={tx("GitHub Commit Checks")}
|
||||
anchor={anchor}
|
||||
placement="top-start"
|
||||
items={items}
|
||||
getKey={(context) => `${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<LogCommitCheck | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const anchor = useRef<HTMLButtonElement>(null);
|
||||
useEffect(() => {
|
||||
if (!isTauri() || !/^[a-f0-9]{40}$/i.test(revision)) return;
|
||||
let timer: ReturnType<typeof setTimeout> | 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 (
|
||||
<>
|
||||
<IconButton
|
||||
ref={anchor}
|
||||
size="small"
|
||||
icon={STATUS_ICON[status.icon]}
|
||||
title={status.label}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " " || e.key === "Enter") e.stopPropagation();
|
||||
}}
|
||||
onClick={() => setOpen(!open)}
|
||||
/>
|
||||
{open && (
|
||||
<CheckDetails
|
||||
key={`${check.target.owner}/${check.target.name}`}
|
||||
check={check}
|
||||
revision={revision}
|
||||
anchor={anchor}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -37,11 +37,13 @@ export function graphqlData<T>(text: string): T {
|
||||
}
|
||||
|
||||
/** Runs a document from `queries.ts` with `variables` (undefined values are dropped). */
|
||||
export async function graphql<T>(doc: string, variables: Record<string, unknown> = {}): Promise<T> {
|
||||
export async function graphql<T>(
|
||||
doc: string,
|
||||
variables: Record<string, unknown> = {},
|
||||
root: string | null = current?.root ?? null,
|
||||
): Promise<T> {
|
||||
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<T>(text);
|
||||
}
|
||||
|
||||
|
||||
@@ -181,13 +181,18 @@ export async function pullRequestTemplates(): Promise<GHRepositoryPullRequestTem
|
||||
* A page of the commit's status check rollup (`getRepositoryCommitStatusContexts`).
|
||||
* @public
|
||||
*/
|
||||
export async function commitStatusContextsPage(oid: string, cursor: string | null, pageSize = 100) {
|
||||
export async function commitStatusContextsPage(
|
||||
oid: string,
|
||||
cursor: string | null,
|
||||
pageSize = 100,
|
||||
target?: GHRepositoryCoordinates & { root: string },
|
||||
) {
|
||||
const data = await graphql<{
|
||||
repository: {
|
||||
object: { statusCheckRollup: { contexts: GHConnection<GHCommitStatusRollupContext> } | 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, { targets: Target[]; expires: number }>();
|
||||
|
||||
async function targets(root: string): Promise<Target[]> {
|
||||
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<string, { statusCheckRollup: { state: GHCommitStatusContextState } | null } | null> | 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<Listener>;
|
||||
started: boolean;
|
||||
}
|
||||
|
||||
const cache = new Map<string, CachedCheck>();
|
||||
const requests = new Map<string, Request>();
|
||||
let scheduled: ReturnType<typeof setTimeout> | 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<string, LogCommitCheck>();
|
||||
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<Listener>(), 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);
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => <CommitSignature commit={c} /> }]
|
||||
: []),
|
||||
...(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 ? <ChecksCell root={root} revision={c.id} /> : 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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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} 审查",
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in new issue
Заблокувати користувача