move getGitLastUpdate fn to gitUtils

This commit is contained in:
sebastien 2025-11-06 18:12:23 +01:00
parent 8af8aab7cb
commit 4fa2e093ac
7 changed files with 168 additions and 170 deletions

View File

@ -1 +0,0 @@
# Hoo hoo, if this path tricks you...

View File

@ -1,7 +0,0 @@
---
id: hello
title: Hello, World !
slug: /
---
Hello

View File

@ -5,13 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {jest} from '@jest/globals';
import fs from 'fs-extra';
import path from 'path';
import {createTempRepo} from '@testing-utils/git';
import execa from 'execa';
import {getGitLastUpdate, readLastUpdateData} from '../lastUpdateUtils';
import {readLastUpdateData} from '../lastUpdateUtils';
import {
VcsHardcoded,
VCS_HARDCODED_UNTRACKED_FILE_PATH,
@ -20,111 +14,6 @@ import {
import type {FrontMatterLastUpdate} from '../lastUpdateUtils';
describe('getGitLastUpdate', () => {
const {repoDir} = createTempRepo();
const existingFilePath = path.join(
__dirname,
'__fixtures__/simple-site/hello.md',
);
it('existing test file in repository with Git timestamp', async () => {
const lastUpdateData = await getGitLastUpdate(existingFilePath);
expect(lastUpdateData).not.toBeNull();
const {lastUpdatedAt, lastUpdatedBy} = lastUpdateData!;
expect(lastUpdatedBy).not.toBeNull();
expect(typeof lastUpdatedBy).toBe('string');
expect(lastUpdatedAt).not.toBeNull();
expect(typeof lastUpdatedAt).toBe('number');
});
it('existing test file with spaces in path', async () => {
const filePathWithSpace = path.join(
__dirname,
'__fixtures__/simple-site/doc with space.md',
);
const lastUpdateData = await getGitLastUpdate(filePathWithSpace);
expect(lastUpdateData).not.toBeNull();
const {lastUpdatedBy, lastUpdatedAt} = lastUpdateData!;
expect(lastUpdatedBy).not.toBeNull();
expect(typeof lastUpdatedBy).toBe('string');
expect(lastUpdatedAt).not.toBeNull();
expect(typeof lastUpdatedAt).toBe('number');
});
it('non-existing file', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const nonExistingFileName = '.nonExisting';
const nonExistingFilePath = path.join(
__dirname,
'__fixtures__',
nonExistingFileName,
);
await expect(getGitLastUpdate(nonExistingFilePath)).rejects.toThrow(
/An error occurred when trying to get the last update date/,
);
expect(consoleMock).toHaveBeenCalledTimes(0);
consoleMock.mockRestore();
});
it('git does not exist', async () => {
const mock = jest.spyOn(execa, 'sync').mockImplementationOnce(() => {
throw new Error('Git does not exist');
});
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const lastUpdateData = await getGitLastUpdate(existingFilePath);
expect(lastUpdateData).toBeNull();
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(
/.*\[WARNING\].* Sorry, the last update options require Git\..*/,
),
);
consoleMock.mockRestore();
mock.mockRestore();
});
it('temporary created file that is not tracked by git', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const tempFilePath = path.join(repoDir, 'file.md');
await fs.writeFile(tempFilePath, 'Lorem ipsum :)');
await expect(getGitLastUpdate(tempFilePath)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/not tracked by git./),
);
await fs.unlink(tempFilePath);
});
it('multiple files not tracked by git', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const tempFilePath1 = path.join(repoDir, 'file1.md');
const tempFilePath2 = path.join(repoDir, 'file2.md');
await fs.writeFile(tempFilePath1, 'Lorem ipsum :)');
await fs.writeFile(tempFilePath2, 'Lorem ipsum :)');
await expect(getGitLastUpdate(tempFilePath1)).resolves.toBeNull();
await expect(getGitLastUpdate(tempFilePath2)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/not tracked by git./),
);
await fs.unlink(tempFilePath1);
await fs.unlink(tempFilePath2);
});
});
describe('readLastUpdateData', () => {
const testDate = '2021-01-01';
const testTimestamp = new Date(testDate).getTime();

View File

@ -123,7 +123,6 @@ export {askPreferredLanguage} from './cliUtils';
export {flattenRoutes} from './routeUtils';
export {
getGitLastUpdate,
readLastUpdateData,
type LastUpdateData,
type FrontMatterLastUpdate,

View File

@ -6,12 +6,6 @@
*/
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {
FileNotTrackedError,
GitNotFoundError,
getFileCommitDate,
} from './vcs/gitUtils';
import {getDefaultVcsConfig} from './vcs/vcs';
import type {PluginOptions, VcsConfig} from '@docusaurus/types';
@ -31,48 +25,6 @@ export type LastUpdateData = {
lastUpdatedBy: string | undefined | null;
};
let showedGitRequirementError = false;
let showedFileNotTrackedError = false;
export async function getGitLastUpdate(
filePath: string,
): Promise<LastUpdateData | null> {
if (!filePath) {
return null;
}
// Wrap in try/catch in case the shell commands fail
// (e.g. project doesn't use Git, etc).
try {
const result = await getFileCommitDate(filePath, {
age: 'newest',
includeAuthor: true,
});
return {lastUpdatedAt: result.timestamp, lastUpdatedBy: result.author};
} catch (err) {
if (err instanceof GitNotFoundError) {
if (!showedGitRequirementError) {
logger.warn('Sorry, the last update options require Git.');
showedGitRequirementError = true;
}
} else if (err instanceof FileNotTrackedError) {
if (!showedFileNotTrackedError) {
logger.warn(
'Cannot infer the update date for some files, as they are not tracked by git.',
);
showedFileNotTrackedError = true;
}
} else {
throw new Error(
`An error occurred when trying to get the last update date`,
{cause: err},
);
}
return null;
}
}
type LastUpdateOptions = Pick<
PluginOptions,
'showLastUpdateAuthor' | 'showLastUpdateTime'

View File

@ -5,10 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {jest} from '@jest/globals';
import fs from 'fs-extra';
import path from 'path';
import {createTempRepo} from '@testing-utils/git';
import {FileNotTrackedError, getFileCommitDate} from '../gitUtils';
import execa from 'execa';
import {FileNotTrackedError, getFileCommitDate,getGitLastUpdate} from '../gitUtils';
/* eslint-disable no-restricted-properties */
function initializeTempRepo() {
@ -143,3 +145,109 @@ describe('getFileCommitDate', () => {
);
});
});
describe('getGitLastUpdate', () => {
const {repoDir} = createTempRepo();
const existingFilePath = path.join(
__dirname,
'__fixtures__/simple-site/docs/doc1.md',
);
it('existing test file in repository with Git timestamp', async () => {
const lastUpdateData = await getGitLastUpdate(existingFilePath);
expect(lastUpdateData).not.toBeNull();
const {timestamp, author} = lastUpdateData!;
expect(author).not.toBeNull();
expect(typeof author).toBe('string');
expect(timestamp).not.toBeNull();
expect(typeof timestamp).toBe('number');
});
it('existing test file with spaces in path', async () => {
const filePathWithSpace = path.join(
__dirname,
'__fixtures__/simple-site/docs/doc with space.md',
);
const lastUpdateData = await getGitLastUpdate(filePathWithSpace);
expect(lastUpdateData).not.toBeNull();
const {timestamp, author} = lastUpdateData!;
expect(author).not.toBeNull();
expect(typeof author).toBe('string');
expect(timestamp).not.toBeNull();
expect(typeof timestamp).toBe('number');
});
it('non-existing file', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const nonExistingFileName = '.nonExisting';
const nonExistingFilePath = path.join(
__dirname,
'__fixtures__',
nonExistingFileName,
);
await expect(getGitLastUpdate(nonExistingFilePath)).rejects.toThrow(
/An error occurred when trying to get the last update date/,
);
expect(consoleMock).toHaveBeenCalledTimes(0);
consoleMock.mockRestore();
});
it('git does not exist', async () => {
const mock = jest.spyOn(execa, 'sync').mockImplementationOnce(() => {
throw new Error('Git does not exist');
});
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const lastUpdateData = await getGitLastUpdate(existingFilePath);
expect(lastUpdateData).toBeNull();
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(
/.*\[WARNING\].* Sorry, the last update options require Git\..*/,
),
);
consoleMock.mockRestore();
mock.mockRestore();
});
it('temporary created file that is not tracked by git', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const tempFilePath = path.join(repoDir, 'file.md');
await fs.writeFile(tempFilePath, 'Lorem ipsum :)');
await expect(getGitLastUpdate(tempFilePath)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/not tracked by git./),
);
await fs.unlink(tempFilePath);
});
it('multiple files not tracked by git', async () => {
const consoleMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const tempFilePath1 = path.join(repoDir, 'file1.md');
const tempFilePath2 = path.join(repoDir, 'file2.md');
await fs.writeFile(tempFilePath1, 'Lorem ipsum :)');
await fs.writeFile(tempFilePath2, 'Lorem ipsum :)');
await expect(getGitLastUpdate(tempFilePath1)).resolves.toBeNull();
await expect(getGitLastUpdate(tempFilePath2)).resolves.toBeNull();
expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/not tracked by git./),
);
await fs.unlink(tempFilePath1);
await fs.unlink(tempFilePath2);
});
});

