From 4fa2e093acdffde98b5c17204afa3fc1951fe4ad Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 6 Nov 2025 18:12:23 +0100 Subject: [PATCH] move getGitLastUpdate fn to gitUtils --- .../simple-site/doc with space.md | 1 - .../__fixtures__/simple-site/hello.md | 7 -- .../src/__tests__/lastUpdateUtils.test.ts | 113 +----------------- packages/docusaurus-utils/src/index.ts | 1 - .../docusaurus-utils/src/lastUpdateUtils.ts | 48 -------- .../src/vcs/__tests__/gitUtils.test.ts | 110 ++++++++++++++++- packages/docusaurus-utils/src/vcs/gitUtils.ts | 58 +++++++++ 7 files changed, 168 insertions(+), 170 deletions(-) delete mode 100644 packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/doc with space.md delete mode 100644 packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/hello.md diff --git a/packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/doc with space.md b/packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/doc with space.md deleted file mode 100644 index 2b2a616da3..0000000000 --- a/packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/doc with space.md +++ /dev/null @@ -1 +0,0 @@ -# Hoo hoo, if this path tricks you... diff --git a/packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/hello.md b/packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/hello.md deleted file mode 100644 index 38e44ab76c..0000000000 --- a/packages/docusaurus-utils/src/__tests__/__fixtures__/simple-site/hello.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -id: hello -title: Hello, World ! -slug: / ---- - -Hello diff --git a/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts b/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts index 8d61ef60cf..ee3f31947c 100644 --- a/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts @@ -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(); diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 7ab31baa69..0b29742d45 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -123,7 +123,6 @@ export {askPreferredLanguage} from './cliUtils'; export {flattenRoutes} from './routeUtils'; export { - getGitLastUpdate, readLastUpdateData, type LastUpdateData, type FrontMatterLastUpdate, diff --git a/packages/docusaurus-utils/src/lastUpdateUtils.ts b/packages/docusaurus-utils/src/lastUpdateUtils.ts index f57bbc4026..b22bd7948f 100644 --- a/packages/docusaurus-utils/src/lastUpdateUtils.ts +++ b/packages/docusaurus-utils/src/lastUpdateUtils.ts @@ -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 { - 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' diff --git a/packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts b/packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts index c2b166e24b..6d9b5f77b5 100644 --- a/packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts +++ b/packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts @@ -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); + }); +}); diff --git a/packages/docusaurus-utils/src/vcs/gitUtils.ts b/packages/docusaurus-utils/src/vcs/gitUtils.ts index 09738110f8..09a21a74d0 100644 --- a/packages/docusaurus-utils/src/vcs/gitUtils.ts +++ b/packages/docusaurus-utils/src/vcs/gitUtils.ts @@ -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 { + 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; + } +}