@riebeckite/plugin-diff 0.0.32 → 0.0.33

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_ja.md CHANGED
@@ -32,7 +32,7 @@ const current = await api.getCurrentDiff("notes/hello.md");
32
32
  | `getCurrentDiff(filePath)` | 最新と一つ前のリビジョンの差分 |
33
33
  | `compareRevisions({ filePath, fromHash, toHash })` | 指定した二つのリビジョンの差分。`fromHash: null` なら空の本文との差分 |
34
34
 
35
- `cwd` は Git リポジトリを探すためのコンテンツルートで、既定値はビルド内の `config.content.directory`、それ以外では `process.cwd()` です。相対パスは `process.cwd()` 基準で解決されます。Git リポジトリ外を指定しても例外は投げず、空の結果を返します。
35
+ `cwd` は Git リポジトリを探すためのコンテンツルートで、既定値はビルド時の `config.content.directory`、それ以外では `process.cwd()` です。相対パスは `process.cwd()` 基準で解決されます。Git リポジトリ外を指定しても例外は送出せず、空の結果を返します。
36
36
 
37
37
  ## オプションと型
38
38
 
@@ -5,3 +5,5 @@ export type PostDiffProps = {
5
5
  };
6
6
  declare const PostDiff: ({ diff, title }: Readonly<PostDiffProps>) => import("hono/jsx/jsx-dev-runtime").JSX.Element;
7
7
  export default PostDiff;
