/** * 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 { 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, target: SuggestionTarget, suggestion: readonly string[], commitMessage?: string, ): Promise { 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, 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(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; }