Files
openidea/src/features/github/timeline/comments/suggestion.ts
T
l0ng-ai f2c620cceb fix(i18n): translate the GitHub pull request operations' progress and failure titles
The GitHub tool window's long operations (checking out a pull request,
fetching it for a diff, creating its worktree, applying or committing a
suggested change, resolving a thread) passed their banner title as an
untagged string, and the failure notification appended " failed" to it, so
the banner stayed English and the error read "Checking out #12失败". Each
operation now names a translated progress title and its own failure title.
2026-11-04 12:01:50 +08:00

121 lines
5.1 KiB
TypeScript

/**
* Applying a suggested change locally (`GHPRReviewCommentBodyViewModel.applySuggestionLocally`
* / `commitSuggestion`, `GHSuggestedChangeApplier`): the suggestion becomes a patch of the
* local file (`buildSuggestionPatch`) applied with `vcsApplyPatch`; "Commit…" then commits
* just the suggestion's lines of that file (a partial commit of its hunks). The thread is
* resolved afterwards when the viewer may. Progress shows in the tool window's banner.
*/
import { useEffect, useState } from "react";
import { useVcs } from "@/features/vcs/store";
import { call, commands } from "@/platform/ipc";
import { tx } from "@/platform/i18n";
import type { GHPullRequest, GHPullRequestReviewThread } from "../../api/types";
import { withBusy } from "../../data/local";
import { resolveThread } from "../../data/operations";
import { usePullRequestData } from "../../data/pullRequest";
import { connectedRoot } from "../../data/repository";
import { buildSuggestionPatch, type SuggestionTarget } from "../model";
/** The connected pull requests' local repository (the project's primary one when unknown). */
async function repositoryRoot(): Promise<string> {
const root = connectedRoot() ?? (await call(commands.vcsRoots()))[0]?.root;
if (!root) throw new Error("The project is not a Git repository");
return String(root).replace(/[\\/]+$/, "");
}
/** Applies `suggestion` to the local file; with `commitMessage`, commits it. Whether it succeeded. */
export async function applySuggestion(
number: number,
thread: Pick<GHPullRequestReviewThread, "id" | "viewerCanResolve">,
target: SuggestionTarget,
suggestion: readonly string[],
commitMessage?: string,
): Promise<boolean> {
const op = commitMessage
? { title: tx("Committing suggested change"), failure: tx("Couldn't commit the suggested change") }
: { title: tx("Applying suggested change"), failure: tx("Couldn't apply the suggested change") };
const done = await withBusy(op, async () => {
const root = await repositoryRoot();
const abs = `${root}/${target.path}`;
const text = await call(commands.fsReadText(abs));
const { patch, start, lines } = buildSuggestionPatch(text, target, suggestion);
const outcome = await call(commands.vcsApplyPatch(connectedRoot(), patch));
const files = outcome.conflicts.join(", ");
if (files) throw new Error(tx`Conflicts in ${files}`);
if (commitMessage) {
const head = (await call(commands.vcsFileAt(abs, "HEAD"))) ?? "";
const disk = await call(commands.fsReadText(abs));
const end = start + Math.max(lines, 1);
const hunks = (await commands.vcsDiffHunks(head, disk))
.filter((h) => h.start < end && h.start + Math.max(h.lines, 1) > start)
.map(({ start, lines, oldStart, oldLines }) => ({ start, lines, oldStart, oldLines }));
if (!hunks.length) throw new Error(tx`${target.path}: nothing to commit`);
await call(
commands.vcsCommit(connectedRoot(), commitMessage, {
amend: false,
signOff: false,
author: null,
runHooks: true,
partial: [{ path: abs, hunks }],
}),
);
}
return true;
});
if (!done) return false;
if (thread.viewerCanResolve)
await withBusy({ title: tx("Resolving thread"), failure: tx("Couldn't resolve the thread") }, () =>
resolveThread(number, thread.id),
);
return true;
}
let branchCache: {
at: number;
version: number;
value: Promise<{ head: string | null; upstream: string | null; commit: string | null }>;
} | null = null;
function branchInfo(version: number) {
if (!branchCache || branchCache.version !== version || Date.now() - branchCache.at > 2000) {
branchCache = {
at: Date.now(),
version,
value: call(commands.vcsBranchInfo(connectedRoot())).then(
(b) => ({ head: b.head, upstream: b.upstream, commit: b.headCommit }),
() => ({ head: null, upstream: null, commit: null }),
),
};
}
return branchCache.value;
}
/** Whether the checked-out branch is the pull request's (it tracks its head branch, or is at its head). */
export function isReviewBranch(
pr: Pick<GHPullRequest, "headRefName" | "headRefOid">,
b: { head: string | null; upstream: string | null; commit: string | null },
): boolean {
if (b.commit && b.commit === pr.headRefOid) return true;
if (b.upstream) return b.upstream.slice(b.upstream.indexOf("/") + 1) === pr.headRefName;
return b.head === pr.headRefName;
}
/** `isOnReviewBranch`, checked again when the repository changes or the window regains focus; null while unknown. */
export function useOnReviewBranch(number: number): boolean | null {
const pr = usePullRequestData(number, "details").value;
const version = useVcs((s) => s.version);
const [value, setValue] = useState<boolean | null>(null);
useEffect(() => {
if (!pr) return;
let alive = true;
const check = () => void branchInfo(version).then((b) => alive && setValue(isReviewBranch(pr, b)));
check();
window.addEventListener("focus", check);
return () => {
alive = false;
window.removeEventListener("focus", check);
};
}, [pr, version]);
return value;
}