View File

@ -11,6 +11,7 @@ import os from 'os';
import _ from 'lodash';
import execa from 'execa';
import PQueue from 'p-queue';
import logger from '@docusaurus/logger';
// Quite high/conservative concurrency value (it was previously "Infinity")
// See https://github.com/facebook/docusaurus/pull/10915
@ -204,3 +205,60 @@ export async function getFileCommitDate(
}
return {date, timestamp};
}
type GitLastUpdateResult = {
/**
* A timestamp in **milliseconds**
* `undefined`: not read
* `null`: no value to read (usual for untracked files)
*/
timestamp: number | undefined | null;
/**
* The Git author's name
* `undefined`: not read
* `null`: no value to read (usual for untracked files)
*/
author: string | undefined | null;
};
let showedGitRequirementError = false;
let showedFileNotTrackedError = false;
export async function getGitLastUpdate(
filePath: string,
): Promise<GitLastUpdateResult | null> {
if (!filePath) {
return null;
}
// Wrap in try/catch in case the shell commands fail
// (e.g. project doesn't use Git, etc).
try {
const result = await getFileCommitDate(filePath, {
age: 'newest',
includeAuthor: true,
});
return {timestamp: result.timestamp, author: result.author};
} catch (err) {
// TODO legacy perf issue: do not use exceptions for control flow!
if (err instanceof GitNotFoundError) {
if (!showedGitRequirementError) {
logger.warn('Sorry, the last update options require Git.');
showedGitRequirementError = true;
}
} else if (err instanceof FileNotTrackedError) {
if (!showedFileNotTrackedError) {
logger.warn(
'Cannot infer the update date for some files, as they are not tracked by git.',
);
showedFileNotTrackedError = true;
}
} else {
throw new Error(
`An error occurred when trying to get the last update date`,
{cause: err},
);
}
return null;
}
}