8
+ export type ArticlePostDiffProps = Omit<PostDiffProps, "diff">;
9
+ export declare function ArticlePostDiffView(props: Readonly<ArticlePostDiffProps>): Promise<any[] | import("hono/utils/html").HtmlEscapedString>;
@@ -1,3 +1,247 @@
1
+ // components/post-diff.tsx
2
+ import { resolvePlugins } from "@riebeckite/core";
3
+ import { useArticleContext, useSiteContext } from "@riebeckite/ui";
4
+
5
+ // src/diff/line_diff.ts
6
+ function createLineDiff(fromMarkdown, toMarkdown) {
7
+ const fromLines = splitLines(fromMarkdown);
8
+ const toLines = splitLines(toMarkdown);
9
+ const table = buildLongestCommonSubsequenceTable(fromLines, toLines);
10
+ const lines = [];
11
+ let fromIndex = 0;
12
+ let toIndex = 0;
13
+ let oldLineNumber = 1;
14
+ let newLineNumber = 1;
15
+ while (fromIndex < fromLines.length && toIndex < toLines.length) {
16
+ if (fromLines[fromIndex] === toLines[toIndex]) {
17
+ lines.push({
18
+ type: "context",
19
+ content: fromLines[fromIndex],
20
+ oldLineNumber,
21
+ newLineNumber
22
+ });
23
+ fromIndex += 1;
24
+ toIndex += 1;
25
+ oldLineNumber += 1;
26
+ newLineNumber += 1;
27
+ continue;
28
+ }
29
+ if (table[fromIndex + 1][toIndex] >= table[fromIndex][toIndex + 1]) {
30
+ lines.push({
31
+ type: "removed",
32
+ content: fromLines[fromIndex],
33
+ oldLineNumber,
34
+ newLineNumber: null
35
+ });
36
+ fromIndex += 1;
37
+ oldLineNumber += 1;
38
+ } else {
39
+ lines.push({
40
+ type: "added",
41
+ content: toLines[toIndex],
42
+ oldLineNumber: null,
43
+ newLineNumber
44
+ });
45
+ toIndex += 1;
46
+ newLineNumber += 1;
47
+ }
48
+ }
49
+ while (fromIndex < fromLines.length) {
50
+ lines.push({
51
+ type: "removed",
52
+ content: fromLines[fromIndex],
53
+ oldLineNumber,
54
+ newLineNumber: null
55
+ });
56
+ fromIndex += 1;
57
+ oldLineNumber += 1;
58
+ }
59
+ while (toIndex < toLines.length) {
60
+ lines.push({
61
+ type: "added",
62
+ content: toLines[toIndex],
63
+ oldLineNumber: null,
64
+ newLineNumber
65
+ });
66
+ toIndex += 1;
67
+ newLineNumber += 1;
68
+ }
69
+ return lines;
70
+ }
71
+ function splitLines(markdown) {
72
+ if (!markdown) return [];
73
+ return markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
74
+ }
75
+ function buildLongestCommonSubsequenceTable(fromLines, toLines) {
76
+ const table = Array.from(
77
+ { length: fromLines.length + 1 },
78
+ () => Array.from({ length: toLines.length + 1 }, () => 0)
79
+ );
80
+ for (let fromIndex = fromLines.length - 1; fromIndex >= 0; fromIndex -= 1) {
81
+ for (let toIndex = toLines.length - 1; toIndex >= 0; toIndex -= 1) {
82
+ table[fromIndex][toIndex] = fromLines[fromIndex] === toLines[toIndex] ? table[fromIndex + 1][toIndex + 1] + 1 : Math.max(
83
+ table[fromIndex + 1][toIndex],
84
+ table[fromIndex][toIndex + 1]
85
+ );
86
+ }
87
+ }
88
+ return table;
89
+ }
90
+
91
+ // src/git/history_reader.ts
92
+ import {
93
+ GitRepository,
94
+ normalizeContentPath,
95
+ runGit,
96
+ toRepositoryPath
97
+ } from "@riebeckite/git-history";
98
+ var fieldSeparator = "";
99
+ var recordSeparator = "";
100
+ var GitMarkdownHistoryReader = class {
101
+ #repository;
102
+ #historyCache = /* @__PURE__ */ new Map();
103
+ #markdownCache = /* @__PURE__ */ new Map();
104
+ /** Maps a content-relative path to `<revision hash>` → work tree path. */
105
+ #revisionPathCache = /* @__PURE__ */ new Map();
106
+ constructor(options = {}) {
107
+ this.#repository = new GitRepository(options);
108
+ }
109
+ /**
110
+ * Revision history for one file, newest first, following renames.
111
+ *
112
+ * @param filePath Content-relative path, e.g. `notes/hello.md`.
113
+ */
114
+ getHistory(filePath) {
115
+ const normalizedPath = normalizeContentPath(filePath);
116
+ const cached = this.#historyCache.get(normalizedPath);
117
+ if (cached) return cached;
118
+ const history = this.#readHistory(normalizedPath);
119
+ this.#historyCache.set(normalizedPath, history);
120
+ return history;
121
+ }
122
+ /** Content of one revision of a file, or `null` when Git cannot read it. */
123
+ getRevisionMarkdown(filePath, hash) {
124
+ const normalizedPath = normalizeContentPath(filePath);
125
+ const cacheKey = `${hash}:${normalizedPath}`;
126
+ const cached = this.#markdownCache.get(cacheKey);
127
+ if (cached) return cached;
128
+ const markdown = this.#readRevisionMarkdown(normalizedPath, hash);
129
+ this.#markdownCache.set(cacheKey, markdown);
130
+ return markdown;
131
+ }
132
+ async #readHistory(filePath) {
133
+ const repository = await this.#resolvePaths();
134
+ if (!repository) return [];
135
+ const format = ["%H", "%h", "%cI", "%s", "%an"].join(fieldSeparator);
136
+ const result = await runGit(
137
+ [
138
+ "log",
139
+ "--follow",
140
+ "--name-status",
141
+ `--format=${recordSeparator}${format}`,
142
+ "--",
143
+ toRepositoryPath(repository, filePath)
144
+ ],
145
+ repository.repositoryRoot
146
+ );
147
+ if (!result.ok || !result.stdout.trim()) return [];
148
+ const revisionPaths = /* @__PURE__ */ new Map();
149
+ const fallbackPath = toRepositoryPath(repository, filePath);
150
+ const revisions = result.stdout.split(recordSeparator).map((record) => record.trim()).filter(Boolean).map((record) => parseRevisionRecord(record, revisionPaths, fallbackPath)).filter((revision) => revision !== null);
151
+ this.#revisionPathCache.set(filePath, revisionPaths);
152
+ return revisions;
153
+ }
154
+ async #readRevisionMarkdown(filePath, hash) {
155
+ const repository = await this.#resolvePaths();
156
+ if (!repository) return null;
157
+ await this.getHistory(filePath);
158
+ const revisionPath = this.#revisionPathCache.get(filePath)?.get(hash) ?? toRepositoryPath(repository, filePath);
159
+ const result = await runGit(
160
+ ["show", `${hash}:${revisionPath}`],
161
+ repository.repositoryRoot
162
+ );
163
+ return result.ok ? result.stdout : null;
164
+ }
165
+ getStateFingerprint() {
166
+ return this.#repository.getStateFingerprint();
167
+ }
168
+ /** `null` when the content directory has no readable work tree. */
169
+ #resolvePaths() {
170
+ return this.#repository.paths();
171
+ }
172
+ };
173
+ function parseRevisionRecord(record, revisionPaths, fallbackPath) {
174
+ const [metadata = "", ...nameStatusLines] = record.split("\n");
175
+ const [hash, shortHash, date, message, author] = metadata.split(fieldSeparator);
176
+ if (!hash || !shortHash || !date) return null;
177
+ revisionPaths.set(hash, getRevisionPath(nameStatusLines, fallbackPath));
178
+ return {
179
+ hash,
180
+ shortHash,
181
+ date,
182
+ message: message ?? "",
183
+ author: author ?? ""
184
+ };
185
+ }
186
+ function getRevisionPath(nameStatusLines, fallbackPath) {
187
+ const line = nameStatusLines.find((entry) => entry.trim().length > 0);
188
+ if (!line) return fallbackPath;
189
+ const [, ...paths] = line.split(" ");
190
+ return normalizeContentPath(paths.at(-1) ?? fallbackPath);
191
+ }
192
+
193
+ // src/api.ts
194
+ function createPostDiffApi(options = {}) {
195
+ return createPostDiffApiFromReader(new GitMarkdownHistoryReader(options));
196
+ }
197
+ function createPostDiffApiFromReader(reader) {
198
+ return {
199
+ getHistory: (filePath) => reader.getHistory(filePath),
200
+ getRevisionMarkdown: (filePath, hash) => reader.getRevisionMarkdown(filePath, hash),
201
+ getCurrentDiff: async (filePath) => {
202
+ const history = await reader.getHistory(filePath);
203
+ const [current, previous] = history;
204
+ if (!current) return null;
205
+ const toMarkdown = await reader.getRevisionMarkdown(
206
+ filePath,
207
+ current.hash
208
+ );
209
+ if (toMarkdown === null) return null;
210
+ const fromMarkdown = previous ? await reader.getRevisionMarkdown(filePath, previous.hash) : null;
211
+ return buildPostDiff({
212
+ from: previous ?? null,
213
+ fromMarkdown,
214
+ to: current,
215
+ toMarkdown
216
+ });
217
+ },
218
+ compareRevisions: async (input) => {
219
+ const history = await reader.getHistory(input.filePath);
220
+ const from = history.find((revision) => revision.hash === input.fromHash);
221
+ const to = history.find((revision) => revision.hash === input.toHash);
222
+ if (!to) return null;
223
+ const [fromMarkdown, toMarkdown] = await Promise.all([
224
+ input.fromHash ? reader.getRevisionMarkdown(input.filePath, input.fromHash) : Promise.resolve(null),
225
+ reader.getRevisionMarkdown(input.filePath, input.toHash)
226
+ ]);
227
+ if (toMarkdown === null) return null;
228
+ return buildPostDiff({
229
+ from: from ?? null,
230
+ fromMarkdown,
231
+ to,
232
+ toMarkdown
233
+ });
234
+ }
235
+ };
236
+ }
237
+ function buildPostDiff(input) {
238
+ return {
239
+ from: input.from,
240
+ to: input.to,
241
+ lines: createLineDiff(input.fromMarkdown ?? "", input.toMarkdown)
242
+ };
243
+ }
244
+
1
245
  // components/post-diff.tsx
