@riebeckite/plugin-diff 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/README_ja.md +4 -2
- package/dist/client.js +107 -8
- package/dist/index.js +175 -81
- package/dist/src/components/diff-history.d.ts +3 -3
- package/dist/src/git/history_reader.d.ts +13 -0
- package/dist/src/git/repo_paths.d.ts +76 -0
- package/dist/src/git/run_git.d.ts +43 -0
- package/dist/src/types.d.ts +6 -0
- package/package.json +3 -3
- package/style.css +98 -0
package/README.md
CHANGED
|
@@ -28,6 +28,9 @@ export default defineConfig({
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
`diff(options?)` accepts the same `GitHistoryReaderOptions` as the API.
|
|
31
|
+
The revision panel renders the initial comparison during the build and computes
|
|
32
|
+
other selected comparisons in the browser. It embeds each available revision
|
|
33
|
+
once rather than every pairwise diff, keeping generated pages compact.
|
|
31
34
|
|
|
32
35
|
## Programmatic API
|
|
33
36
|
|
|
@@ -61,7 +64,7 @@ const compare = await api.compareRevisions({
|
|
|
61
64
|
|
|
62
65
|
| Option | Type | Default | Description |
|
|
63
66
|
| ------ | ---- | ------- | ----------- |
|
|
64
|
-
| `cwd` | `string` | `process.cwd()` |
|
|
67
|
+
| `cwd` | `string` | `config.content.directory`, else `process.cwd()` | Content root used to locate the Git work tree |
|
|
65
68
|
|
|
66
69
|
When `cwd` is not inside a Git repository, API calls resolve to empty results
|
|
67
70
|
(`[]` / `null`) instead of throwing.
|
|
@@ -87,4 +90,4 @@ When `cwd` is not inside a Git repository, API calls resolve to empty results
|
|
|
87
90
|
|
|
88
91
|
## See also
|
|
89
92
|
|
|
90
|
-
- [Plugin guide](../../../docs/en/reference/plugin-api.md)
|
|
93
|
+
- [Plugin guide](../../../docs/en/docs/reference/plugin-api.md)
|
package/README_ja.md
CHANGED
|
@@ -28,7 +28,9 @@ const current = await api.getCurrentDiff("notes/hello.md");
|
|
|
28
28
|
| `getCurrentDiff(filePath)` | 最新と一つ前のリビジョンの差分 |
|
|
29
29
|
| `compareRevisions(input)` | 指定した二つのリビジョンの差分 |
|
|
30
30
|
|
|
31
|
-
`cwd` は Git
|
|
31
|
+
`cwd` は Git リポジトリを探すためのコンテンツルートで、既定値はビルド内の `config.content.directory`、それ以外では `process.cwd()` です。相対パスは `process.cwd()` 基準で解決されます。Git リポジトリ外を指定しても例外は投げず、空の結果を返します。
|
|
32
|
+
|
|
33
|
+
リビジョンパネルは初期表示の差分だけをビルド時に描画し、利用者が選んだ比較はブラウザ上で計算します。各リビジョンは一度だけ埋め込むため、全組み合わせの差分を出力する場合より生成ページを小さく保てます。
|
|
32
34
|
|
|
33
35
|
## 公開 API
|
|
34
36
|
|
|
@@ -38,4 +40,4 @@ const current = await api.getCurrentDiff("notes/hello.md");
|
|
|
38
40
|
|
|
39
41
|
## 関連資料
|
|
40
42
|
|
|
41
|
-
- [プラグインシステム](../../../docs/ja/reference/plugin-api.md)
|
|
43
|
+
- [プラグインシステム](../../../docs/ja/docs/reference/plugin-api.md)
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,92 @@ function escapeHtml(value) {
|
|
|
3
3
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
// src/diff/line_diff.ts
|
|
7
|
+
function createLineDiff(fromMarkdown, toMarkdown) {
|
|
8
|
+
const fromLines = splitLines(fromMarkdown);
|
|
9
|
+
const toLines = splitLines(toMarkdown);
|
|
10
|
+
const table = buildLongestCommonSubsequenceTable(fromLines, toLines);
|
|
11
|
+
const lines = [];
|
|
12
|
+
let fromIndex = 0;
|
|
13
|
+
let toIndex = 0;
|
|
14
|
+
let oldLineNumber = 1;
|
|
15
|
+
let newLineNumber = 1;
|
|
16
|
+
while (fromIndex < fromLines.length && toIndex < toLines.length) {
|
|
17
|
+
if (fromLines[fromIndex] === toLines[toIndex]) {
|
|
18
|
+
lines.push({
|
|
19
|
+
type: "context",
|
|
20
|
+
content: fromLines[fromIndex],
|
|
21
|
+
oldLineNumber,
|
|
22
|
+
newLineNumber
|
|
23
|
+
});
|
|
24
|
+
fromIndex += 1;
|
|
25
|
+
toIndex += 1;
|
|
26
|
+
oldLineNumber += 1;
|
|
27
|
+
newLineNumber += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (table[fromIndex + 1][toIndex] >= table[fromIndex][toIndex + 1]) {
|
|
31
|
+
lines.push({
|
|
32
|
+
type: "removed",
|
|
33
|
+
content: fromLines[fromIndex],
|
|
34
|
+
oldLineNumber,
|
|
35
|
+
newLineNumber: null
|
|
36
|
+
});
|
|
37
|
+
fromIndex += 1;
|
|
38
|
+
oldLineNumber += 1;
|
|
39
|
+
} else {
|
|
40
|
+
lines.push({
|
|
41
|
+
type: "added",
|
|
42
|
+
content: toLines[toIndex],
|
|
43
|
+
oldLineNumber: null,
|
|
44
|
+
newLineNumber
|
|
45
|
+
});
|
|
46
|
+
toIndex += 1;
|
|
47
|
+
newLineNumber += 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
while (fromIndex < fromLines.length) {
|
|
51
|
+
lines.push({
|
|
52
|
+
type: "removed",
|
|
53
|
+
content: fromLines[fromIndex],
|
|
54
|
+
oldLineNumber,
|
|
55
|
+
newLineNumber: null
|
|
56
|
+
});
|
|
57
|
+
fromIndex += 1;
|
|
58
|
+
oldLineNumber += 1;
|
|
59
|
+
}
|
|
60
|
+
while (toIndex < toLines.length) {
|
|
61
|
+
lines.push({
|
|
62
|
+
type: "added",
|
|
63
|
+
content: toLines[toIndex],
|
|
64
|
+
oldLineNumber: null,
|
|
65
|
+
newLineNumber
|
|
66
|
+
});
|
|
67
|
+
toIndex += 1;
|
|
68
|
+
newLineNumber += 1;
|
|
69
|
+
}
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
function splitLines(markdown) {
|
|
73
|
+
if (!markdown) return [];
|
|
74
|
+
return markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
75
|
+
}
|
|
76
|
+
function buildLongestCommonSubsequenceTable(fromLines, toLines) {
|
|
77
|
+
const table = Array.from(
|
|
78
|
+
{ length: fromLines.length + 1 },
|
|
79
|
+
() => Array.from({ length: toLines.length + 1 }, () => 0)
|
|
80
|
+
);
|
|
81
|
+
for (let fromIndex = fromLines.length - 1; fromIndex >= 0; fromIndex -= 1) {
|
|
82
|
+
for (let toIndex = toLines.length - 1; toIndex >= 0; toIndex -= 1) {
|
|
83
|
+
table[fromIndex][toIndex] = fromLines[fromIndex] === toLines[toIndex] ? table[fromIndex + 1][toIndex + 1] + 1 : Math.max(
|
|
84
|
+
table[fromIndex + 1][toIndex],
|
|
85
|
+
table[fromIndex][toIndex + 1]
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return table;
|
|
90
|
+
}
|
|
91
|
+
|
|
6
92
|
// client/diff-viewer.ts
|
|
7
93
|
function initDiffHistory() {
|
|
8
94
|
for (const root of document.querySelectorAll(
|
|
@@ -13,7 +99,10 @@ function initDiffHistory() {
|
|
|
13
99
|
}
|
|
14
100
|
function initDiffHistoryRoot(root) {
|
|
15
101
|
const payload = readPayload(root);
|
|
16
|
-
if (!payload || payload.
|
|
102
|
+
if (!payload || payload.revisions.length === 0) return;
|
|
103
|
+
const revisions = new Map(
|
|
104
|
+
payload.revisions.map((revision) => [revision.hash, revision])
|
|
105
|
+
);
|
|
17
106
|
const fromSelect = root.querySelector(
|
|
18
107
|
"[data-rr-diff-from]"
|
|
19
108
|
);
|
|
@@ -25,7 +114,8 @@ function initDiffHistoryRoot(root) {
|
|
|
25
114
|
)) {
|
|
26
115
|
button.addEventListener("click", () => {
|
|
27
116
|
const toHash = button.dataset.rrDiffSelect;
|
|
28
|
-
|
|
117
|
+
if (!toHash) return;
|
|
118
|
+
const diff = createDiff(revisions, fromSelect.value, toHash);
|
|
29
119
|
if (!diff) return;
|
|
30
120
|
fromSelect.value = diff.from?.hash ?? "";
|
|
31
121
|
toSelect.value = diff.to.hash;
|
|
@@ -34,9 +124,7 @@ function initDiffHistoryRoot(root) {
|
|
|
34
124
|
});
|
|
35
125
|
}
|
|
36
126
|
const updateFromSelects = () => {
|
|
37
|
-
const diff =
|
|
38
|
-
(entry) => (entry.from?.hash ?? "") === fromSelect.value && entry.to.hash === toSelect.value
|
|
39
|
-
);
|
|
127
|
+
const diff = createDiff(revisions, fromSelect.value, toSelect.value);
|
|
40
128
|
if (!diff) return;
|
|
41
129
|
updateSelectedCommit(root, diff.to.hash);
|
|
42
130
|
renderPanel(panel, diff);
|
|
@@ -44,6 +132,17 @@ function initDiffHistoryRoot(root) {
|
|
|
44
132
|
fromSelect.addEventListener("change", updateFromSelects);
|
|
45
133
|
toSelect.addEventListener("change", updateFromSelects);
|
|
46
134
|
}
|
|
135
|
+
function createDiff(revisions, fromHash, toHash) {
|
|
136
|
+
const to = revisions.get(toHash);
|
|
137
|
+
if (!to) return null;
|
|
138
|
+
const from = fromHash ? revisions.get(fromHash) : null;
|
|
139
|
+
if (fromHash && !from) return null;
|
|
140
|
+
return {
|
|
141
|
+
from: from ?? null,
|
|
142
|
+
to,
|
|
143
|
+
lines: createLineDiff(from?.markdown ?? "", to.markdown)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
47
146
|
function readPayload(root) {
|
|
48
147
|
const script = root.querySelector(
|
|
49
148
|
"[data-rr-diff-history-data]"
|
|
@@ -67,14 +166,14 @@ function updateSelectedCommit(root, hash) {
|
|
|
67
166
|
}
|
|
68
167
|
function renderPanel(panel, diff) {
|
|
69
168
|
const fromLabel = diff.from?.shortHash ?? "Beginning";
|
|
70
|
-
panel.innerHTML = `<
|
|
71
|
-
<
|
|
169
|
+
panel.innerHTML = `<details class="rr-diff-history__diff" data-rr-diff-rendered open>
|
|
170
|
+
<summary class="rr-diff-history__diff-head">${escapeHtml(fromLabel)} → ${escapeHtml(diff.to.shortHash)}</summary>
|
|
72
171
|
<div class="rr-diff-history__table-wrap">
|
|
73
172
|
<table class="rr-diff-history__table">
|
|
74
173
|
<tbody>${diff.lines.map(renderLine).join("")}</tbody>
|
|
75
174
|
</table>
|
|
76
175
|
</div>
|
|
77
|
-
</
|
|
176
|
+
</details>`;
|
|
78
177
|
}
|
|
79
178
|
function renderLine(line) {
|
|
80
179
|
const prefix = line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
|
package/dist/index.js
CHANGED
|
@@ -54,14 +54,14 @@ function renderDiffViewer({
|
|
|
54
54
|
}
|
|
55
55
|
function renderDiffPanel(diff2) {
|
|
56
56
|
const fromLabel = diff2.from?.shortHash ?? "Beginning";
|
|
57
|
-
return `<
|
|
58
|
-
<
|
|
57
|
+
return `<details class="rr-diff-history__diff" data-rr-diff-rendered open>
|
|
58
|
+
<summary class="rr-diff-history__diff-head">${escapeHtml(fromLabel)} → ${escapeHtml(diff2.to.shortHash)}</summary>
|
|
59
59
|
<div class="rr-diff-history__table-wrap">
|
|
60
60
|
<table class="rr-diff-history__table">
|
|
61
61
|
<tbody>${diff2.lines.map((line) => renderDiffLine({ line })).join("")}</tbody>
|
|
62
62
|
</table>
|
|
63
63
|
</div>
|
|
64
|
-
</
|
|
64
|
+
</details>`;
|
|
65
65
|
}
|
|
66
66
|
function renderRevisionSelect(kind, history, value) {
|
|
67
67
|
const emptyOption = kind === "from" ? `<option value="">Beginning</option>` : "";
|
|
@@ -72,23 +72,29 @@ function renderRevisionSelect(kind, history, value) {
|
|
|
72
72
|
|
|
73
73
|
// src/components/diff-history.tsx
|
|
74
74
|
function renderDiffHistory(input) {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
75
|
+
const history = input.revisions;
|
|
76
|
+
const selected = input.selected;
|
|
77
|
+
const payload = JSON.stringify({ revisions: input.revisions }).replace(
|
|
78
|
+
/</g,
|
|
79
|
+
"\\u003c"
|
|
80
|
+
);
|
|
77
81
|
return `<section class="rr-diff-history" data-rr-diff-history>
|
|
78
|
-
<
|
|
82
|
+
<details class="rr-diff-history__panel" open>
|
|
83
|
+
<summary class="rr-diff-history__header">
|
|
79
84
|
<h2 class="rr-diff-history__title">History</h2>
|
|
80
|
-
<span class="rr-diff-history__count">${
|
|
81
|
-
</
|
|
82
|
-
${
|
|
85
|
+
<span class="rr-diff-history__header-meta"><span class="rr-diff-history__count">${history.length} changes</span><span class="rr-diff-history__toggle-label rr-diff-history__toggle-label--open">Collapse</span><span class="rr-diff-history__toggle-label rr-diff-history__toggle-label--closed">Expand</span><span class="rr-diff-history__toggle" aria-hidden="true"><svg class="rr-diff-history__toggle-icon" viewBox="0 0 16 16" width="16" height="16" focusable="false"><path d="M5.2 3.4 10.8 8l-5.6 4.6" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/></svg></span></span>
|
|
86
|
+
</summary>
|
|
87
|
+
${history.length === 0 || !selected ? renderEmptyState() : renderContent(history, selected)}
|
|
88
|
+
</details>
|
|
83
89
|
<script type="application/json" data-rr-diff-history-data>${payload}</script>
|
|
84
90
|
</section>`;
|
|
85
91
|
}
|
|
86
|
-
function renderContent(
|
|
92
|
+
function renderContent(history, selected) {
|
|
87
93
|
return `<div class="rr-diff-history__layout">
|
|
88
94
|
<ol class="rr-diff-history__list" aria-label="Commit history">
|
|
89
|
-
${
|
|
95
|
+
${history.map((revision, index) => renderRevisionButton(revision, index === 0)).join("")}
|
|
90
96
|
</ol>
|
|
91
|
-
${renderDiffViewer({ history
|
|
97
|
+
${renderDiffViewer({ history, selected })}
|
|
92
98
|
</div>`;
|
|
93
99
|
}
|
|
94
100
|
function renderEmptyState() {
|
|
@@ -191,30 +197,131 @@ function buildLongestCommonSubsequenceTable(fromLines, toLines) {
|
|
|
191
197
|
}
|
|
192
198
|
|
|
193
199
|
// src/git/history_reader.ts
|
|
200
|
+
import path2 from "node:path";
|
|
201
|
+
|
|
202
|
+
// src/git/repo_paths.ts
|
|
203
|
+
import path from "node:path";
|
|
204
|
+
|
|
205
|
+
// src/git/run_git.ts
|
|
194
206
|
import { execFile } from "node:child_process";
|
|
195
207
|
import { promisify } from "node:util";
|
|
196
208
|
var execFileAsync = promisify(execFile);
|
|
209
|
+
function isGitCommandFailure(result) {
|
|
210
|
+
return result.ok === false;
|
|
211
|
+
}
|
|
212
|
+
async function runGit(args, cwd) {
|
|
213
|
+
try {
|
|
214
|
+
const { stdout } = await execFileAsync(
|
|
215
|
+
"git",
|
|
216
|
+
["-c", "core.quotePath=false", ...args],
|
|
217
|
+
{
|
|
218
|
+
...cwd === void 0 ? {} : { cwd },
|
|
219
|
+
maxBuffer: 1024 * 1024 * 10
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
return { ok: true, stdout };
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
spawnError: isSpawnFailure(readErrorCode(error)),
|
|
227
|
+
detail: describeGitFailure(error)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function readErrorCode(error) {
|
|
232
|
+
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
233
|
+
}
|
|
234
|
+
function isSpawnFailure(code) {
|
|
235
|
+
return typeof code === "string" && !code.startsWith("ERR_");
|
|
236
|
+
}
|
|
237
|
+
function describeGitFailure(error) {
|
|
238
|
+
if (typeof error !== "object" || error === null) {
|
|
239
|
+
return error instanceof Error ? error.message : String(error);
|
|
240
|
+
}
|
|
241
|
+
const stderr = "stderr" in error ? error.stderr : void 0;
|
|
242
|
+
if (typeof stderr === "string" && stderr.trim()) return stderr.trim();
|
|
243
|
+
if (error instanceof Error) return error.message;
|
|
244
|
+
return String(error);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/git/repo_paths.ts
|
|
248
|
+
async function resolveRepositoryPaths(contentRoot) {
|
|
249
|
+
const resolvedContentRoot = path.resolve(contentRoot);
|
|
250
|
+
const result = await runGit([
|
|
251
|
+
"-C",
|
|
252
|
+
resolvedContentRoot,
|
|
253
|
+
"rev-parse",
|
|
254
|
+
"--show-toplevel",
|
|
255
|
+
"--show-prefix"
|
|
256
|
+
]);
|
|
257
|
+
if (isGitCommandFailure(result)) {
|
|
258
|
+
return result.spawnError ? { kind: "git-unavailable", detail: result.detail } : { kind: "content-outside-repository", detail: result.detail };
|
|
259
|
+
}
|
|
260
|
+
const [toplevel, prefix] = parseRevParseOutput(result.stdout);
|
|
261
|
+
if (!toplevel) {
|
|
262
|
+
return {
|
|
263
|
+
kind: "content-outside-repository",
|
|
264
|
+
detail: "git rev-parse reported no work tree root"
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
kind: "resolved",
|
|
269
|
+
paths: {
|
|
270
|
+
repositoryRoot: toPosixPath(path.normalize(toplevel)),
|
|
271
|
+
contentRoot: toPosixPath(resolvedContentRoot),
|
|
272
|
+
contentPrefix: toPosixPath(prefix).replace(/\/+$/, "")
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function toRepositoryPath(paths, contentRelativePath) {
|
|
277
|
+
const normalized = normalizeContentPath(contentRelativePath);
|
|
278
|
+
if (!paths.contentPrefix) return normalized;
|
|
279
|
+
if (!normalized) return paths.contentPrefix;
|
|
280
|
+
return `${paths.contentPrefix}/${normalized}`;
|
|
281
|
+
}
|
|
282
|
+
function normalizeContentPath(value) {
|
|
283
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
284
|
+
}
|
|
285
|
+
function isResolvedRepository(resolution) {
|
|
286
|
+
return resolution.kind === "resolved";
|
|
287
|
+
}
|
|
288
|
+
function parseRevParseOutput(stdout) {
|
|
289
|
+
const [toplevel = "", prefix = ""] = stdout.split(/\r?\n/);
|
|
290
|
+
return [toplevel.trim(), prefix.trim()];
|
|
291
|
+
}
|
|
292
|
+
function toPosixPath(value) {
|
|
293
|
+
return value.replace(/\\/g, "/");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/git/history_reader.ts
|
|
197
297
|
var fieldSeparator = "";
|
|
198
298
|
var recordSeparator = "";
|
|
199
299
|
var GitMarkdownHistoryReader = class {
|
|
200
|
-
#
|
|
300
|
+
#contentRoot;
|
|
201
301
|
#historyCache = /* @__PURE__ */ new Map();
|
|
202
302
|
#markdownCache = /* @__PURE__ */ new Map();
|
|
303
|
+
/** Maps a content-relative path to `<revision hash>` → work tree path. */
|
|
203
304
|
#revisionPathCache = /* @__PURE__ */ new Map();
|
|
204
|
-
#
|
|
305
|
+
#repository;
|
|
205
306
|
constructor(options = {}) {
|
|
206
|
-
this.#
|
|
307
|
+
this.#contentRoot = path2.resolve(options.cwd ?? process.cwd());
|
|
207
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Revision history for one file, newest first, following renames.
|
|
311
|
+
*
|
|
312
|
+
* @param filePath Content-relative path, e.g. `notes/hello.md`.
|
|
313
|
+
*/
|
|
208
314
|
getHistory(filePath) {
|
|
209
|
-
const normalizedPath =
|
|
315
|
+
const normalizedPath = normalizeContentPath(filePath);
|
|
210
316
|
const cached = this.#historyCache.get(normalizedPath);
|
|
211
317
|
if (cached) return cached;
|
|
212
318
|
const history = this.#readHistory(normalizedPath);
|
|
213
319
|
this.#historyCache.set(normalizedPath, history);
|
|
214
320
|
return history;
|
|
215
321
|
}
|
|
322
|
+
/** Content of one revision of a file, or `null` when Git cannot read it. */
|
|
216
323
|
getRevisionMarkdown(filePath, hash) {
|
|
217
|
-
const normalizedPath =
|
|
324
|
+
const normalizedPath = normalizeContentPath(filePath);
|
|
218
325
|
const cacheKey = `${hash}:${normalizedPath}`;
|
|
219
326
|
const cached = this.#markdownCache.get(cacheKey);
|
|
220
327
|
if (cached) return cached;
|
|
@@ -223,35 +330,44 @@ var GitMarkdownHistoryReader = class {
|
|
|
223
330
|
return markdown;
|
|
224
331
|
}
|
|
225
332
|
async #readHistory(filePath) {
|
|
226
|
-
|
|
333
|
+
const repository = await this.#resolvePaths();
|
|
334
|
+
if (!repository) return [];
|
|
227
335
|
const format = ["%H", "%h", "%cI", "%s", "%an"].join(fieldSeparator);
|
|
228
|
-
const result = await runGit(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
336
|
+
const result = await runGit(
|
|
337
|
+
[
|
|
338
|
+
"log",
|
|
339
|
+
"--follow",
|
|
340
|
+
"--name-status",
|
|
341
|
+
`--format=${recordSeparator}${format}`,
|
|
342
|
+
"--",
|
|
343
|
+
toRepositoryPath(repository, filePath)
|
|
344
|
+
],
|
|
345
|
+
repository.repositoryRoot
|
|
346
|
+
);
|
|
236
347
|
if (!result.ok || !result.stdout.trim()) return [];
|
|
237
348
|
const revisionPaths = /* @__PURE__ */ new Map();
|
|
238
|
-
const
|
|
349
|
+
const fallbackPath = toRepositoryPath(repository, filePath);
|
|
350
|
+
const revisions = result.stdout.split(recordSeparator).map((record) => record.trim()).filter(Boolean).map((record) => parseRevisionRecord(record, revisionPaths, fallbackPath)).filter((revision) => revision !== null);
|
|
239
351
|
this.#revisionPathCache.set(filePath, revisionPaths);
|
|
240
352
|
return revisions;
|
|
241
353
|
}
|
|
242
354
|
async #readRevisionMarkdown(filePath, hash) {
|
|
243
|
-
|
|
355
|
+
const repository = await this.#resolvePaths();
|
|
356
|
+
if (!repository) return null;
|
|
244
357
|
await this.getHistory(filePath);
|
|
245
|
-
const revisionPath = this.#revisionPathCache.get(filePath)?.get(hash) ?? filePath;
|
|
246
|
-
const result = await runGit(
|
|
358
|
+
const revisionPath = this.#revisionPathCache.get(filePath)?.get(hash) ?? toRepositoryPath(repository, filePath);
|
|
359
|
+
const result = await runGit(
|
|
360
|
+
["show", `${hash}:${revisionPath}`],
|
|
361
|
+
repository.repositoryRoot
|
|
362
|
+
);
|
|
247
363
|
return result.ok ? result.stdout : null;
|
|
248
364
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return
|
|
365
|
+
/** `null` when the content directory has no readable work tree. */
|
|
366
|
+
async #resolvePaths() {
|
|
367
|
+
this.#repository ??= resolveRepositoryPaths(this.#contentRoot);
|
|
368
|
+
const resolution = await this.#repository;
|
|
369
|
+
if (!isResolvedRepository(resolution)) return null;
|
|
370
|
+
return resolution.paths;
|
|
255
371
|
}
|
|
256
372
|
};
|
|
257
373
|
function parseRevisionRecord(record, revisionPaths, fallbackPath) {
|
|
@@ -271,44 +387,38 @@ function getRevisionPath(nameStatusLines, fallbackPath) {
|
|
|
271
387
|
const line = nameStatusLines.find((entry) => entry.trim().length > 0);
|
|
272
388
|
if (!line) return fallbackPath;
|
|
273
389
|
const [, ...paths] = line.split(" ");
|
|
274
|
-
return paths.at(-1) ?? fallbackPath;
|
|
275
|
-
}
|
|
276
|
-
function normalizeGitPath(filePath) {
|
|
277
|
-
return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
278
|
-
}
|
|
279
|
-
async function runGit(cwd, args) {
|
|
280
|
-
try {
|
|
281
|
-
const { stdout } = await execFileAsync("git", args, {
|
|
282
|
-
cwd,
|
|
283
|
-
maxBuffer: 1024 * 1024 * 10
|
|
284
|
-
});
|
|
285
|
-
return { ok: true, stdout };
|
|
286
|
-
} catch (error) {
|
|
287
|
-
const stdout = typeof error === "object" && error !== null && "stdout" in error && typeof error.stdout === "string" ? error.stdout : "";
|
|
288
|
-
return { ok: false, stdout };
|
|
289
|
-
}
|
|
390
|
+
return normalizeContentPath(paths.at(-1) ?? fallbackPath);
|
|
290
391
|
}
|
|
291
392
|
|
|
292
393
|
// index.ts
|
|
293
394
|
function diff(options = {}) {
|
|
294
|
-
const api = createPostDiffApi(options);
|
|
295
395
|
const ui = { enabled: true, maxRevisions: 20, ...options.ui };
|
|
396
|
+
let api;
|
|
296
397
|
return definePlugin({
|
|
297
398
|
name: "diff",
|
|
399
|
+
processedContentCache: {
|
|
400
|
+
version: "diff-v2",
|
|
401
|
+
dependencyMode: "unsafe"
|
|
402
|
+
},
|
|
298
403
|
options,
|
|
299
404
|
assets: ui.enabled === false ? [] : [createStyleAsset("diff")],
|
|
300
405
|
clientEntries: ui.enabled === false ? [] : [createClientEntry("diff", "initDiffHistory")],
|
|
301
406
|
onPostProcessed: async (context) => {
|
|
302
407
|
if (ui.enabled === false) return;
|
|
408
|
+
api ??= createPostDiffApi({
|
|
409
|
+
...options,
|
|
410
|
+
cwd: options.cwd ?? context.config?.content.directory
|
|
411
|
+
});
|
|
303
412
|
const filePath = resolvePostFilePath(context);
|
|
304
413
|
const history = (await api.getHistory(filePath)).slice(
|
|
305
414
|
0,
|
|
306
415
|
ui.maxRevisions
|
|
307
416
|
);
|
|
308
|
-
const
|
|
417
|
+
const revisions = await getMarkdownRevisions(api, filePath, history);
|
|
418
|
+
const selected = await api.getCurrentDiff(filePath);
|
|
309
419
|
context.content.html += renderDiffHistory({
|
|
310
|
-
|
|
311
|
-
|
|
420
|
+
revisions,
|
|
421
|
+
selected
|
|
312
422
|
});
|
|
313
423
|
}
|
|
314
424
|
});
|
|
@@ -354,32 +464,16 @@ function createPostDiffApi(options = {}) {
|
|
|
354
464
|
}
|
|
355
465
|
};
|
|
356
466
|
}
|
|
357
|
-
function
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
comparisons.push({
|
|
367
|
-
filePath,
|
|
368
|
-
fromHash: previous?.hash ?? null,
|
|
369
|
-
toHash: to.hash
|
|
370
|
-
});
|
|
371
|
-
for (let fromIndex = toIndex + 2; fromIndex < history.length; fromIndex += 1) {
|
|
372
|
-
comparisons.push({
|
|
373
|
-
filePath,
|
|
374
|
-
fromHash: history[fromIndex].hash,
|
|
375
|
-
toHash: to.hash
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
const diffs = await Promise.all(
|
|
380
|
-
comparisons.map((comparison) => api.compareRevisions(comparison))
|
|
467
|
+
async function getMarkdownRevisions(api, filePath, history) {
|
|
468
|
+
const revisions = await Promise.all(
|
|
469
|
+
history.map(async (revision) => ({
|
|
470
|
+
revision,
|
|
471
|
+
markdown: await api.getRevisionMarkdown(filePath, revision.hash)
|
|
472
|
+
}))
|
|
473
|
+
);
|
|
474
|
+
return revisions.flatMap(
|
|
475
|
+
({ revision, markdown }) => markdown === null ? [] : [{ ...revision, markdown }]
|
|
381
476
|
);
|
|
382
|
-
return diffs.filter((entry) => entry !== null);
|
|
383
477
|
}
|
|
384
478
|
function resolvePostFilePath(context) {
|
|
385
479
|
const indexedPath = context.contentIndex.get(context.slug.toLowerCase());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MarkdownRevision, PostDiff } from "../types.js";
|
|
2
2
|
export type DiffHistoryProps = {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
revisions: MarkdownRevision[];
|
|
4
|
+
selected: PostDiff | null;
|
|
5
5
|
};
|
|
6
6
|
export declare function renderDiffHistory(input: DiffHistoryProps): string;
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { DiffRevision, GitHistoryReaderOptions } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads Markdown revisions out of a local Git repository.
|
|
4
|
+
*
|
|
5
|
+
* Callers speak content-relative paths — the same paths the content index
|
|
6
|
+
* reports. The reader resolves the owning work tree once and translates every
|
|
7
|
+
* path itself, so the process working directory never influences a result.
|
|
8
|
+
*/
|
|
2
9
|
export declare class GitMarkdownHistoryReader {
|
|
3
10
|
#private;
|
|
4
11
|
constructor(options?: GitHistoryReaderOptions);
|
|
12
|
+
/**
|
|
13
|
+
* Revision history for one file, newest first, following renames.
|
|
14
|
+
*
|
|
15
|
+
* @param filePath Content-relative path, e.g. `notes/hello.md`.
|
|
16
|
+
*/
|
|
5
17
|
getHistory(filePath: string): Promise<DiffRevision[]>;
|
|
18
|
+
/** Content of one revision of a file, or `null` when Git cannot read it. */
|
|
6
19
|
getRevisionMarkdown(filePath: string, hash: string): Promise<string | null>;
|
|
7
20
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paths that locate a content directory inside a Git work tree.
|
|
3
|
+
*
|
|
4
|
+
* Every field is a POSIX-separated path (`git` prints and accepts those on every
|
|
5
|
+
* platform, including Windows).
|
|
6
|
+
*/
|
|
7
|
+
export type GitRepositoryPaths = {
|
|
8
|
+
/** Work tree root reported by `git rev-parse --show-toplevel`. */
|
|
9
|
+
repositoryRoot: string;
|
|
10
|
+
/** Absolute path of the content directory. */
|
|
11
|
+
contentRoot: string;
|
|
12
|
+
/**
|
|
13
|
+
* Content directory relative to `repositoryRoot`, without a trailing slash.
|
|
14
|
+
* Empty when the content directory is the work tree root itself.
|
|
15
|
+
*/
|
|
16
|
+
contentPrefix: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Why Git history cannot be read for a content directory.
|
|
20
|
+
*
|
|
21
|
+
* The reason is modelled explicitly so a plugin can report an actionable
|
|
22
|
+
* diagnostic instead of quietly rendering nothing.
|
|
23
|
+
*/
|
|
24
|
+
export type RepositoryPathsResolution = {
|
|
25
|
+
kind: "resolved";
|
|
26
|
+
paths: GitRepositoryPaths;
|
|
27
|
+
}
|
|
28
|
+
/** Git is not installed, or could not be started. */
|
|
29
|
+
| {
|
|
30
|
+
kind: "git-unavailable";
|
|
31
|
+
detail: string;
|
|
32
|
+
}
|
|
33
|
+
/** Git ran, but the content directory does not live in a work tree. */
|
|
34
|
+
| {
|
|
35
|
+
kind: "content-outside-repository";
|
|
36
|
+
detail: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Locates the Git work tree that owns `contentRoot`.
|
|
40
|
+
*
|
|
41
|
+
* Both the work tree root and the content prefix come from Git itself, so their
|
|
42
|
+
* casing and symlink resolution always agree with the paths `git log` prints —
|
|
43
|
+
* the reason content-relative paths can be translated without guessing.
|
|
44
|
+
*
|
|
45
|
+
* The work tree is resolved through `git -C <contentRoot>` rather than by
|
|
46
|
+
* running Git in `contentRoot`, so a missing directory surfaces as a Git
|
|
47
|
+
* failure instead of an indistinguishable spawn failure.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveRepositoryPaths(contentRoot: string): Promise<RepositoryPathsResolution>;
|
|
50
|
+
/**
|
|
51
|
+
* Translates a content-relative path into the work tree-relative path that
|
|
52
|
+
* `git log` pathspecs and `git show <rev>:<path>` expect.
|
|
53
|
+
*/
|
|
54
|
+
export declare function toRepositoryPath(paths: GitRepositoryPaths, contentRelativePath: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Translates a work tree-relative path printed by `git` back into a
|
|
57
|
+
* content-relative path, or returns `null` when the path lies outside the
|
|
58
|
+
* content directory.
|
|
59
|
+
*/
|
|
60
|
+
export declare function toContentPath(paths: GitRepositoryPaths, repositoryRelativePath: string): string | null;
|
|
61
|
+
/** Normalizes a path to the POSIX form used for content and work tree paths. */
|
|
62
|
+
export declare function normalizeContentPath(value: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* True when the content directory has a usable work tree. Narrows the
|
|
65
|
+
* resolution so callers can read `paths`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isResolvedRepository(resolution: RepositoryPathsResolution): resolution is Extract<RepositoryPathsResolution, {
|
|
68
|
+
kind: "resolved";
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* True when history cannot be read and a reason is available. Narrows the
|
|
72
|
+
* resolution so callers can read `detail`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isRepositoryUnavailable(resolution: RepositoryPathsResolution): resolution is Exclude<RepositoryPathsResolution, {
|
|
75
|
+
kind: "resolved";
|
|
76
|
+
}>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result of a `git` invocation.
|
|
3
|
+
*
|
|
4
|
+
* Failures are returned instead of thrown: a site whose Git history cannot be
|
|
5
|
+
* read must still build, so every caller decides what an unavailable repository
|
|
6
|
+
* means for its own output.
|
|
7
|
+
*/
|
|
8
|
+
export type GitCommandResult = {
|
|
9
|
+
ok: true;
|
|
10
|
+
stdout: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `spawnError` separates "Git could not be started" (missing or unexecutable
|
|
14
|
+
* binary) from "Git ran and rejected the command" (not a work tree, unknown
|
|
15
|
+
* revision, unreadable path). The distinction drives which diagnostic a
|
|
16
|
+
* plugin reports.
|
|
17
|
+
*/
|
|
18
|
+
| {
|
|
19
|
+
ok: false;
|
|
20
|
+
spawnError: boolean;
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Narrows a failed command so its failure fields can be read.
|
|
25
|
+
*
|
|
26
|
+
* Type predicates are used instead of `result.ok` checks throughout this
|
|
27
|
+
* package because the build type-checks without `strictNullChecks`, where
|
|
28
|
+
* discriminant narrowing of an inline condition does not apply.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isGitCommandFailure(result: GitCommandResult): result is Extract<GitCommandResult, {
|
|
31
|
+
ok: false;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Runs `git` with `args`.
|
|
35
|
+
*
|
|
36
|
+
* `core.quotePath=false` keeps non-ASCII paths verbatim so the printed paths
|
|
37
|
+
* match the ones the content index reports; Git otherwise escapes them into
|
|
38
|
+
* octal sequences that no path lookup can resolve.
|
|
39
|
+
*
|
|
40
|
+
* @param cwd Directory Git runs in. Omit when the command carries its own
|
|
41
|
+
* `-C <dir>` argument.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runGit(args: readonly string[], cwd?: string): Promise<GitCommandResult>;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ export type RevisionComparisonInput = {
|
|
|
26
26
|
toHash: string;
|
|
27
27
|
};
|
|
28
28
|
export type GitHistoryReaderOptions = {
|
|
29
|
+
/**
|
|
30
|
+
* Content root: the directory holding the notes, used to locate the owning
|
|
31
|
+
* Git work tree. Defaults to `config.content.directory` when the plugin runs
|
|
32
|
+
* inside a build, otherwise `process.cwd()`. A relative value is resolved
|
|
33
|
+
* against `process.cwd()`.
|
|
34
|
+
*/
|
|
29
35
|
cwd?: string;
|
|
30
36
|
};
|
|
31
37
|
export type DiffPluginUiOptions = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riebeckite/plugin-diff",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"./style.css": "./style.css"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@riebeckite/core": "0.0.
|
|
24
|
+
"@riebeckite/core": "0.0.15"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^24.5.2",
|
|
28
28
|
"tsx": "^4.22.4",
|
|
29
|
-
"@riebeckite/test": "0.0.
|
|
29
|
+
"@riebeckite/test": "0.0.15"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
package/style.css
CHANGED
|
@@ -12,6 +12,25 @@
|
|
|
12
12
|
justify-content: space-between;
|
|
13
13
|
gap: 1rem;
|
|
14
14
|
margin-block-end: 1rem;
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
user-select: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.rr-diff-history__panel {
|
|
20
|
+
interpolate-size: allow-keywords;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.rr-diff-history__header::marker {
|
|
24
|
+
content: "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.rr-diff-history__header::-webkit-details-marker {
|
|
28
|
+
display: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.rr-diff-history__header:focus-visible {
|
|
32
|
+
outline: 2px solid var(--rb-color-accent, #f66620);
|
|
33
|
+
outline-offset: 2px;
|
|
15
34
|
}
|
|
16
35
|
|
|
17
36
|
.rr-diff-history__title {
|
|
@@ -21,6 +40,72 @@
|
|
|
21
40
|
text-transform: uppercase;
|
|
22
41
|
}
|
|
23
42
|
|
|
43
|
+
.rr-diff-history__header-meta {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 0.5rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.rr-diff-history__toggle-label {
|
|
50
|
+
color: var(
|
|
51
|
+
--rb-color-muted,
|
|
52
|
+
color-mix(in srgb, currentColor 62%, transparent)
|
|
53
|
+
);
|
|
54
|
+
font-size: 0.75rem;
|
|
55
|
+
letter-spacing: 0.04em;
|
|
56
|
+
text-transform: uppercase;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.rr-diff-history__toggle-label--open {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.rr-diff-history__panel[open] .rr-diff-history__toggle-label--open {
|
|
64
|
+
display: inline;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.rr-diff-history__panel[open] .rr-diff-history__toggle-label--closed {
|
|
68
|
+
display: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.rr-diff-history__toggle {
|
|
72
|
+
display: inline-grid;
|
|
73
|
+
place-items: center;
|
|
74
|
+
width: 1.5rem;
|
|
75
|
+
height: 1.5rem;
|
|
76
|
+
color: var(
|
|
77
|
+
--rb-color-muted,
|
|
78
|
+
color-mix(in srgb, currentColor 62%, transparent)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.rr-diff-history__toggle-icon {
|
|
83
|
+
transform: rotate(180deg);
|
|
84
|
+
transition: transform 180ms ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.rr-diff-history__panel[open] .rr-diff-history__toggle-icon {
|
|
88
|
+
transform: rotate(90deg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@supports selector(::details-content) {
|
|
92
|
+
.rr-diff-history__panel::details-content {
|
|
93
|
+
block-size: 0;
|
|
94
|
+
overflow: clip;
|
|
95
|
+
opacity: 0;
|
|
96
|
+
transition:
|
|
97
|
+
block-size 220ms ease,
|
|
98
|
+
content-visibility 220ms ease allow-discrete,
|
|
99
|
+
opacity 160ms ease;
|
|
100
|
+
transition-behavior: allow-discrete;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.rr-diff-history__panel[open]::details-content {
|
|
104
|
+
block-size: auto;
|
|
105
|
+
opacity: 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
24
109
|
.rr-diff-history__count,
|
|
25
110
|
.rr-diff-history__date,
|
|
26
111
|
.rr-diff-history__meta {
|
|
@@ -98,6 +183,7 @@
|
|
|
98
183
|
}
|
|
99
184
|
|
|
100
185
|
.rr-diff-history__commit:focus-visible,
|
|
186
|
+
.rr-diff-history__diff-head:focus-visible,
|
|
101
187
|
.rr-diff-history__controls select:focus-visible {
|
|
102
188
|
outline: 2px solid var(--rb-color-accent, #f66620);
|
|
103
189
|
outline-offset: 2px;
|
|
@@ -137,6 +223,9 @@
|
|
|
137
223
|
}
|
|
138
224
|
|
|
139
225
|
.rr-diff-history__diff-head {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
gap: 0.5rem;
|
|
140
229
|
border-block-end: 1px solid
|
|
141
230
|
var(--rb-color-border, color-mix(in srgb, currentColor 18%, transparent));
|
|
142
231
|
padding: 0.625rem 0.75rem;
|
|
@@ -148,6 +237,15 @@
|
|
|
148
237
|
monospace
|
|
149
238
|
);
|
|
150
239
|
font-size: 0.8125rem;
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
user-select: none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.rr-diff-history__diff-head::marker {
|
|
245
|
+
color: var(
|
|
246
|
+
--rb-color-muted,
|
|
247
|
+
color-mix(in srgb, currentColor 62%, transparent)
|
|
248
|
+
);
|
|
151
249
|
}
|
|
152
250
|
|
|
153
251
|
.rr-diff-history__table-wrap {
|