@docusaurus/utils 3.9.2 → 3.10.1
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/lib/index.d.ts +5 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +13 -11
- package/lib/index.js.map +1 -1
- package/lib/lastUpdateUtils.d.ts +2 -6
- package/lib/lastUpdateUtils.d.ts.map +1 -1
- package/lib/lastUpdateUtils.js +13 -60
- package/lib/lastUpdateUtils.js.map +1 -1
- package/lib/markdownHeadingIdUtils.d.ts +49 -0
- package/lib/markdownHeadingIdUtils.d.ts.map +1 -0
- package/lib/markdownHeadingIdUtils.js +148 -0
- package/lib/markdownHeadingIdUtils.js.map +1 -0
- package/lib/markdownUtils.d.ts +0 -31
- package/lib/markdownUtils.d.ts.map +1 -1
- package/lib/markdownUtils.js +0 -89
- package/lib/markdownUtils.js.map +1 -1
- package/lib/moduleUtils.d.ts.map +1 -1
- package/lib/moduleUtils.js +4 -4
- package/lib/moduleUtils.js.map +1 -1
- package/lib/{gitUtils.d.ts → vcs/gitUtils.d.ts} +18 -0
- package/lib/vcs/gitUtils.d.ts.map +1 -0
- package/lib/vcs/gitUtils.js +343 -0
- package/lib/vcs/gitUtils.js.map +1 -0
- package/lib/vcs/vcs.d.ts +19 -0
- package/lib/vcs/vcs.d.ts.map +1 -0
- package/lib/vcs/vcs.js +46 -0
- package/lib/vcs/vcs.js.map +1 -0
- package/lib/vcs/vcsDefaultV1.d.ts +13 -0
- package/lib/vcs/vcsDefaultV1.d.ts.map +1 -0
- package/lib/vcs/vcsDefaultV1.js +33 -0
- package/lib/vcs/vcsDefaultV1.js.map +1 -0
- package/lib/vcs/vcsDefaultV2.d.ts +13 -0
- package/lib/vcs/vcsDefaultV2.d.ts.map +1 -0
- package/lib/vcs/vcsDefaultV2.js +33 -0
- package/lib/vcs/vcsDefaultV2.js.map +1 -0
- package/lib/vcs/vcsDisabled.d.ts +12 -0
- package/lib/vcs/vcsDisabled.d.ts.map +1 -0
- package/lib/vcs/vcsDisabled.js +24 -0
- package/lib/vcs/vcsDisabled.js.map +1 -0
- package/lib/vcs/vcsGitAdHoc.d.ts +16 -0
- package/lib/vcs/vcsGitAdHoc.d.ts.map +1 -0
- package/lib/vcs/vcsGitAdHoc.js +29 -0
- package/lib/vcs/vcsGitAdHoc.js.map +1 -0
- package/lib/vcs/vcsGitEager.d.ts +10 -0
- package/lib/vcs/vcsGitEager.d.ts.map +1 -0
- package/lib/vcs/vcsGitEager.js +89 -0
- package/lib/vcs/vcsGitEager.js.map +1 -0
- package/lib/vcs/vcsHardcoded.d.ts +17 -0
- package/lib/vcs/vcsHardcoded.d.ts.map +1 -0
- package/lib/vcs/vcsHardcoded.js +41 -0
- package/lib/vcs/vcsHardcoded.js.map +1 -0
- package/package.json +7 -7
- package/src/index.ts +11 -8
- package/src/lastUpdateUtils.ts +18 -76
- package/src/markdownHeadingIdUtils.ts +209 -0
- package/src/markdownUtils.ts +0 -119
- package/src/moduleUtils.ts +6 -8
- package/src/vcs/gitUtils.ts +541 -0
- package/src/vcs/vcs.ts +54 -0
- package/src/vcs/vcsDefaultV1.ts +33 -0
- package/src/vcs/vcsDefaultV2.ts +33 -0
- package/src/vcs/vcsDisabled.ts +25 -0
- package/src/vcs/vcsGitAdHoc.ts +30 -0
- package/src/vcs/vcsGitEager.ts +135 -0
- package/src/vcs/vcsHardcoded.ts +45 -0
- package/lib/cliUtils.d.ts +0 -14
- package/lib/cliUtils.d.ts.map +0 -1
- package/lib/cliUtils.js +0 -49
- package/lib/cliUtils.js.map +0 -1
- package/lib/gitUtils.d.ts.map +0 -1
- package/lib/gitUtils.js +0 -103
- package/lib/gitUtils.js.map +0 -1
- package/src/cliUtils.ts +0 -65
- package/src/gitUtils.ts +0 -200
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import _ from 'lodash';
|
|
12
|
+
import execa from 'execa';
|
|
13
|
+
import PQueue from 'p-queue';
|
|
14
|
+
import logger from '@docusaurus/logger';
|
|
15
|
+
|
|
16
|
+
// Quite high/conservative concurrency value (it was previously "Infinity")
|
|
17
|
+
// See https://github.com/facebook/docusaurus/pull/10915
|
|
18
|
+
const DefaultGitCommandConcurrency =
|
|
19
|
+
// TODO Docusaurus v4: bump node, availableParallelism() now always exists
|
|
20
|
+
(typeof os.availableParallelism === 'function'
|
|
21
|
+
? os.availableParallelism()
|
|
22
|
+
: os.cpus().length) * 4;
|
|
23
|
+
|
|
24
|
+
const GitCommandConcurrencyEnv = process.env.DOCUSAURUS_GIT_COMMAND_CONCURRENCY
|
|
25
|
+
? parseInt(process.env.DOCUSAURUS_GIT_COMMAND_CONCURRENCY, 10)
|
|
26
|
+
: undefined;
|
|
27
|
+
|
|
28
|
+
const GitCommandConcurrency =
|
|
29
|
+
GitCommandConcurrencyEnv && GitCommandConcurrencyEnv > 0
|
|
30
|
+
? GitCommandConcurrencyEnv
|
|
31
|
+
: DefaultGitCommandConcurrency;
|
|
32
|
+
|
|
33
|
+
// We use a queue to avoid running too many concurrent Git commands at once
|
|
34
|
+
// See https://github.com/facebook/docusaurus/issues/10348
|
|
35
|
+
const GitCommandQueue = new PQueue({
|
|
36
|
+
concurrency: GitCommandConcurrency,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const realHasGitFn = () => {
|
|
40
|
+
try {
|
|
41
|
+
return execa.sync('git', ['--version']).exitCode === 0;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// The hasGit call is synchronous IO so we memoize it
|
|
48
|
+
// The user won't install Git in the middle of a build anyway...
|
|
49
|
+
const hasGit =
|
|
50
|
+
process.env.NODE_ENV === 'test' ? realHasGitFn : _.memoize(realHasGitFn);
|
|
51
|
+
|
|
52
|
+
// TODO Docusaurus v4: remove this
|
|
53
|
+
// Exceptions are not made for control flow logic
|
|
54
|
+
/** Custom error thrown when git is not found in `PATH`. */
|
|
55
|
+
export class GitNotFoundError extends Error {}
|
|
56
|
+
|
|
57
|
+
// TODO Docusaurus v4: remove this, only kept for retro-compatibility
|
|
58
|
+
// Exceptions are not made for control flow logic
|
|
59
|
+
/** Custom error thrown when the current file is not tracked by git. */
|
|
60
|
+
export class FileNotTrackedError extends Error {}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fetches the git history of a file and returns a relevant commit date.
|
|
64
|
+
* It gets the commit date instead of author date so that amended commits
|
|
65
|
+
* can have their dates updated.
|
|
66
|
+
*
|
|
67
|
+
* @throws {@link GitNotFoundError} If git is not found in `PATH`.
|
|
68
|
+
* @throws {@link FileNotTrackedError} If the current file is not tracked by git.
|
|
69
|
+
* @throws Also throws when `git log` exited with non-zero, or when it outputs
|
|
70
|
+
* unexpected text.
|
|
71
|
+
*/
|
|
72
|
+
export async function getFileCommitDate(
|
|
73
|
+
/** Absolute path to the file. */
|
|
74
|
+
file: string,
|
|
75
|
+
args: {
|
|
76
|
+
/**
|
|
77
|
+
* `"oldest"` is the commit that added the file, following renames;
|
|
78
|
+
* `"newest"` is the last commit that edited the file.
|
|
79
|
+
*/
|
|
80
|
+
age?: 'oldest' | 'newest';
|
|
81
|
+
/** Use `includeAuthor: true` to get the author information as well. */
|
|
82
|
+
includeAuthor?: false;
|
|
83
|
+
},
|
|
84
|
+
): Promise<{
|
|
85
|
+
/** Relevant commit date. */
|
|
86
|
+
date: Date; // TODO duplicate data, not really useful?
|
|
87
|
+
/** Timestamp returned from git, converted to **milliseconds**. */
|
|
88
|
+
timestamp: number;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Fetches the git history of a file and returns a relevant commit date.
|
|
92
|
+
* It gets the commit date instead of author date so that amended commits
|
|
93
|
+
* can have their dates updated.
|
|
94
|
+
*
|
|
95
|
+
* @throws {@link GitNotFoundError} If git is not found in `PATH`.
|
|
96
|
+
* @throws {@link FileNotTrackedError} If the current file is not tracked by git.
|
|
97
|
+
* @throws Also throws when `git log` exited with non-zero, or when it outputs
|
|
98
|
+
* unexpected text.
|
|
99
|
+
*/
|
|
100
|
+
export async function getFileCommitDate(
|
|
101
|
+
/** Absolute path to the file. */
|
|
102
|
+
file: string,
|
|
103
|
+
args: {
|
|
104
|
+
/**
|
|
105
|
+
* `"oldest"` is the commit that added the file, following renames;
|
|
106
|
+
* `"newest"` is the last commit that edited the file.
|
|
107
|
+
*/
|
|
108
|
+
age?: 'oldest' | 'newest';
|
|
109
|
+
includeAuthor: true;
|
|
110
|
+
},
|
|
111
|
+
): Promise<{
|
|
112
|
+
/** Relevant commit date. */
|
|
113
|
+
date: Date;
|
|
114
|
+
/** Timestamp returned from git, converted to **milliseconds**. */
|
|
115
|
+
timestamp: number;
|
|
116
|
+
/** The author's name, as returned from git. */
|
|
117
|
+
author: string;
|
|
118
|
+
}>;
|
|
119
|
+
|
|
120
|
+
export async function getFileCommitDate(
|
|
121
|
+
file: string,
|
|
122
|
+
{
|
|
123
|
+
age = 'oldest',
|
|
124
|
+
includeAuthor = false,
|
|
125
|
+
}: {
|
|
126
|
+
age?: 'oldest' | 'newest';
|
|
127
|
+
includeAuthor?: boolean;
|
|
128
|
+
},
|
|
129
|
+
): Promise<{
|
|
130
|
+
date: Date;
|
|
131
|
+
timestamp: number;
|
|
132
|
+
author?: string;
|
|
133
|
+
}> {
|
|
134
|
+
if (!hasGit()) {
|
|
135
|
+
throw new GitNotFoundError(
|
|
136
|
+
`Failed to retrieve git history for "${file}" because git is not installed.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!(await fs.pathExists(file))) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Failed to retrieve git history for "${file}" because the file does not exist.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// We add a "RESULT:" prefix to make parsing easier
|
|
147
|
+
// See why: https://github.com/facebook/docusaurus/pull/10022
|
|
148
|
+
const resultFormat = includeAuthor ? 'RESULT:%ct,%an' : 'RESULT:%ct';
|
|
149
|
+
|
|
150
|
+
const args = [
|
|
151
|
+
`--format=${resultFormat}`,
|
|
152
|
+
'--max-count=1',
|
|
153
|
+
age === 'oldest' ? '--follow --diff-filter=A' : undefined,
|
|
154
|
+
]
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.join(' ');
|
|
157
|
+
|
|
158
|
+
// Do not include GPG signature in the log output
|
|
159
|
+
// See https://github.com/facebook/docusaurus/pull/10022
|
|
160
|
+
const command = `git -c log.showSignature=false log ${args} -- "${path.basename(
|
|
161
|
+
file,
|
|
162
|
+
)}"`;
|
|
163
|
+
|
|
164
|
+
const result = (await GitCommandQueue.add(() => {
|
|
165
|
+
return execa(command, {
|
|
166
|
+
cwd: path.dirname(file),
|
|
167
|
+
shell: true,
|
|
168
|
+
});
|
|
169
|
+
}))!;
|
|
170
|
+
|
|
171
|
+
if (result.exitCode !== 0) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Failed to retrieve the git history for file "${file}" with exit code ${result.exitCode}: ${result.stderr}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// We only parse the output line starting with our "RESULT:" prefix
|
|
178
|
+
// See why https://github.com/facebook/docusaurus/pull/10022
|
|
179
|
+
const regex = includeAuthor
|
|
180
|
+
? /(?:^|\n)RESULT:(?<timestamp>\d+),(?<author>.+)(?:$|\n)/
|
|
181
|
+
: /(?:^|\n)RESULT:(?<timestamp>\d+)(?:$|\n)/;
|
|
182
|
+
|
|
183
|
+
const output = result.stdout.trim();
|
|
184
|
+
|
|
185
|
+
if (!output) {
|
|
186
|
+
throw new FileNotTrackedError(
|
|
187
|
+
`Failed to retrieve the git history for file "${file}" because the file is not tracked by git.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const match = output.match(regex);
|
|
192
|
+
|
|
193
|
+
if (!match) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Failed to retrieve the git history for file "${file}" with unexpected output: ${output}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const timestampInSeconds = Number(match.groups!.timestamp);
|
|
200
|
+
const timestamp = timestampInSeconds * 1_000;
|
|
201
|
+
const date = new Date(timestamp);
|
|
202
|
+
|
|
203
|
+
if (includeAuthor) {
|
|
204
|
+
return {date, timestamp, author: match.groups!.author!};
|
|
205
|
+
}
|
|
206
|
+
return {date, timestamp};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let showedGitRequirementError = false;
|
|
210
|
+
let showedFileNotTrackedError = false;
|
|
211
|
+
|
|
212
|
+
type GitCommitInfo = {timestamp: number; author: string};
|
|
213
|
+
|
|
214
|
+
async function getGitCommitInfo(
|
|
215
|
+
filePath: string,
|
|
216
|
+
age: 'oldest' | 'newest',
|
|
217
|
+
): Promise<GitCommitInfo | null> {
|
|
218
|
+
if (!filePath) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
// Wrap in try/catch in case the shell commands fail
|
|
222
|
+
// (e.g. project doesn't use Git, etc).
|
|
223
|
+
try {
|
|
224
|
+
const result = await getFileCommitDate(filePath, {
|
|
225
|
+
age,
|
|
226
|
+
includeAuthor: true,
|
|
227
|
+
});
|
|
228
|
+
return {timestamp: result.timestamp, author: result.author};
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// TODO legacy perf issue: do not use exceptions for control flow!
|
|
231
|
+
if (err instanceof GitNotFoundError) {
|
|
232
|
+
if (!showedGitRequirementError) {
|
|
233
|
+
logger.warn('Sorry, the last update options require Git.');
|
|
234
|
+
showedGitRequirementError = true;
|
|
235
|
+
}
|
|
236
|
+
} else if (err instanceof FileNotTrackedError) {
|
|
237
|
+
if (!showedFileNotTrackedError) {
|
|
238
|
+
logger.warn(
|
|
239
|
+
'Cannot infer the update date for some files, as they are not tracked by git.',
|
|
240
|
+
);
|
|
241
|
+
showedFileNotTrackedError = true;
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`An error occurred when trying to get the file ${
|
|
246
|
+
age === 'oldest' ? 'creation' : 'last update'
|
|
247
|
+
} date from Git`,
|
|
248
|
+
{cause: err},
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function getGitLastUpdate(
|
|
256
|
+
filePath: string,
|
|
257
|
+
): Promise<GitCommitInfo | null> {
|
|
258
|
+
return getGitCommitInfo(filePath, 'newest');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function getGitCreation(
|
|
262
|
+
filePath: string,
|
|
263
|
+
): Promise<GitCommitInfo | null> {
|
|
264
|
+
return getGitCommitInfo(filePath, 'oldest');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function isGitInsideWorktree(cwd: string): Promise<boolean> {
|
|
268
|
+
try {
|
|
269
|
+
const result = await execa('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
270
|
+
cwd,
|
|
271
|
+
reject: false,
|
|
272
|
+
});
|
|
273
|
+
return result.exitCode === 0;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Couldn't check if this directory is within a Git worktree: ${cwd}`,
|
|
277
|
+
{cause: error},
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function getGitRepoRoot(cwd: string): Promise<string> {
|
|
283
|
+
const createErrorMessageBase = () => {
|
|
284
|
+
return `Couldn't find the git repository root directory
|
|
285
|
+
Failure while running ${logger.code(
|
|
286
|
+
'git rev-parse --show-toplevel',
|
|
287
|
+
)} from cwd=${logger.path(cwd)}`;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const result = await execa('git', ['rev-parse', '--show-toplevel'], {
|
|
291
|
+
cwd,
|
|
292
|
+
}).catch((error) => {
|
|
293
|
+
// We enter this rejection when cwd is not a dir for example
|
|
294
|
+
throw new Error(
|
|
295
|
+
`${createErrorMessageBase()}
|
|
296
|
+
The command executed throws an error: ${error.message}`,
|
|
297
|
+
{cause: error},
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (result.exitCode !== 0) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`${createErrorMessageBase()}
|
|
304
|
+
The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue(
|
|
305
|
+
result.stderr,
|
|
306
|
+
)}`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return fs.realpath.native(result.stdout.trim());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// A Git "superproject" is a Git repository that contains submodules
|
|
314
|
+
// See https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt---show-superproject-working-tree
|
|
315
|
+
// See https://git-scm.com/book/en/v2/Git-Tools-Submodules
|
|
316
|
+
export async function getGitSuperProjectRoot(
|
|
317
|
+
cwd: string,
|
|
318
|
+
): Promise<string | null> {
|
|
319
|
+
const createErrorMessageBase = () => {
|
|
320
|
+
return `Couldn't find the git superproject root directory
|
|
321
|
+
Failure while running ${logger.code(
|
|
322
|
+
'git rev-parse --show-superproject-working-tree',
|
|
323
|
+
)} from cwd=${logger.path(cwd)}`;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const result = await execa(
|
|
327
|
+
'git',
|
|
328
|
+
['rev-parse', '--show-superproject-working-tree'],
|
|
329
|
+
{
|
|
330
|
+
cwd,
|
|
331
|
+
},
|
|
332
|
+
).catch((error) => {
|
|
333
|
+
// We enter this rejection when cwd is not a dir for example
|
|
334
|
+
throw new Error(
|
|
335
|
+
`${createErrorMessageBase()}
|
|
336
|
+
The command executed throws an error: ${error.message}`,
|
|
337
|
+
{cause: error},
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (result.exitCode !== 0) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`${createErrorMessageBase()}
|
|
344
|
+
The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue(
|
|
345
|
+
result.stderr,
|
|
346
|
+
)}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const output = result.stdout.trim();
|
|
351
|
+
// this command only works when inside submodules
|
|
352
|
+
// otherwise it doesn't return anything when we are inside the main repo
|
|
353
|
+
if (output) {
|
|
354
|
+
return fs.realpath.native(output);
|
|
355
|
+
}
|
|
356
|
+
return getGitRepoRoot(cwd);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// See https://git-scm.com/book/en/v2/Git-Tools-Submodules
|
|
360
|
+
export async function getGitSubmodulePaths(cwd: string): Promise<string[]> {
|
|
361
|
+
const createErrorMessageBase = () => {
|
|
362
|
+
return `Couldn't read the list of git submodules
|
|
363
|
+
Failure while running ${logger.code(
|
|
364
|
+
'git submodule status',
|
|
365
|
+
)} from cwd=${logger.path(cwd)}`;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const result = await execa('git', ['submodule', 'status'], {
|
|
369
|
+
cwd,
|
|
370
|
+
}).catch((error) => {
|
|
371
|
+
// We enter this rejection when cwd is not a dir for example
|
|
372
|
+
throw new Error(
|
|
373
|
+
`${createErrorMessageBase()}
|
|
374
|
+
The command executed throws an error: ${error.message}`,
|
|
375
|
+
{cause: error},
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
if (result.exitCode !== 0) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`${createErrorMessageBase()}
|
|
382
|
+
The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue(
|
|
383
|
+
result.stderr,
|
|
384
|
+
)}`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const output = result.stdout.trim();
|
|
389
|
+
|
|
390
|
+
if (!output) {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* The output may contain a space/-/+/U prefix, for example
|
|
395
|
+
1234567e3e35d1f5b submodules/foo (heads/main)
|
|
396
|
+
-9ab1f1d3a2d77b0a4 submodules/bar (heads/dev)
|
|
397
|
+
+f00ba42e1b3ddead submodules/baz (remotes/origin/main)
|
|
398
|
+
Udeadbeefcafe1234 submodules/qux
|
|
399
|
+
*/
|
|
400
|
+
const getSubmodulePath = async (line: string) => {
|
|
401
|
+
const submodulePath = line.substring(1).split(' ')[1];
|
|
402
|
+
if (!submodulePath) {
|
|
403
|
+
throw new Error(`Failed to parse git submodule line: ${line}`);
|
|
404
|
+
}
|
|
405
|
+
return submodulePath;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return Promise.all(output.split('\n').map(getSubmodulePath));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Find the root git repository alongside all its submodules, if any
|
|
412
|
+
export async function getGitAllRepoRoots(cwd: string): Promise<string[]> {
|
|
413
|
+
try {
|
|
414
|
+
const superProjectRoot = await getGitSuperProjectRoot(cwd);
|
|
415
|
+
if (!superProjectRoot) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
let submodulePaths = await getGitSubmodulePaths(superProjectRoot);
|
|
419
|
+
submodulePaths = await Promise.all(
|
|
420
|
+
submodulePaths.map((submodulePath) =>
|
|
421
|
+
fs.realpath.native(path.resolve(superProjectRoot, submodulePath)),
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
return [superProjectRoot, ...submodulePaths];
|
|
425
|
+
} catch (error) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Could not get all the git repository root paths (superproject + submodules) from cwd=${cwd}`,
|
|
428
|
+
{cause: error},
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Useful information about a file tracked in a Git repository
|
|
434
|
+
export type GitFileInfo = {
|
|
435
|
+
creation: GitCommitInfo;
|
|
436
|
+
lastUpdate: GitCommitInfo;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// A map of all the files tracked in a Git repository
|
|
440
|
+
export type GitFileInfoMap = Map<string, GitFileInfo>;
|
|
441
|
+
|
|
442
|
+
// Logic inspired from Astro Starlight:
|
|
443
|
+
// See https://bsky.app/profile/bluwy.me/post/3lyihod6qos2a
|
|
444
|
+
// See https://github.com/withastro/starlight/blob/c417f1efd463be63b7230617d72b120caed098cd/packages/starlight/utils/git.ts#L58
|
|
445
|
+
export async function getGitRepositoryFilesInfo(
|
|
446
|
+
cwd: string,
|
|
447
|
+
): Promise<GitFileInfoMap> {
|
|
448
|
+
// git --no-pager -c log.showSignature=false log --format=t:%ct,a:%an --name-status
|
|
449
|
+
const result = await execa(
|
|
450
|
+
'git',
|
|
451
|
+
[
|
|
452
|
+
'--no-pager',
|
|
453
|
+
// Do not include GPG signature in the log output
|
|
454
|
+
// See https://github.com/facebook/docusaurus/pull/10022
|
|
455
|
+
'-c',
|
|
456
|
+
'log.showSignature=false',
|
|
457
|
+
// The git command we want to run
|
|
458
|
+
'log',
|
|
459
|
+
// Format each history entry as t:<seconds since epoch>
|
|
460
|
+
'--format=t:%ct,a:%an',
|
|
461
|
+
// In each entry include the name and status for each modified file
|
|
462
|
+
'--name-status',
|
|
463
|
+
|
|
464
|
+
// For creation info, should we use --follow --find-renames=100% ???
|
|
465
|
+
],
|
|
466
|
+
{
|
|
467
|
+
cwd,
|
|
468
|
+
encoding: 'utf-8',
|
|
469
|
+
// TODO use streaming to avoid a large buffer
|
|
470
|
+
// See https://github.com/withastro/starlight/issues/3154
|
|
471
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
if (result.exitCode !== 0) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Docusaurus failed to run the 'git log' to retrieve tracked files last update date/author.
|
|
478
|
+
The command exited with code ${result.exitCode}: ${result.stderr}`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const logLines = result.stdout.split('\n');
|
|
483
|
+
|
|
484
|
+
const now = Date.now();
|
|
485
|
+
|
|
486
|
+
// TODO not fail-fast
|
|
487
|
+
let runningDate = now;
|
|
488
|
+
let runningAuthor = 'N/A';
|
|
489
|
+
const runningMap: GitFileInfoMap = new Map();
|
|
490
|
+
|
|
491
|
+
for (const logLine of logLines) {
|
|
492
|
+
if (logLine.startsWith('t:')) {
|
|
493
|
+
// t:<timestamp>,a:<author name>
|
|
494
|
+
const [timestampStr, authorStr] = logLine.split(',') as [string, string];
|
|
495
|
+
const timestamp = Number.parseInt(timestampStr.slice(2), 10) * 1000;
|
|
496
|
+
const author = authorStr.slice(2);
|
|
497
|
+
|
|
498
|
+
runningDate = timestamp;
|
|
499
|
+
runningAuthor = author;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// TODO the code below doesn't handle delete/move/rename operations properly
|
|
503
|
+
// it returns files that no longer exist in the repo (deleted/moved)
|
|
504
|
+
|
|
505
|
+
// - Added files take the format `A\t<file>`
|
|
506
|
+
// - Modified files take the format `M\t<file>`
|
|
507
|
+
// - Deleted files take the format `D\t<file>`
|
|
508
|
+
// - Renamed files take the format `R<count>\t<old>\t<new>`
|
|
509
|
+
// - Copied files take the format `C<count>\t<old>\t<new>`
|
|
510
|
+
// The name of the file as of the commit being processed is always
|
|
511
|
+
// the last part of the log line.
|
|
512
|
+
const tabSplit = logLine.lastIndexOf('\t');
|
|
513
|
+
if (tabSplit === -1) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const relativeFile = logLine.slice(tabSplit + 1);
|
|
517
|
+
|
|
518
|
+
const currentFileInfo = runningMap.get(relativeFile);
|
|
519
|
+
|
|
520
|
+
const currentCreationTime = currentFileInfo?.creation.timestamp || now;
|
|
521
|
+
const newCreationTime = Math.min(currentCreationTime, runningDate);
|
|
522
|
+
const newCreation: GitCommitInfo =
|
|
523
|
+
!currentFileInfo || newCreationTime !== currentCreationTime
|
|
524
|
+
? {timestamp: newCreationTime, author: runningAuthor}
|
|
525
|
+
: currentFileInfo.creation;
|
|
526
|
+
|
|
527
|
+
const currentLastUpdateTime = currentFileInfo?.lastUpdate.timestamp || 0;
|
|
528
|
+
const newLastUpdateTime = Math.max(currentLastUpdateTime, runningDate);
|
|
529
|
+
const newLastUpdate: GitCommitInfo =
|
|
530
|
+
!currentFileInfo || newLastUpdateTime !== currentLastUpdateTime
|
|
531
|
+
? {timestamp: newLastUpdateTime, author: runningAuthor}
|
|
532
|
+
: currentFileInfo.lastUpdate;
|
|
533
|
+
|
|
534
|
+
runningMap.set(relativeFile, {
|
|
535
|
+
creation: newCreation,
|
|
536
|
+
lastUpdate: newLastUpdate,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return runningMap;
|
|
541
|
+
}
|
package/src/vcs/vcs.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
VCS_HARDCODED_CREATION_INFO,
|
|
10
|
+
VCS_HARDCODED_LAST_UPDATE_INFO,
|
|
11
|
+
VCS_HARDCODED_UNTRACKED_FILE_PATH,
|
|
12
|
+
VcsHardcoded,
|
|
13
|
+
} from './vcsHardcoded';
|
|
14
|
+
import {VcsGitAdHoc} from './vcsGitAdHoc';
|
|
15
|
+
import {VscGitEager} from './vcsGitEager';
|
|
16
|
+
import {VcsDisabled} from './vcsDisabled';
|
|
17
|
+
import {VcsDefaultV1} from './vcsDefaultV1';
|
|
18
|
+
import {VcsDefaultV2} from './vcsDefaultV2';
|
|
19
|
+
import type {VcsConfig, VcsPreset} from '@docusaurus/types';
|
|
20
|
+
|
|
21
|
+
const VcsPresets: Record<VcsPreset, VcsConfig> = {
|
|
22
|
+
'git-ad-hoc': VcsGitAdHoc,
|
|
23
|
+
'git-eager': VscGitEager,
|
|
24
|
+
hardcoded: VcsHardcoded,
|
|
25
|
+
disabled: VcsDisabled,
|
|
26
|
+
|
|
27
|
+
'default-v1': VcsDefaultV1,
|
|
28
|
+
'default-v2': VcsDefaultV2,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const VcsPresetNames = Object.keys(VcsPresets) as VcsPreset[];
|
|
32
|
+
|
|
33
|
+
export function findVcsPreset(presetName: string): VcsConfig | undefined {
|
|
34
|
+
return VcsPresets[presetName as VcsPreset];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getVcsPreset(presetName: VcsPreset): VcsConfig {
|
|
38
|
+
const vcs = findVcsPreset(presetName);
|
|
39
|
+
if (vcs) {
|
|
40
|
+
return vcs;
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Unknown Docusaurus VCS preset name: ${process.env.DOCUSAURUS_VCS}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Convenient export for writing unit tests depending on VCS
|
|
49
|
+
export const TEST_VCS = {
|
|
50
|
+
CREATION_INFO: VCS_HARDCODED_CREATION_INFO,
|
|
51
|
+
LAST_UPDATE_INFO: VCS_HARDCODED_LAST_UPDATE_INFO,
|
|
52
|
+
UNTRACKED_FILE_PATH: VCS_HARDCODED_UNTRACKED_FILE_PATH,
|
|
53
|
+
...VcsHardcoded,
|
|
54
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {VcsHardcoded} from './vcsHardcoded';
|
|
9
|
+
import {VcsGitAdHoc} from './vcsGitAdHoc';
|
|
10
|
+
import type {VcsConfig} from '@docusaurus/types';
|
|
11
|
+
|
|
12
|
+
function getDynamicStrategy(): VcsConfig {
|
|
13
|
+
return process.env.NODE_ENV === 'development' ||
|
|
14
|
+
process.env.NODE_ENV === 'test'
|
|
15
|
+
? VcsHardcoded
|
|
16
|
+
: VcsGitAdHoc;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This VCS implements the historical Git automatic strategy.
|
|
21
|
+
* It is only enabled in production mode, using ad-hoc git log commands.
|
|
22
|
+
*/
|
|
23
|
+
export const VcsDefaultV1: VcsConfig = {
|
|
24
|
+
initialize: (...params) => {
|
|
25
|
+
return getDynamicStrategy().initialize(...params);
|
|
26
|
+
},
|
|
27
|
+
getFileCreationInfo: (...params) => {
|
|
28
|
+
return getDynamicStrategy().getFileCreationInfo(...params);
|
|
29
|
+
},
|
|
30
|
+
getFileLastUpdateInfo: (...params) => {
|
|
31
|
+
return getDynamicStrategy().getFileLastUpdateInfo(...params);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {VcsHardcoded} from './vcsHardcoded';
|
|
9
|
+
import {VscGitEager} from './vcsGitEager';
|
|
10
|
+
import type {VcsConfig} from '@docusaurus/types';
|
|
11
|
+
|
|
12
|
+
function getStrategy(): VcsConfig {
|
|
13
|
+
return process.env.NODE_ENV === 'development' ||
|
|
14
|
+
process.env.NODE_ENV === 'test'
|
|
15
|
+
? VcsHardcoded
|
|
16
|
+
: VscGitEager;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This VCS implements the new eager Git automatic strategy.
|
|
21
|
+
* It is only enabled in production mode, reading the git repository eagerly.
|
|
22
|
+
*/
|
|
23
|
+
export const VcsDefaultV2: VcsConfig = {
|
|
24
|
+
initialize: (...params) => {
|
|
25
|
+
return getStrategy().initialize(...params);
|
|
26
|
+
},
|
|
27
|
+
getFileCreationInfo: (...params) => {
|
|
28
|
+
return getStrategy().getFileCreationInfo(...params);
|
|
29
|
+
},
|
|
30
|
+
getFileLastUpdateInfo: (...params) => {
|
|
31
|
+
return getStrategy().getFileLastUpdateInfo(...params);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {VcsConfig} from '@docusaurus/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This VCS implementation always returns null values
|
|
12
|
+
*/
|
|
13
|
+
export const VcsDisabled: VcsConfig = {
|
|
14
|
+
initialize: () => {
|
|
15
|
+
// Noop
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
getFileCreationInfo: async (_filePath) => {
|
|
19
|
+
return null;
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
getFileLastUpdateInfo: async (_ilePath) => {
|
|
23
|
+
return null;
|
|
24
|
+
},
|
|
25
|
+
};
|