2
246
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
3
247
  var PostDiff = (({ diff, title = "History" }) => {
@@ -23,6 +267,23 @@ var PostDiff = (({ diff, title = "History" }) => {
23
267
  ] });
24
268
  });
25
269
  var post_diff_default = PostDiff;
270
+ async function ArticlePostDiffView(props) {
271
+ const site = useSiteContext();
272
+ const article = useArticleContext();
273
+ if (!site || !article?.entry) return [];
274
+ const options = findDiffOptions(site.config.plugins);
275
+ if (!options || options.ui?.enabled === false) return [];
276
+ const filePath = site.manifest.contentIndex.get(article.entry.slug.toLowerCase()) ?? `${article.entry.slug}.md`;
277
+ const diff = await createPostDiffApi({
278
+ ...options,
279
+ cwd: options.cwd ?? site.config.content.directory
280
+ }).getCurrentDiff(filePath);
281
+ return /* @__PURE__ */ jsx(PostDiff, { diff, title: props.title });
282
+ }
283
+ function findDiffOptions(plugins) {
284
+ return resolvePlugins([...plugins]).find((plugin) => plugin.name === "diff")?.options;
285
+ }
26
286
  export {
287
+ ArticlePostDiffView,
27
288
  post_diff_default as default
28
289
  };
package/dist/index.d.ts CHANGED
@@ -1,25 +1,18 @@
1
1
  import { type PluginDataHelper } from "@riebeckite/core";
2
- export type { PostDiffProps } from "./components/post-diff.js";
3
- export { default as PostDiffView } from "./components/post-diff.js";
2
+ export type { ArticlePostDiffProps, PostDiffProps, } from "./components/post-diff.js";
3
+ export { ArticlePostDiffView, default as PostDiffView, } from "./components/post-diff.js";
4
4
  export { renderDiffHistory } from "./src/components/diff-history.js";
5
5
  export { renderDiffLine } from "./src/components/diff-line.js";
6
6
  export { renderDiffPanel, renderDiffViewer, } from "./src/components/diff-viewer.js";
7
- import type { DiffPluginUiOptions, DiffRevision, GitHistoryReaderOptions, PostDiff, RevisionComparisonInput } from "./src/types.js";
7
+ import { type PostDiffApi } from "./src/api.js";
8
+ import type { DiffPluginOptions, PostDiff } from "./src/types.js";
9
+ export type { PostDiffApi } from "./src/api.js";
10
+ export { createPostDiffApi } from "./src/api.js";
8
11
  export { createLineDiff } from "./src/diff/line_diff.js";
9
12
  export { GitMarkdownHistoryReader } from "./src/git/history_reader.js";
10
- export type { DiffLine, DiffLineType, DiffPluginUiOptions, DiffRevision, GitHistoryReaderOptions, MarkdownRevision, PostDiff, RevisionComparisonInput, } from "./src/types.js";
11
- export type DiffPluginOptions = GitHistoryReaderOptions & {
12
- ui?: DiffPluginUiOptions;
13
- };
14
- export type PostDiffApi = {
15
- getHistory(filePath: string): Promise<DiffRevision[]>;
16
- getRevisionMarkdown(filePath: string, hash: string): Promise<string | null>;
17
- getCurrentDiff(filePath: string): Promise<PostDiff | null>;
18
- compareRevisions(input: RevisionComparisonInput): Promise<PostDiff | null>;
19
- };
13
+ export type { DiffLine, DiffLineType, DiffPluginOptions, DiffPluginUiOptions, DiffRevision, GitHistoryReaderOptions, MarkdownRevision, PostDiff, RevisionComparisonInput, } from "./src/types.js";
20
14
  export declare const getPostDiff: PluginDataHelper<{
21
15
  api: PostDiffApi;
22
16
  filePath: string;
23
17
  }, Promise<PostDiff | null>>;
24
18
  export declare function diff(options?: DiffPluginOptions): import("@riebeckite/core").RiebeckitePlugin<DiffPluginOptions>;
25
- export declare function createPostDiffApi(options?: GitHistoryReaderOptions): PostDiffApi;
package/dist/index.js CHANGED
@@ -7,128 +7,8 @@ import {
7
7
  } from "@riebeckite/core";
8
8
 
9
9
  // components/post-diff.tsx
