@riebeckite/plugin-changelog 0.0.14 → 0.0.16

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 CHANGED
@@ -44,7 +44,7 @@ export default defineConfig({
44
44
 
45
45
  | Option | Type | Default | Description |
46
46
  | ------ | ---- | ------- | ----------- |
47
- | `cwd` | `string` | `process.cwd()` | Working directory for Git commands |
47
+ | `cwd` | `string` | `config.content.directory`, else `process.cwd()` | Content root used to locate the Git work tree |
48
48
  | `lookbackDays` | `number` | — (full history) | Only include commits newer than this many days |
49
49
  | `dateFormat` | `"iso" \| "long" \| "short"` | `"iso"` | How dates are rendered |
50
50
  | `locale` | `string` | `"en"` | Locale for `"long"` / `"short"` dates |
@@ -114,10 +114,12 @@ exported HTML directly.
114
114
 
115
115
  ## Failure behavior
116
116
 
117
- When `git` is missing or `cwd` is not a Git repository, the plugin reports a
118
- `changelog-git-unavailable` warning diagnostic and a logger warning, then leaves
119
- the build output untouched. The build never fails because history is
120
- unavailable, and the same guarantee holds for an empty repository.
117
+ When `git` cannot be started, the plugin reports a `changelog-git-unavailable`
118
+ warning diagnostic. When the content directory is not inside a Git working
119
+ tree, it reports `changelog-content-outside-repository`. Either way a logger
120
+ warning accompanies it and the build output is left untouched. The build never
121
+ fails because history is unavailable, and the same guarantee holds for an empty
122
+ repository.
121
123
 
122
124
  ## Style
123
125
 
@@ -150,4 +152,5 @@ import "@riebeckite/plugin-changelog/style.css";
150
152
  ## See also
151
153
 
152
154
  - [plugin-diff](../diff/README.md) — revision history and line diffs
153
- - [Plugin guide](../../../docs/en/reference/plugin-api.md)
155
+ - [Plugin guide](../../../docs/en/docs/reference/plugin-api.md)
156
+
package/README_ja.md CHANGED
@@ -33,7 +33,7 @@ export default defineConfig({
33
33
 
34
34
  | オプション | 型 | 初期値 | 説明 |
35
35
  | --- | --- | --- | --- |
36
- | `cwd` | `string` | `process.cwd()` | Git コマンドを実行するディレクトリ |
36
+ | `cwd` | `string` | `config.content.directory`、無ければ `process.cwd()` | Git リポジトリを探すためのコンテンツルート |
37
37
  | `lookbackDays` | `number` | なし(全履歴) | 直近この日数以内のコミットだけを含める |
38
38
  | `dateFormat` | `"iso" \| "long" \| "short"` | `"iso"` | 日付の表示形式 |
39
39
  | `locale` | `string` | `"en"` | `"long"` / `"short"` のロケール |
@@ -97,7 +97,7 @@ const html = renderSiteChangelog(dataset, options);
97
97
 
98
98
  ## 失敗時の挙動
99
99
 
100
- `git` が無い、または `cwd` が Git リポジトリでない場合、プラグインは `changelog-git-unavailable` 警告診断とロガー警告を出し、ビルド成果物には何も加えません。履歴が取得できないことを理由にビルドが失敗することはなく、空のリポジトリでも同じです。
100
+ `git` を起動できない場合は `changelog-git-unavailable` 警告診断、コンテンツディレクトリが Git ワーキングツリーの外にある場合は `changelog-content-outside-repository` 警告診断を出し、どちらの場合もロガー警告を伴い、ビルド成果物には何も加えません。履歴が取得できないことを理由にビルドが失敗することはなく、空のリポジトリでも同じです。
101
101
 
102
102
  ## スタイル
103
103
 
@@ -122,4 +122,5 @@ import "@riebeckite/plugin-changelog/style.css";
122
122
  ## 関連資料
123
123
 
124
124
  - [plugin-diff](../diff/README_ja.md) — リビジョン履歴と行差分
125
- - [プラグインシステム](../../../docs/ja/reference/plugin-api.md)
125
+ - [プラグインシステム](../../../docs/ja/docs/reference/plugin-api.md)
126
+
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ChangelogOptions } from "./src/types.js";
2
- export { buildNoteChangeHistory, buildSiteChangelog, filterChangelogCommits, formatChangelogDate, resolveLookbackSince, } from "./src/changelog.js";
2
+ export { buildNoteChangeHistory, buildSiteChangelog, filterChangelogCommits, formatChangelogDate, resolveContentFilePath, resolveLookbackSince, } from "./src/changelog.js";
3
3
  export { GitChangelogReader, type GitLogWindow, } from "./src/git/history_reader.js";
4
4
  export { DEFAULT_CHANGELOG_CLASS_NAME, DEFAULT_CHANGELOG_HEADING, DEFAULT_CHANGELOG_LOCALE, DEFAULT_CHANGELOG_MAX_PER_NOTE, DEFAULT_CHANGELOG_MAX_SITE_WIDE, DEFAULT_CHANGELOG_PER_NOTE_HEADING, DEFAULT_CHANGELOG_SITE_WIDE_HEADING, DEFAULT_CHANGELOG_SITE_WIDE_SLUG, resolveChangelogOptions, } from "./src/options.js";
5
5
  export { CHANGELOG_NOTE_ATTRIBUTE, CHANGELOG_SITE_ATTRIBUTE, renderNoteChangeHistory, renderSiteChangelog, } from "./src/render.js";
package/dist/index.js CHANGED
@@ -71,11 +71,15 @@ function resolveCommitNotes(commit, byPath, bySlug) {
71
71
  function invertContentIndex(contentIndex, entries) {
72
72
  const byPath = /* @__PURE__ */ new Map();
73
73
  for (const entry of entries) {
74
- const path = contentIndex.get(entry.slug.toLowerCase());
75
- if (path) byPath.set(normalizePath(path), entry.slug);
74
+ const path3 = resolveContentFilePath(contentIndex, entry.slug);
75
+ if (path3) byPath.set(path3, entry.slug);
76
76
  }
77
77
  return byPath;
78
78
  }
79
+ function resolveContentFilePath(contentIndex, slug) {
80
+ const indexedPath = contentIndex.get(slug.toLowerCase());
81
+ return indexedPath ? `${normalizePath(indexedPath)}.md` : null;
82
+ }
79
83
  function resolveCutoff(lookbackDays) {
80
84
  if (lookbackDays === void 0) return null;
81
85
  return Date.now() - lookbackDays * 24 * 60 * 60 * 1e3;
@@ -90,23 +94,143 @@ function normalizePath(filePath) {
90
94
  }
91
95
 
92
96
  // src/git/history_reader.ts
97
+ import path2 from "node:path";
98
+
99
+ // src/git/repo_paths.ts
100
+ import path from "node:path";
101
+
102
+ // src/git/run_git.ts
93
103
  import { execFile } from "node:child_process";
94
104
  import { promisify } from "node:util";
95
105
  var execFileAsync = promisify(execFile);
106
+ function isGitCommandFailure(result) {
107
+ return result.ok === false;
108
+ }
109
+ async function runGit(args, cwd) {
110
+ try {
111
+ const { stdout } = await execFileAsync(
112
+ "git",
113
+ ["-c", "core.quotePath=false", ...args],
114
+ {
115
+ ...cwd === void 0 ? {} : { cwd },
116
+ maxBuffer: 1024 * 1024 * 10
117
+ }
118
+ );
119
+ return { ok: true, stdout };
120
+ } catch (error) {
121
+ return {
122
+ ok: false,
123
+ spawnError: isSpawnFailure(readErrorCode(error)),
124
+ detail: describeGitFailure(error)
125
+ };
126
+ }
127
+ }
128
+ function readErrorCode(error) {
129
+ return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
130
+ }
131
+ function isSpawnFailure(code) {
132
+ return typeof code === "string" && !code.startsWith("ERR_");
133
+ }
134
+ function describeGitFailure(error) {
135
+ if (typeof error !== "object" || error === null) {
136
+ return error instanceof Error ? error.message : String(error);
137
+ }
138
+ const stderr = "stderr" in error ? error.stderr : void 0;
139
+ if (typeof stderr === "string" && stderr.trim()) return stderr.trim();
140
+ if (error instanceof Error) return error.message;
141
+ return String(error);
142
+ }
143
+
144
+ // src/git/repo_paths.ts
145
+ async function resolveRepositoryPaths(contentRoot) {
146
+ const resolvedContentRoot = path.resolve(contentRoot);
147
+ const result = await runGit([
148
+ "-C",
149
+ resolvedContentRoot,
150
+ "rev-parse",
151
+ "--show-toplevel",
152
+ "--show-prefix"
153
+ ]);
154
+ if (isGitCommandFailure(result)) {
155
+ return result.spawnError ? { kind: "git-unavailable", detail: result.detail } : { kind: "content-outside-repository", detail: result.detail };
156
+ }
157
+ const [toplevel, prefix] = parseRevParseOutput(result.stdout);
158
+ if (!toplevel) {
159
+ return {
160
+ kind: "content-outside-repository",
161
+ detail: "git rev-parse reported no work tree root"
162
+ };
163
+ }
164
+ return {
165
+ kind: "resolved",
166
+ paths: {
167
+ repositoryRoot: toPosixPath(path.normalize(toplevel)),
168
+ contentRoot: toPosixPath(resolvedContentRoot),
169
+ contentPrefix: toPosixPath(prefix).replace(/\/+$/, "")
170
+ }
171
+ };
172
+ }
173
+ function toRepositoryPath(paths, contentRelativePath) {
174
+ const normalized = normalizeContentPath(contentRelativePath);
175
+ if (!paths.contentPrefix) return normalized;
176
+ if (!normalized) return paths.contentPrefix;
177
+ return `${paths.contentPrefix}/${normalized}`;
178
+ }
179
+ function toContentPath(paths, repositoryRelativePath) {
180
+ const normalized = normalizeContentPath(repositoryRelativePath);
181
+ if (!normalized) return null;
182
+ if (!paths.contentPrefix) return normalized;
183
+ if (!normalized.startsWith(`${paths.contentPrefix}/`)) return null;
184
+ return normalized.slice(paths.contentPrefix.length + 1);
185
+ }
186
+ function normalizeContentPath(value) {
187
+ return value.replace(/\\/g, "/").replace(/^\.\//, "");
188
+ }
189
+ function isResolvedRepository(resolution) {
190
+ return resolution.kind === "resolved";
191
+ }
192
+ function isRepositoryUnavailable(resolution) {
193
+ return resolution.kind !== "resolved";
194
+ }
195
+ function parseRevParseOutput(stdout) {
196
+ const [toplevel = "", prefix = ""] = stdout.split(/\r?\n/);
197
+ return [toplevel.trim(), prefix.trim()];
198
+ }
199
+ function toPosixPath(value) {
200
+ return value.replace(/\\/g, "/");
201
+ }
202
+
203
+ // src/git/history_reader.ts
96
204
  var fieldSeparator = "";
97
205
  var recordSeparator = "";
98
206
  var logFormat = ["%H", "%h", "%cI", "%s", "%an"].join(fieldSeparator);
99
207
  var GitChangelogReader = class {
100
- #cwd;
208
+ #contentRoot;
101
209
  #fileCache = /* @__PURE__ */ new Map();
102
210
  #recentCache;
103
- #repositoryAvailable;
211
+ #repository;
104
212
  constructor(options = {}) {
105
- this.#cwd = options.cwd ?? process.cwd();
213
+ this.#contentRoot = path2.resolve(options.cwd ?? process.cwd());
106
214
  }
107
- /** Commit history for one file, newest first, following renames. */
215
+ /**
216
+ * Resolves the Git work tree that owns the content directory, or the reason
217
+ * it cannot be resolved. Memoized: every Git query reuses the same answer.
218
+ */
219
+ resolveRepository() {
220
+ this.#repository ??= resolveRepositoryPaths(this.#contentRoot);
221
+ return this.#repository;
222
+ }
223
+ /** True when the content directory lives inside a Git work tree. */
224
+ async isAvailable() {
225
+ return isResolvedRepository(await this.resolveRepository());
226
+ }
227
+ /**
228
+ * Commit history for one file, newest first, following renames.
229
+ *
230
+ * @param filePath Content-relative path, e.g. `notes/hello.md`.
231
+ */
108
232
  getFileHistory(filePath, window = {}) {
109
- const normalizedPath = normalizeGitPath(filePath);
233
+ const normalizedPath = normalizeContentPath(filePath);
110
234
  if (window.since)
111
235
  return this.#readFileHistory(normalizedPath, window.since);
112
236
  const cached = this.#fileCache.get(normalizedPath);
@@ -116,43 +240,46 @@ var GitChangelogReader = class {
116
240
  return history;
117
241
  }
118
242
  /**
119
- * Recent commits that touch the working directory, newest first, each with
120
- * the paths it changed.
243
+ * Recent commits that touch the content directory, newest first, each with
244
+ * the content-relative paths it changed.
121
245
  */
122
246
  getRecentCommits(window = {}) {
123
247
  if (window.since) return this.#readRecentCommits(window.since);
124
248
  this.#recentCache ??= this.#readRecentCommits();
125
249
  return this.#recentCache;
126
250
  }
127
- /** True when `cwd` is inside a Git working tree. */
128
- async isAvailable() {
129
- this.#repositoryAvailable ??= runGit(this.#cwd, [
130
- "rev-parse",
131
- "--is-inside-work-tree"
132
- ]).then((result) => result.ok && result.stdout.trim() === "true");
133
- return this.#repositoryAvailable;
134
- }
135
251
  async #readFileHistory(filePath, since) {
136
- if (!await this.isAvailable()) return [];
252
+ const repository = await this.#resolvePaths();
253
+ if (!repository) return [];
137
254
  const args = ["log", "--follow", `--format=${recordSeparator}${logFormat}`];
138
255
  if (since) args.push(`--since=${since}`);
139
- args.push("--", filePath);
140
- const result = await runGit(this.#cwd, args);
256
+ args.push("--", toRepositoryPath(repository, filePath));
257
+ const result = await runGit(args, repository.repositoryRoot);
141
258
  if (!result.ok) return [];
142
259
  return parseCommitRecords(result.stdout);
143
260
  }
144
261
  async #readRecentCommits(since) {
145
- if (!await this.isAvailable()) return [];
262
+ const repository = await this.#resolvePaths();
263
+ if (!repository) return [];
146
264
  const args = [
147
265
  "log",
148
266
  `--format=${recordSeparator}${logFormat}`,
149
267
  "--name-only"
150
268
  ];
151
269
  if (since) args.push(`--since=${since}`);
152
- args.push("--", ".");
153
- const result = await runGit(this.#cwd, args);
270
+ args.push("--", repository.contentPrefix || ".");
271
+ const result = await runGit(args, repository.repositoryRoot);
154
272
  if (!result.ok) return [];
155
- return parseCommitRecords(result.stdout);
273
+ return parseCommitRecords(result.stdout).map((commit) => ({
274
+ ...commit,
275
+ files: commit.files.map((file) => toContentPath(repository, file)).filter((file) => file !== null)
276
+ }));
277
+ }
278
+ /** `null` when the content directory has no readable work tree. */
279
+ async #resolvePaths() {
280
+ const resolution = await this.resolveRepository();
281
+ if (!isResolvedRepository(resolution)) return null;
282
+ return resolution.paths;
156
283
  }
157
284
  };
158
285
  function parseCommitRecords(stdout) {
@@ -169,24 +296,9 @@ function parseCommitRecord(chunk) {
169
296
  date,
170
297
  subject: subject ?? "",
171
298
  author: author ?? "",
172
- files: rest.map((line) => line.trim()).filter(Boolean).map(normalizeGitPath)
299
+ files: rest.map((line) => line.trim()).filter(Boolean).map(normalizeContentPath)
173
300
  };
174
301
  }
175
- function normalizeGitPath(filePath) {
176
- return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
177
- }
178
- async function runGit(cwd, args) {
179
- try {
180
- const { stdout } = await execFileAsync("git", args, {
181
- cwd,
182
- maxBuffer: 1024 * 1024 * 10
183
- });
184
- return { ok: true, stdout };
185
- } catch (error) {
186
- const stdout = typeof error === "object" && error !== null && "stdout" in error && typeof error.stdout === "string" ? error.stdout : "";
187
- return { ok: false, stdout };
188
- }
189
- }
190
302
 
191
303
  // src/options.ts
192
304
  var DEFAULT_CHANGELOG_DATE_FORMAT = "iso";
@@ -278,6 +390,7 @@ function changelog(options = {}) {
278
390
  const resolved = resolveChangelogOptions(options);
279
391
  return definePlugin({
280
392
  name: CHANGELOG_PLUGIN_NAME,
393
+ outputDependencies: [{ type: "unknown" }],
281
394
  options,
282
395
  validateOptions: validateChangelogOptions,
283
396
  onManifestCreated: (context) => applyChangelog(context, resolved),
@@ -287,9 +400,12 @@ function changelog(options = {}) {
287
400
  var changelogPlugin = changelog;
288
401
  async function applyChangelog(context, options) {
289
402
  if (!options.perNote && !options.siteWide) return;
290
- const reader = new GitChangelogReader({ cwd: options.cwd });
291
- if (!await reader.isAvailable()) {
292
- reportGitUnavailable(context);
403
+ const reader = new GitChangelogReader({
404
+ cwd: resolveContentRoot(context, options)
405
+ });
406
+ const resolution = await reader.resolveRepository();
407
+ if (isRepositoryUnavailable(resolution)) {
408
+ reportRepositoryUnavailable(context, resolution);
293
409
  return;
294
410
  }
295
411
  const since = resolveLookbackSince(options.lookbackDays);
@@ -341,19 +457,23 @@ async function applySiteChangelog(context, options, reader, since) {
341
457
  if (html) appendContentBodySlot(target, CHANGELOG_BODY_SLOT, html);
342
458
  }
343
459
  function resolveEntryFilePath(entry, contentIndex) {
344
- const indexedPath = contentIndex.get(entry.slug.toLowerCase());
345
- return indexedPath ?? `${entry.slug}.md`;
460
+ return resolveContentFilePath(contentIndex, entry.slug) ?? `${entry.slug}.md`;
346
461
  }
347
462
  function hasSlot(entry, attribute) {
348
463
  return entry.bodySlots?.[CHANGELOG_BODY_SLOT]?.includes(attribute) ?? false;
349
464
  }
350
- function reportGitUnavailable(context) {
351
- const message = "changelog: Git is unavailable or this directory is not a Git repository; change history was skipped.";
465
+ function resolveContentRoot(context, options) {
466
+ return options.cwd ?? context.config?.content.directory ?? process.cwd();
467
+ }
468
+ function reportRepositoryUnavailable(context, resolution) {
469
+ const gitUnavailable = resolution.kind === "git-unavailable";
470
+ const message = gitUnavailable ? `changelog: Git could not be started; change history was skipped. (${resolution.detail})` : `changelog: the content directory is not inside a Git working tree; change history was skipped. (${resolution.detail})`;
352
471
  context.diagnostics.push({
353
- code: "changelog-git-unavailable",
472
+ code: gitUnavailable ? "changelog-git-unavailable" : "changelog-content-outside-repository",
354
473
  severity: "warning",
355
474
  pluginName: CHANGELOG_PLUGIN_NAME,
356
- message
475
+ message,
476
+ suggestion: gitUnavailable ? "Install Git and make sure it is available on PATH." : "Move the content directory inside a Git working tree, or set the plugin's cwd option to a path inside one."
357
477
  });
358
478
  context.logger.warn(message);
359
479
  }
@@ -441,5 +561,6 @@ export {
441
561
  renderNoteChangeHistory,
442
562
  renderSiteChangelog,
443
563
  resolveChangelogOptions,
564
+ resolveContentFilePath,
444
565
  resolveLookbackSince
445
566
  };
@@ -25,3 +25,13 @@ export declare function buildSiteChangelog(args: {
25
25
  contentIndex: Map<string, string>;
26
26
  options: ResolvedChangelogOptions;
27
27
  }): SiteChangelog;
28
+ /**
29
+ * Resolves the content-relative file path of a note from the content index.
30
+ *
31
+ * Index values drop the `.md` extension of Markdown notes, so it is restored
32
+ * here: Git needs the real on-disk path, not the slug-shaped key the index is
33
+ * built for.
34
+ *
35
+ * Returns `null` when the slug is not in the index.
36
+ */
37
+ export declare function resolveContentFilePath(contentIndex: Map<string, string>, slug: string): string | null;
@@ -1,25 +1,39 @@
1
1
  import type { ChangelogCommit, GitChangelogReaderOptions } from "../types.js";
2
+ import { type RepositoryPathsResolution } from "./repo_paths.js";
2
3
  /** Optional window applied to a Git query. */
3
4
  export type GitLogWindow = {
4
5
  /** ISO timestamp passed to `git log --since`. */
5
6
  since?: string;
6
7
  };
7
8
  /**
8
- * Reads local Git history for individual files and for the whole working
9
+ * Reads local Git history for individual files and for the whole content
9
10
  * directory. Mirrors the Git access used by `@riebeckite/plugin-diff`: it runs
10
11
  * `git` through `execFile` and degrades to empty results when Git is missing or
11
- * the directory is not a repository.
12
+ * the content directory is not a repository.
13
+ *
14
+ * Callers speak content-relative paths — the same paths the content index
15
+ * reports. The reader resolves the owning work tree once and translates every
16
+ * path itself, so the process working directory never influences a result.
12
17
  */
13
18
  export declare class GitChangelogReader {
14
19
  #private;
15
20
  constructor(options?: GitChangelogReaderOptions);
16
- /** Commit history for one file, newest first, following renames. */
21
+ /**
22
+ * Resolves the Git work tree that owns the content directory, or the reason
23
+ * it cannot be resolved. Memoized: every Git query reuses the same answer.
24
+ */
25
+ resolveRepository(): Promise<RepositoryPathsResolution>;
26
+ /** True when the content directory lives inside a Git work tree. */
27
+ isAvailable(): Promise<boolean>;
28
+ /**
29
+ * Commit history for one file, newest first, following renames.
30
+ *
31
+ * @param filePath Content-relative path, e.g. `notes/hello.md`.
32
+ */
17
33
  getFileHistory(filePath: string, window?: GitLogWindow): Promise<ChangelogCommit[]>;
18
34
  /**
19
- * Recent commits that touch the working directory, newest first, each with
20
- * the paths it changed.
35
+ * Recent commits that touch the content directory, newest first, each with
36
+ * the content-relative paths it changed.
21
37
  */
22
38
  getRecentCommits(window?: GitLogWindow): Promise<ChangelogCommit[]>;
23
- /** True when `cwd` is inside a Git working tree. */
24
- isAvailable(): Promise<boolean>;
25
39
  }
@@ -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>;
@@ -3,8 +3,10 @@ export type ChangelogDateFormat = "iso" | "long" | "short";
3
3
  /** Options accepted by `changelog()`. */
4
4
  export type ChangelogOptions = {
5
5
  /**
6
- * Working directory for Git commands. Defaults to `process.cwd()`. Point it
7
- * at the content directory when the site is built from elsewhere.
6
+ * Content root: the directory holding the notes, used to locate the owning
7
+ * Git work tree. Defaults to `config.content.directory` when the plugin runs
8
+ * inside a build, otherwise `process.cwd()`. A relative value is resolved
9
+ * against `process.cwd()`.
8
10
  */
9
11
  cwd?: string;
10
12
  /**
@@ -66,7 +68,7 @@ export type ChangelogCommit = {
66
68
  date: string;
67
69
  subject: string;
68
70
  author: string;
69
- /** Paths changed by the commit, relative to the Git working directory. */
71
+ /** Content-relative path of the file changed by the commit. */
70
72
  files: string[];
71
73
  };
72
74
  /** A commit prepared for rendering. */
@@ -103,6 +105,6 @@ export type SiteChangelog = {
103
105
  };
104
106
  /** Options accepted by `GitChangelogReader`. */
105
107
  export type GitChangelogReaderOptions = {
106
- /** Working directory for Git commands. Defaults to `process.cwd()`. */
108
+ /** Content root used to locate the Git work tree. Defaults to `process.cwd()`. */
107
109
  cwd?: string;
108
110
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-changelog",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,10 +15,11 @@
15
15
  "./style.css": "./style.css"
16
16
  },
17
17
  "dependencies": {
18
- "@riebeckite/core": "0.0.14"
18
+ "@riebeckite/core": "0.0.16"
19
19
  },
20
20
  "devDependencies": {
21
- "@types/node": "^24.5.2"
21
+ "@types/node": "^24.5.2",
22
+ "tsx": "^4.22.4"
22
23
  },
23
24
  "publishConfig": {
24
25
  "access": "public"
@@ -53,6 +54,7 @@
53
54
  "./style.css"
54
55
  ],
55
56
  "scripts": {
56
- "build": "node ../../../scripts/build_package.mjs"
57
+ "build": "node ../../../scripts/build_package.mjs",
58
+ "test": "node --import tsx --test \"test/*.test.ts\""
57
59
  }
58
60
  }