diff --git a/src/components/CopyButton.svelte b/src/components/CopyButton.svelte new file mode 100644 index 000000000..5d42b50de --- /dev/null +++ b/src/components/CopyButton.svelte @@ -0,0 +1,46 @@ + + + + + diff --git a/src/components/DiffRow.svelte b/src/components/DiffRow.svelte index b2869f063..1777ba9e3 100644 --- a/src/components/DiffRow.svelte +++ b/src/components/DiffRow.svelte @@ -4,7 +4,9 @@ import escape from "lodash/escape"; import type { DiffRow } from "@app/lib/diffRows"; + import { fileDiffToText } from "@app/lib/diffText"; + import CopyButton from "@app/components/CopyButton.svelte"; import Icon from "@app/components/Icon.svelte"; import Path from "@app/components/Path.svelte"; @@ -191,6 +193,7 @@ -{row.file.diff.stats.deletions} {/if} + fileDiffToText(row.file)} title="Copy file diff" /> {:else if row.type === "hunk-header"}
diff --git a/src/lib/diffText.ts b/src/lib/diffText.ts new file mode 100644 index 000000000..22095d42b --- /dev/null +++ b/src/lib/diffText.ts @@ -0,0 +1,40 @@ +import type { Diff } from "@bindings/diff/Diff"; +import type { FileDiff } from "@bindings/diff/FileDiff"; + +// Selection across off-screen lines isn't possible once the diff is +// virtualized, so these build copyable unified-diff text straight from the +// model for the "copy file" / "copy diff" buttons. + +function filePath(file: FileDiff): string { + if (file.status === "moved" || file.status === "copied") { + return `${file.oldPath} → ${file.newPath}`; + } + return file.path; +} + +// Diff line/header text from the backend keeps its trailing newline; strip it +// so join("\n") doesn't double every line. +function stripEol(s: string): string { + return s.replace(/\r?\n$/, ""); +} + +export function fileDiffToText(file: FileDiff): string { + const lines: string[] = [filePath(file)]; + if (file.diff.type === "plain") { + for (const hunk of file.diff.hunks) { + lines.push(stripEol(hunk.header)); + for (const m of hunk.lines) { + const prefix = + m.type === "addition" ? "+" : m.type === "deletion" ? "-" : " "; + lines.push(`${prefix}${stripEol(m.line)}`); + } + } + } else if (file.diff.type === "binary") { + lines.push("Binary file"); + } + return lines.join("\n"); +} + +export function diffToText(diff: Diff): string { + return diff.files.map(fileDiffToText).join("\n\n"); +} diff --git a/src/views/repo/RepoCommit.svelte b/src/views/repo/RepoCommit.svelte index a09afbd4e..9a6475b92 100644 --- a/src/views/repo/RepoCommit.svelte +++ b/src/views/repo/RepoCommit.svelte @@ -3,6 +3,7 @@ import type { Commit } from "@bindings/repo/Commit"; import type { RepoInfo } from "@bindings/repo/RepoInfo"; + import { diffToText } from "@app/lib/diffText"; import * as router from "@app/lib/router"; import { absoluteTimestamp, @@ -14,6 +15,7 @@ } from "@app/lib/utils"; import Changeset from "@app/components/Changeset.svelte"; + import CopyButton from "@app/components/CopyButton.svelte"; import DiffStatBadge from "@app/components/DiffStatBadge.svelte"; import ExternalLink from "@app/components/ExternalLink.svelte"; import Icon from "@app/components/Icon.svelte"; @@ -227,6 +229,7 @@ {pluralize("file", diff.stats.filesChanged)} changed
+ diffToText(diff)} title="Copy diff" />