@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 +9 -6
- package/README_ja.md +4 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +170 -49
- package/dist/src/changelog.d.ts +10 -0
- package/dist/src/git/history_reader.d.ts +21 -7
- 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 -4
- package/package.json +6 -4
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()` |
|
|
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`
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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`
|
|
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
|
|
75
|
-
if (
|
|
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
|
-
#
|
|
208
|
+
#contentRoot;
|
|
101
209
|
#fileCache = /* @__PURE__ */ new Map();
|
|
102
210
|
#recentCache;
|
|
103
|
-
#
|
|
211
|
+
#repository;
|
|
104
212
|
constructor(options = {}) {
|
|
105
|
-
this.#
|
|
213
|
+
this.#contentRoot = path2.resolve(options.cwd ?? process.cwd());
|
|
106
214
|
}
|
|
107
|
-
/**
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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({
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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
|
|
351
|
-
|
|
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
|
};
|
package/dist/src/changelog.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
|
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>;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ export type ChangelogDateFormat = "iso" | "long" | "short";
|
|
|
3
3
|
/** Options accepted by `changelog()`. */
|
|
4
4
|
export type ChangelogOptions = {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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
|
}
|