Skip to content

Commit 3bfe0bb

Browse files
slorberclaude
andauthored
refactor(utils): remove GitNotFoundError and FileNotTrackedError (AI-assisted) (#12678)
Exceptions should not be used for control flow: - getFileCommitDate() now returns null when the file is not tracked by git, and throws a plain Error when git is not installed - getGitCommitInfo() checks for git upfront and handles the null result, keeping the same one-time warnings and null fallbacks as before Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d7a12f1 commit 3bfe0bb

2 files changed

Lines changed: 43 additions & 59 deletions

File tree

‎packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import os from 'node:os';
1212
import {execa, type Options, type Result} from 'execa';
1313

1414
import {
15-
FileNotTrackedError,
1615
getFileCommitDate,
1716
getGitLastUpdate,
1817
getGitCreation,
@@ -267,15 +266,15 @@ describe('commit info APIs', () => {
267266
});
268267
});
269268

270-
it('throws custom error when file is not tracked', async () => {
269+
it('returns null when file is not tracked', async () => {
271270
const repoDir = await repoDirPromise;
272271

273-
await expect(() =>
272+
await expect(
274273
getFileCommitDate(path.join(repoDir, 'untracked.txt'), {
275274
age: 'newest',
276275
includeAuthor: true,
277276
}),
278-
).rejects.toThrow(FileNotTrackedError);
277+
).resolves.toBeNull();
279278
});
280279

281280
it('throws when file not found', async () => {

‎packages/docusaurus-utils/src/vcs/gitUtils.ts‎

Lines changed: 40 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,15 @@ const realHasGitFn = () => {
4545
const hasGit =
4646
process.env.NODE_ENV === 'test' ? realHasGitFn : _.memoize(realHasGitFn);
4747

48-
// TODO Docusaurus v4: remove this
49-
// Exceptions are not made for control flow logic
50-
/** Custom error thrown when git is not found in `PATH`. */
51-
class GitNotFoundError extends Error {}
52-
53-
// TODO Docusaurus v4: remove this, only kept for retro-compatibility
54-
// Exceptions are not made for control flow logic
55-
/** Custom error thrown when the current file is not tracked by git. */
56-
export class FileNotTrackedError extends Error {}
57-
5848
/**
5949
* Fetches the git history of a file and returns a relevant commit date.
6050
* It gets the commit date instead of author date so that amended commits
6151
* can have their dates updated.
6252
*
63-
* @throws {@link GitNotFoundError} If git is not found in `PATH`.
64-
* @throws {@link FileNotTrackedError} If the current file is not tracked by git.
65-
* @throws Also throws when `git log` exited with non-zero, or when it outputs
66-
* unexpected text.
53+
* Returns `null` if the file is not tracked by git.
54+
*
55+
* @throws If git is not found in `PATH`, if the file does not exist, if
56+
* `git log` exited with non-zero, or if it outputs unexpected text.
6757
*/
6858
export async function getFileCommitDate(
6959
/** Absolute path to the file. */
@@ -82,16 +72,16 @@ export async function getFileCommitDate(
8272
date: Date; // TODO duplicate data, not really useful?
8373
/** Timestamp returned from git, converted to **milliseconds**. */
8474
timestamp: number;
85-
}>;
75+
} | null>;
8676
/**
8777
* Fetches the git history of a file and returns a relevant commit date.
8878
* It gets the commit date instead of author date so that amended commits
8979
* can have their dates updated.
9080
*
91-
* @throws {@link GitNotFoundError} If git is not found in `PATH`.
92-
* @throws {@link FileNotTrackedError} If the current file is not tracked by git.
93-
* @throws Also throws when `git log` exited with non-zero, or when it outputs
94-
* unexpected text.
81+
* Returns `null` if the file is not tracked by git.
82+
*
83+
* @throws If git is not found in `PATH`, if the file does not exist, if
84+
* `git log` exited with non-zero, or if it outputs unexpected text.
9585
*/
9686
export async function getFileCommitDate(
9787
/** Absolute path to the file. */
@@ -111,7 +101,7 @@ export async function getFileCommitDate(
111101
timestamp: number;
112102
/** The author's name, as returned from git. */
113103
author: string;
114-
}>;
104+
} | null>;
115105

116106
export async function getFileCommitDate(
117107
file: string,
@@ -126,9 +116,9 @@ export async function getFileCommitDate(
126116
date: Date;
127117
timestamp: number;
128118
author?: string;
129-
}> {
119+
} | null> {
130120
if (!hasGit()) {
131-
throw new GitNotFoundError(
121+
throw new Error(
132122
`Failed to retrieve git history for "${file}" because git is not installed.`,
133123
);
134124
}
@@ -172,10 +162,9 @@ export async function getFileCommitDate(
172162

173163
const output = result.stdout.trim();
174164

165+
// No output: the file is not tracked by git
175166
if (!output) {
176-
throw new FileNotTrackedError(
177-
`Failed to retrieve the git history for file "${file}" because the file is not tracked by git.`,
178-
);
167+
return null;
179168
}
180169

181170
const match = output.match(regex);
@@ -196,8 +185,8 @@ export async function getFileCommitDate(
196185
return {date, timestamp};
197186
}
198187

199-
let showedGitRequirementError = false;
200-
let showedFileNotTrackedError = false;
188+
let showedGitRequirementWarning = false;
189+
let showedFileNotTrackedWarning = false;
201190

202191
type GitCommitInfo = {timestamp: number; author: string};
203192

@@ -208,38 +197,34 @@ async function getGitCommitInfo(
208197
if (!filePath) {
209198
return null;
210199
}
211-
// Wrap in try/catch in case the shell commands fail
212-
// (e.g. project doesn't use Git, etc).
213-
try {
214-
const result = await getFileCommitDate(filePath, {
215-
age,
216-
includeAuthor: true,
217-
});
218-
return {timestamp: result.timestamp, author: result.author};
219-
} catch (err) {
220-
// TODO legacy perf issue: do not use exceptions for control flow!
221-
if (err instanceof GitNotFoundError) {
222-
if (!showedGitRequirementError) {
223-
logger.warn('Sorry, the last update options require Git.');
224-
showedGitRequirementError = true;
225-
}
226-
} else if (err instanceof FileNotTrackedError) {
227-
if (!showedFileNotTrackedError) {
228-
logger.warn(
229-
'Cannot infer the update date for some files, as they are not tracked by git.',
230-
);
231-
showedFileNotTrackedError = true;
232-
}
233-
} else {
234-
throw new Error(
235-
`An error occurred when trying to get the file ${
236-
age === 'oldest' ? 'creation' : 'last update'
237-
} date from Git`,
238-
{cause: err},
200+
if (!hasGit()) {
201+
if (!showedGitRequirementWarning) {
202+
logger.warn('Sorry, the last update options require Git.');
203+
showedGitRequirementWarning = true;
204+
}
205+
return null;
206+
}
207+
const result = await getFileCommitDate(filePath, {
208+
age,
209+
includeAuthor: true,
210+
}).catch((err) => {
211+
throw new Error(
212+
`An error occurred when trying to get the file ${
213+
age === 'oldest' ? 'creation' : 'last update'
214+
} date from Git`,
215+
{cause: err},
216+
);
217+
});
218+
if (!result) {
219+
if (!showedFileNotTrackedWarning) {
220+
logger.warn(
221+
'Cannot infer the update date for some files, as they are not tracked by git.',
239222
);
223+
showedFileNotTrackedWarning = true;
240224
}
241225
return null;
242226
}
227+
return {timestamp: result.timestamp, author: result.author};
243228
}
244229

245230
export async function getGitLastUpdate(

0 commit comments

Comments
 (0)