10
- import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- var PostDiff = (({ diff: diff2, title = "History" }) => {
12
- if (!diff2) return null;
13
- return /* @__PURE__ */ jsxs("section", { class: "rr-diff-history", "data-rr-diff-history": true, children: [
14
- /* @__PURE__ */ jsx("h2", { class: "rr-diff-history__title", children: title }),
15
- /* @__PURE__ */ jsxs("p", { class: "rr-diff-history__meta", children: [
16
- diff2.from ? `${diff2.from.shortHash} → ` : "",
17
- diff2.to.shortHash
18
- ] }),
19
- /* @__PURE__ */ jsx("pre", { class: "rr-diff-history__diff", children: /* @__PURE__ */ jsx("code", { children: diff2.lines.map((line, index) => /* @__PURE__ */ jsxs(
20
- "span",
21
- {
22
- class: `rr-diff-line rr-diff-line--${line.type}`,
23
- children: [
24
- line.type === "added" ? "+" : line.type === "removed" ? "-" : " ",
25
- line.content,
26
- "\n"
27
- ]
28
- },
29
- `${line.oldLineNumber}-${line.newLineNumber}-${index}`
30
- )) }) })
31
- ] });
32
- });
33
- var post_diff_default = PostDiff;
34
-
35
- // src/components/diff-line.tsx
36
- function renderDiffLine({ line }) {
37
- const prefix = line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
38
- return `<tr class="rr-diff-history__line rr-diff-history__line--${line.type}">
39
- <td class="rr-diff-history__num">${line.oldLineNumber ?? ""}</td>
40
- <td class="rr-diff-history__num">${line.newLineNumber ?? ""}</td>
41
- <td class="rr-diff-history__code"><code>${prefix} ${escapeHtml(line.content)}</code></td>
42
- </tr>`;
43
- }
44
- function escapeHtml(value) {
45
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
46
- }
47
-
48
- // src/components/format-date.ts
49
- var MONTHS = [
50
- "Jan",
51
- "Feb",
52
- "Mar",
53
- "Apr",
54
- "May",
55
- "Jun",
56
- "Jul",
57
- "Aug",
58
- "Sep",
59
- "Oct",
60
- "Nov",
61
- "Dec"
62
- ];
63
- function formatDiffDate(value) {
64
- const date = new Date(value);
65
- if (Number.isNaN(date.getTime())) return value;
66
- return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
67
- }
68
-
69
- // src/components/diff-viewer.tsx
70
- function renderDiffViewer({
71
- history,
72
- selected
73
- }) {
74
- return `<div class="rr-diff-history__viewer">
75
- <div class="rr-diff-history__controls">
76
- <label>From ${renderRevisionSelect("from", history, selected.from?.hash ?? "")}</label>
77
- <label>To ${renderRevisionSelect("to", history, selected.to.hash)}</label>
78
- </div>
79
- <div data-rr-diff-panel>${renderDiffPanel(selected)}</div>
80
- </div>`;
81
- }
82
- function renderDiffPanel(diff2) {
83
- return `<div class="rr-diff-history__diff" data-rr-diff-rendered>
84
- <div class="rr-diff-history__table-wrap">
85
- <table class="rr-diff-history__table">
86
- <tbody>${diff2.lines.map((line) => renderDiffLine({ line })).join("")}</tbody>
87
- </table>
88
- </div>
89
- </div>`;
90
- }
91
- function renderRevisionSelect(kind, history, value) {
92
- const emptyOption = kind === "from" ? `<option value="">Beginning</option>` : "";
93
- return `<select data-rr-diff-${kind}>${emptyOption}${history.map(
94
- (revision) => `<option value="${escapeHtml(revision.hash)}"${revision.hash === value ? " selected" : ""}>${escapeHtml(revision.shortHash)} · ${escapeHtml(formatDiffDate(revision.date))}</option>`
95
- ).join("")}</select>`;
96
- }
97
-
98
- // src/components/diff-history.tsx
99
- function renderDiffHistory(input) {
100
- const history = input.revisions;
101
- const selected = input.selected;
102
- const payloadPath = input.payloadPath;
103
- if (history.length === 0 || !selected) return "";
104
- return `<section class="rr-diff-history" data-rr-diff-history>
105
- <details class="rr-diff-history__panel">
106
- <summary class="rr-diff-history__header">
107
- <h2 class="rr-diff-history__title">History</h2>
108
- <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>
109
- </summary>
110
- ${renderContent(history, selected)}
111
- </details>
112
- ${payloadPath ? `<script type="application/json" data-rr-diff-history-data>${JSON.stringify({ path: payloadPath }).replace(/</g, "\\u003c")}</script>` : ""}
113
- </section>`;
114
- }
115
- function renderContent(history, selected) {
116
- return `<div class="rr-diff-history__layout">
117
- <ol class="rr-diff-history__list" aria-label="Commit history">
118
- ${history.map((revision, index) => renderRevisionButton(revision, index === 0)).join("")}
119
- </ol>
120
- ${renderDiffViewer({ history, selected })}
121
- </div>`;
122
- }
123
- function renderRevisionButton(revision, selected) {
124
- return `<li class="rr-diff-history__item">
125
- <button class="rr-diff-history__commit" type="button" data-rr-diff-select="${escapeHtml(revision.hash)}" aria-pressed="${selected}">
126
- <span class="rr-diff-history__date">${escapeHtml(formatDiffDate(revision.date))}</span>
127
- <span class="rr-diff-history__message">${escapeHtml(revision.message || "Untitled change")}</span>
128
- <span class="rr-diff-history__meta">${escapeHtml(revision.shortHash)} · ${escapeHtml(revision.author)}</span>
129
- </button>
130
- </li>`;
131
- }
10
+ import { resolvePlugins } from "@riebeckite/core";
11
+ import { useArticleContext, useSiteContext } from "@riebeckite/ui";
132
12
 
133
13
  // src/diff/line_diff.ts
