mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 17:02:17 +00:00
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.
121 lines
5.1 KiB
TypeScript
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;
|
|
}
|