134
14
  function createLineDiff(fromMarkdown, toMarkdown) {
@@ -217,114 +97,22 @@ function buildLongestCommonSubsequenceTable(fromLines, toLines) {
217
97
  }
218
98
 
219
99
  // src/git/history_reader.ts
220
- import path2 from "node:path";
221
-
222
- // src/git/repo_paths.ts
223
- import path from "node:path";
224
-
225
- // src/git/run_git.ts
226
- import { execFile } from "node:child_process";
227
- import { promisify } from "node:util";
228
- var execFileAsync = promisify(execFile);
229
- function isGitCommandFailure(result) {
230
- return result.ok === false;
231
- }
232
- async function runGit(args, cwd) {
233
- try {
234
- const { stdout } = await execFileAsync(
235
- "git",
236
- ["-c", "core.quotePath=false", ...args],
237
- {
238
- ...cwd === void 0 ? {} : { cwd },
239
- maxBuffer: 1024 * 1024 * 10
240
- }
241
- );
242
- return { ok: true, stdout };
243
- } catch (error) {
244
- return {
245
- ok: false,
246
- spawnError: isSpawnFailure(readErrorCode(error)),
247
- detail: describeGitFailure(error)
248
- };
249
- }
250
- }
251
- function readErrorCode(error) {
252
- return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
253
- }
254
- function isSpawnFailure(code) {
255
- return typeof code === "string" && !code.startsWith("ERR_");
256
- }
257
- function describeGitFailure(error) {
258
- if (typeof error !== "object" || error === null) {
259
- return error instanceof Error ? error.message : String(error);
260
- }
261
- const stderr = "stderr" in error ? error.stderr : void 0;
262
- if (typeof stderr === "string" && stderr.trim()) return stderr.trim();
263
- if (error instanceof Error) return error.message;
264
- return String(error);
265
- }
266
-
267
- // src/git/repo_paths.ts
268
- async function resolveRepositoryPaths(contentRoot) {
269
- const resolvedContentRoot = path.resolve(contentRoot);
270
- const result = await runGit([
271
- "-C",
272
- resolvedContentRoot,
273
- "rev-parse",
274
- "--show-toplevel",
275
- "--show-prefix"
276
- ]);
277
- if (isGitCommandFailure(result)) {
278
- return result.spawnError ? { kind: "git-unavailable", detail: result.detail } : { kind: "content-outside-repository", detail: result.detail };
279
- }
280
- const [toplevel, prefix] = parseRevParseOutput(result.stdout);
281
- if (!toplevel) {
282
- return {
283
- kind: "content-outside-repository",
284
- detail: "git rev-parse reported no work tree root"
285
- };
286
- }
287
- return {
288
- kind: "resolved",
289
- paths: {
290
- repositoryRoot: toPosixPath(path.normalize(toplevel)),
291
- contentRoot: toPosixPath(resolvedContentRoot),
292
- contentPrefix: toPosixPath(prefix).replace(/\/+$/, "")
293
- }
294
- };
295
- }
296
- function toRepositoryPath(paths, contentRelativePath) {
297
- const normalized = normalizeContentPath(contentRelativePath);
298
- if (!paths.contentPrefix) return normalized;
299
- if (!normalized) return paths.contentPrefix;
300
- return `${paths.contentPrefix}/${normalized}`;
301
- }
302
- function normalizeContentPath(value) {
303
- return value.replace(/\\/g, "/").replace(/^\.\//, "");
304
- }
305
- function isResolvedRepository(resolution) {
306
- return resolution.kind === "resolved";
307
- }
308
- function parseRevParseOutput(stdout) {
309
- const [toplevel = "", prefix = ""] = stdout.split(/\r?\n/);
310
- return [toplevel.trim(), prefix.trim()];
311
- }
312
- function toPosixPath(value) {
313
- return value.replace(/\\/g, "/");
314
- }
315
-
316
- // src/git/history_reader.ts
100
+ import {
101
+ GitRepository,
102
+ normalizeContentPath,
103
+ runGit,
104
+ toRepositoryPath
105
+ } from "@riebeckite/git-history";
317
106
  var fieldSeparator = "";
318
107
  var recordSeparator = "";
319
108
  var GitMarkdownHistoryReader = class {
320
- #contentRoot;
109
+ #repository;
321
110
  #historyCache = /* @__PURE__ */ new Map();
322
111
  #markdownCache = /* @__PURE__ */ new Map();
323
112
  /** Maps a content-relative path to `<revision hash>` → work tree path. */
324
113
  #revisionPathCache = /* @__PURE__ */ new Map();
325
- #repository;
326
114
  constructor(options = {}) {
327
- this.#contentRoot = path2.resolve(options.cwd ?? process.cwd());
115
+ this.#repository = new GitRepository(options);
328
116
  }
329
117
  /**
330
118
  * Revision history for one file, newest first, following renames.
@@ -382,29 +170,12 @@ var GitMarkdownHistoryReader = class {
382
170
  );
383
171
  return result.ok ? result.stdout : null;
384
172
  }
385
- async getStateFingerprint() {
386
- const repository = await this.#resolvePaths();
387
- if (!repository) return null;
388
- const results = await Promise.all([
389
- runGit(["rev-parse", "HEAD"], repository.repositoryRoot),
390
- runGit(
391
- ["status", "--porcelain=v1", "--untracked-files=all"],
392
- repository.repositoryRoot
393
- ),
394
- runGit(
395
- ["for-each-ref", "--format=%(refname):%(objectname)"],
396
- repository.repositoryRoot
397
- )
398
- ]);
399
- if (results.some((result) => !result.ok)) return null;
400
- return results.filter((result) => result.ok).map((result) => result.stdout).join("");
173
+ getStateFingerprint() {
174
+ return this.#repository.getStateFingerprint();
401
175
  }
402
176
  /** `null` when the content directory has no readable work tree. */
403
- async #resolvePaths() {
404
- this.#repository ??= resolveRepositoryPaths(this.#contentRoot);
405
- const resolution = await this.#repository;
406
- if (!isResolvedRepository(resolution)) return null;
407
- return resolution.paths;
177
+ #resolvePaths() {
178
+ return this.#repository.paths();
408
179
  }
409
180
  };
410
181
  function parseRevisionRecord(record, revisionPaths, fallbackPath) {
@@ -427,8 +198,212 @@ function getRevisionPath(nameStatusLines, fallbackPath) {
427
198
  return normalizeContentPath(paths.at(-1) ?? fallbackPath);
428
199
  }
429
200
 
201
+ // src/api.ts
202
+ function createPostDiffApi(options = {}) {
203
+ return createPostDiffApiFromReader(new GitMarkdownHistoryReader(options));
204
+ }
205
+ function createPostDiffApiFromReader(reader) {
206
+ return {
207
+ getHistory: (filePath) => reader.getHistory(filePath),
208
+ getRevisionMarkdown: (filePath, hash) => reader.getRevisionMarkdown(filePath, hash),
209
+ getCurrentDiff: async (filePath) => {
210
+ const history = await reader.getHistory(filePath);
211
+ const [current, previous] = history;
212
+ if (!current) return null;
213
+ const toMarkdown = await reader.getRevisionMarkdown(
214
+ filePath,
215
+ current.hash
216
+ );
217
+ if (toMarkdown === null) return null;
218
+ const fromMarkdown = previous ? await reader.getRevisionMarkdown(filePath, previous.hash) : null;
219
+ return buildPostDiff({
220
+ from: previous ?? null,
221
+ fromMarkdown,
222
+ to: current,
223
+ toMarkdown
224
+ });
225
+ },
226
+ compareRevisions: async (input) => {
227
+ const history = await reader.getHistory(input.filePath);
228
+ const from = history.find((revision) => revision.hash === input.fromHash);
229
+ const to = history.find((revision) => revision.hash === input.toHash);
230
+ if (!to) return null;
231
+ const [fromMarkdown, toMarkdown] = await Promise.all([
232
+ input.fromHash ? reader.getRevisionMarkdown(input.filePath, input.fromHash) : Promise.resolve(null),
233
+ reader.getRevisionMarkdown(input.filePath, input.toHash)
234
+ ]);
235
+ if (toMarkdown === null) return null;
236
+ return buildPostDiff({
237
+ from: from ?? null,
238
+ fromMarkdown,
239
+ to,
240
+ toMarkdown
241
+ });
242
+ }
243
+ };
244
+ }
245
+ async function getMarkdownRevisions(api, filePath, history) {
246
+ const revisions = await Promise.all(
247
+ history.map(async (revision) => ({
248
+ revision,
249
+ markdown: await api.getRevisionMarkdown(filePath, revision.hash)
250
+ }))
251
+ );
252
+ return revisions.flatMap(
253
+ ({ revision, markdown }) => markdown === null ? [] : [{ ...revision, markdown }]
254
+ );
255
+ }
256
+ function buildPostDiff(input) {
257
+ return {
258
+ from: input.from,
259
+ to: input.to,
260
+ lines: createLineDiff(input.fromMarkdown ?? "", input.toMarkdown)
261
+ };
262
+ }
263
+
264
+ // components/post-diff.tsx
265
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
266
+ var PostDiff = (({ diff: diff2, title = "History" }) => {
267
+ if (!diff2) return null;
268
+ return /* @__PURE__ */ jsxs("section", { class: "rr-diff-history", "data-rr-diff-history": true, children: [
269
+ /* @__PURE__ */ jsx("h2", { class: "rr-diff-history__title", children: title }),
270
+ /* @__PURE__ */ jsxs("p", { class: "rr-diff-history__meta", children: [
271
+ diff2.from ? `${diff2.from.shortHash} → ` : "",
272
+ diff2.to.shortHash
273
+ ] }),
274
+ /* @__PURE__ */ jsx("pre", { class: "rr-diff-history__diff", children: /* @__PURE__ */ jsx("code", { children: diff2.lines.map((line, index) => /* @__PURE__ */ jsxs(
275
+ "span",
276
+ {
277
+ class: `rr-diff-line rr-diff-line--${line.type}`,
278
+ children: [
279
+ line.type === "added" ? "+" : line.type === "removed" ? "-" : " ",
280
+ line.content,
281
+ "\n"
282
+ ]
283
+ },
284
+ `${line.oldLineNumber}-${line.newLineNumber}-${index}`
285
+ )) }) })
286
+ ] });
287
+ });
288
+ var post_diff_default = PostDiff;
289
+ async function ArticlePostDiffView(props) {
290
+ const site = useSiteContext();
291
+ const article = useArticleContext();
292
+ if (!site || !article?.entry) return [];
293
+ const options = findDiffOptions(site.config.plugins);
294
+ if (!options || options.ui?.enabled === false) return [];
295
+ const filePath = site.manifest.contentIndex.get(article.entry.slug.toLowerCase()) ?? `${article.entry.slug}.md`;
296
+ const diff2 = await createPostDiffApi({
297
+ ...options,
298
+ cwd: options.cwd ?? site.config.content.directory
299
+ }).getCurrentDiff(filePath);
300
+ return /* @__PURE__ */ jsx(PostDiff, { diff: diff2, title: props.title });
301
+ }
302
+ function findDiffOptions(plugins) {
303
+ return resolvePlugins([...plugins]).find((plugin) => plugin.name === "diff")?.options;
304
+ }
305
+
306
+ // src/components/diff-line.tsx
307
+ function renderDiffLine({ line }) {
308
+ const prefix = line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
309
+ return `<tr class="rr-diff-history__line rr-diff-history__line--${line.type}">
310
+ <td class="rr-diff-history__num">${line.oldLineNumber ?? ""}</td>
311
+ <td class="rr-diff-history__num">${line.newLineNumber ?? ""}</td>
312
+ <td class="rr-diff-history__code"><code>${prefix} ${escapeHtml(line.content)}</code></td>
313
+ </tr>`;
314
+ }
315
+ function escapeHtml(value) {
316
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
317
+ }
318
+
319
+ // src/components/format-date.ts
320
+ var MONTHS = [
321
+ "Jan",
322
+ "Feb",
323
+ "Mar",
324
+ "Apr",
325
+ "May",
326
+ "Jun",
327
+ "Jul",
328
+ "Aug",
329
+ "Sep",
330
+ "Oct",
331
+ "Nov",
332
+ "Dec"
333
+ ];
334
+ function formatDiffDate(value) {
335
+ const date = new Date(value);
336
+ if (Number.isNaN(date.getTime())) return value;
337
+ return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
338
+ }
339
+
340
+ // src/components/diff-viewer.tsx
341
+ function renderDiffViewer({
342
+ history,
343
+ selected
344
+ }) {
345
+ return `<div class="rr-diff-history__viewer">
346
+ <div class="rr-diff-history__controls">
347
+ <label>From ${renderRevisionSelect("from", history, selected.from?.hash ?? "")}</label>
348
+ <label>To ${renderRevisionSelect("to", history, selected.to.hash)}</label>
349
+ </div>
350
+ <div data-rr-diff-panel>${renderDiffPanel(selected)}</div>
351
+ </div>`;
352
+ }
353
+ function renderDiffPanel(diff2) {
354
+ return `<div class="rr-diff-history__diff" data-rr-diff-rendered>
355
+ <div class="rr-diff-history__table-wrap">
356
+ <table class="rr-diff-history__table">
357
+ <tbody>${diff2.lines.map((line) => renderDiffLine({ line })).join("")}</tbody>
358
+ </table>
359
+ </div>
360
+ </div>`;
361
+ }
362
+ function renderRevisionSelect(kind, history, value) {
363
+ const emptyOption = kind === "from" ? `<option value="">Beginning</option>` : "";
364
+ return `<select data-rr-diff-${kind}>${emptyOption}${history.map(
365
+ (revision) => `<option value="${escapeHtml(revision.hash)}"${revision.hash === value ? " selected" : ""}>${escapeHtml(revision.shortHash)} · ${escapeHtml(formatDiffDate(revision.date))}</option>`
366
+ ).join("")}</select>`;
367
+ }
368
+
369
+ // src/components/diff-history.tsx
370
+ function renderDiffHistory(input) {
371
+ const history = input.revisions;
372
+ const selected = input.selected;
373
+ const payloadPath = input.payloadPath;
374
+ if (history.length === 0 || !selected) return "";
375
+ return `<section class="rr-diff-history" data-rr-diff-history>
376
+ <details class="rr-diff-history__panel">
377
+ <summary class="rr-diff-history__header">
378
+ <h2 class="rr-diff-history__title">History</h2>
379
+ <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>
380
+ </summary>
381
+ ${renderContent(history, selected)}
382
+ </details>
383
+ ${payloadPath ? `<script type="application/json" data-rr-diff-history-data>${JSON.stringify({ path: payloadPath }).replace(/</g, "\\u003c")}</script>` : ""}
384
+ </section>`;
385
+ }
386
+ function renderContent(history, selected) {
387
+ return `<div class="rr-diff-history__layout">
388
+ <ol class="rr-diff-history__list" aria-label="Commit history">
389
+ ${history.map((revision, index) => renderRevisionButton(revision, index === 0)).join("")}
390
+ </ol>
391
+ ${renderDiffViewer({ history, selected })}
392
+ </div>`;
393
+ }
394
+ function renderRevisionButton(revision, selected) {
395
+ return `<li class="rr-diff-history__item">
396
+ <button class="rr-diff-history__commit" type="button" data-rr-diff-select="${escapeHtml(revision.hash)}" aria-pressed="${selected}">
397
+ <span class="rr-diff-history__date">${escapeHtml(formatDiffDate(revision.date))}</span>
398
+ <span class="rr-diff-history__message">${escapeHtml(revision.message || "Untitled change")}</span>
399
+ <span class="rr-diff-history__meta">${escapeHtml(revision.shortHash)} · ${escapeHtml(revision.author)}</span>
400
+ </button>
401
+ </li>`;
402
+ }
403
+
430
404
  // index.ts
431
405
  var getPostDiff = ({ api, filePath }) => api.getCurrentDiff(filePath);
406
+ var manifestConcurrency = 12;
432
407
  function diff(options = {}) {
433
408
  const ui = { enabled: true, maxRevisions: 20, ...options.ui };
434
409
  let api;
@@ -486,28 +461,40 @@ function diff(options = {}) {
486
461
  return;
487
462
  }
488
463
  api ??= createPostDiffApiFromReader(snapshot.value);
489
- for (const entry of context.manifest.discoverableEntries) {
490
- const filePath = resolvePostFilePath({
491
- slug: entry.slug,
492
- contentIndex: context.contentIndex
493
- });
494
- const history = (await api.getHistory(filePath)).slice(
495
- 0,
496
- ui.maxRevisions
464
+ const postDiffApi = api;
465
+ const entries = context.manifest.discoverableEntries;
466
+ for (let index = 0; index < entries.length; index += manifestConcurrency) {
467
+ const outputs = await Promise.all(
468
+ entries.slice(index, index + manifestConcurrency).map(async (entry) => {
469
+ const filePath = resolvePostFilePath({
470
+ slug: entry.slug,
471
+ contentIndex: context.contentIndex
472
+ });
473
+ const history = (await postDiffApi.getHistory(filePath)).slice(
474
+ 0,
475
+ ui.maxRevisions
476
+ );
477
+ const revisions = await getMarkdownRevisions(
478
+ postDiffApi,
479
+ filePath,
480
+ history
481
+ );
482
+ if (revisions.length === 0) return void 0;
483
+ const selected = await postDiffApi.getCurrentDiff(filePath);
484
+ if (!selected) return void 0;
485
+ return {
486
+ path: diffPayloadPath(entry.slug),
487
+ content: JSON.stringify({ revisions }).replace(/</g, "\\u003c"),
488
+ dependencies: [
489
+ { type: "content", slug: entry.slug },
490
+ { type: "external", id: "diff:git-history" }
491
+ ]
492
+ };
493
+ })
497
494
  );
498
- const revisions = await getMarkdownRevisions(api, filePath, history);
499
- if (revisions.length === 0) continue;
500
- const selected = await api.getCurrentDiff(filePath);
501
- if (!selected) continue;
502
- const payloadPath = diffPayloadPath(entry.slug);
503
- context.output.emit({
504
- path: payloadPath,
505
- content: JSON.stringify({ revisions }).replace(/</g, "\\u003c"),
506
- dependencies: [
507
- { type: "content", slug: entry.slug },
508
- { type: "external", id: "diff:git-history" }
509
- ]
510
- });
495
+ for (const output of outputs) {
496
+ if (output) context.output.emit(output);
497
+ }
511
498
  }
512
499
  }
513
500
  });
@@ -515,73 +502,13 @@ function diff(options = {}) {
515
502
  function diffPayloadPath(slug) {
516
503
  return `_riebeckite/diff/${Buffer.from(slug).toString("base64url")}.json`;
517
504
  }
518
- function createPostDiffApi(options = {}) {
519
- return createPostDiffApiFromReader(new GitMarkdownHistoryReader(options));
520
- }
521
- function createPostDiffApiFromReader(reader) {
522
- return {
523
- getHistory: (filePath) => reader.getHistory(filePath),
524
- getRevisionMarkdown: (filePath, hash) => reader.getRevisionMarkdown(filePath, hash),
525
- getCurrentDiff: async (filePath) => {
526
- const history = await reader.getHistory(filePath);
527
- const [current, previous] = history;
528
- if (!current) return null;
529
- const toMarkdown = await reader.getRevisionMarkdown(
530
- filePath,
531
- current.hash
532
- );
533
- if (toMarkdown === null) return null;
534
- const fromMarkdown = previous ? await reader.getRevisionMarkdown(filePath, previous.hash) : null;
535
- return buildPostDiff({
536
- from: previous ?? null,
537
- fromMarkdown,
538
- to: current,
539
- toMarkdown
540
- });
541
- },
542
- compareRevisions: async (input) => {
543
- const history = await reader.getHistory(input.filePath);
544
- const from = history.find((revision) => revision.hash === input.fromHash);
545
- const to = history.find((revision) => revision.hash === input.toHash);
546
- if (!to) return null;
547
- const [fromMarkdown, toMarkdown] = await Promise.all([
548
- input.fromHash ? reader.getRevisionMarkdown(input.filePath, input.fromHash) : Promise.resolve(null),
549
- reader.getRevisionMarkdown(input.filePath, input.toHash)
550
- ]);
551
- if (toMarkdown === null) return null;
552
- return buildPostDiff({
553
- from: from ?? null,
554
- fromMarkdown,
555
- to,
556
- toMarkdown
557
- });
558
- }
559
- };
560
- }
561
- async function getMarkdownRevisions(api, filePath, history) {
562
- const revisions = await Promise.all(
563
- history.map(async (revision) => ({
564
- revision,
565
- markdown: await api.getRevisionMarkdown(filePath, revision.hash)
566
- }))
567
- );
568
- return revisions.flatMap(
569
- ({ revision, markdown }) => markdown === null ? [] : [{ ...revision, markdown }]
570
- );
571
- }
572
505
  function resolvePostFilePath(context) {
573
506
  const indexedPath = context.contentIndex.get(context.slug.toLowerCase());
574
507
  const contentPath = `${indexedPath ?? context.slug}.md`;
575
508
  return contentPath;
576
509
  }
577
- function buildPostDiff(input) {
578
- return {
579
- from: input.from,
580
- to: input.to,
581
- lines: createLineDiff(input.fromMarkdown ?? "", input.toMarkdown)
582
- };
583
- }
584
510
  export {
511
+ ArticlePostDiffView,
585
512
  GitMarkdownHistoryReader,
586
513
  post_diff_default as PostDiffView,
587
514
  createLineDiff,
@@ -0,0 +1,11 @@
1
+ import { GitMarkdownHistoryReader } from "./git/history_reader.js";
2
+ import type { DiffRevision, GitHistoryReaderOptions, MarkdownRevision, PostDiff, RevisionComparisonInput } from "./types.js";
3
+ export type PostDiffApi = {
4
+ getHistory(filePath: string): Promise<DiffRevision[]>;
5
+ getRevisionMarkdown(filePath: string, hash: string): Promise<string | null>;
6
+ getCurrentDiff(filePath: string): Promise<PostDiff | null>;
7
+ compareRevisions(input: RevisionComparisonInput): Promise<PostDiff | null>;
8
+ };
9
+ export declare function createPostDiffApi(options?: GitHistoryReaderOptions): PostDiffApi;
10
+ export declare function createPostDiffApiFromReader(reader: GitMarkdownHistoryReader): PostDiffApi;
11
+ export declare function getMarkdownRevisions(api: PostDiffApi, filePath: string, history: DiffRevision[]): Promise<MarkdownRevision[]>;
@@ -1,7 +1,8 @@
1
1
  import type { MarkdownRevision, PostDiff } from "../types.js";
2
- export type DiffHistoryProps = {
2
+ type DiffHistoryProps = {
3
3
  revisions: MarkdownRevision[];
4
4
  selected: PostDiff | null;
5
5
  payloadPath?: string;
6
6
  };
7
7
  export declare function renderDiffHistory(input: DiffHistoryProps): string;
8
+ export {};
@@ -1,6 +1,7 @@
1
1
  import type { PostDiff } from "../types.js";
2
- export type DiffLineProps = {
2
+ type DiffLineProps = {
3
3
  line: PostDiff["lines"][number];
4
4
  };
5
5
  export declare function renderDiffLine({ line }: DiffLineProps): string;
6
6
  export declare function escapeHtml(value: string): string;
7
+ export {};
@@ -1,7 +1,8 @@
1
1
  import type { DiffRevision, PostDiff } from "../types.js";
2
- export type DiffViewerProps = {
2
+ type DiffViewerProps = {
3
3
  history: DiffRevision[];
4
4
  selected: PostDiff;
5
5
  };
6
6
  export declare function renderDiffViewer({ history, selected, }: DiffViewerProps): string;
7
7
  export declare function renderDiffPanel(diff: PostDiff): string;
8
+ export {};
@@ -38,3 +38,6 @@ export type DiffPluginUiOptions = {
38
38
  enabled?: boolean;
39
39
  maxRevisions?: number;
40
40
  };
41
+ export type DiffPluginOptions = GitHistoryReaderOptions & {
42
+ ui?: DiffPluginUiOptions;
43
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-diff",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,13 +27,15 @@
27
27
  "./style.css": "./style.css"
28
28
  },
29
29
  "dependencies": {
30
- "hono": "^4.12.25",
31
- "@riebeckite/core": "0.0.32"
30
+ "@riebeckite/core": "0.0.33",
31
+ "@riebeckite/git-history": "0.0.33",
32
+ "@riebeckite/ui": "0.0.33",
33
+ "hono": "^4.12.25"
32
34
  },
33
35
  "devDependencies": {
36
+ "@riebeckite/test": "0.0.33",
34
37
  "@types/node": "^24.5.2",
35
- "tsx": "^4.22.4",
36
- "@riebeckite/test": "0.0.32"
38
+ "tsx": "^4.22.4"
37
39
  },
38
40
  "publishConfig": {
39
41
  "access": "public"
@@ -1,76 +0,0 @@
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
- }>;
@@ -1,43 +0,0 @@
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>;