diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts index 5e5217a9a3..200bb84432 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts @@ -5,14 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import fs from 'fs-extra'; import path from 'path'; import pluginContentBlog from '../index'; import type {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types'; import {PluginOptionSchema} from '../pluginOptionSchema'; import type {BlogPost} from '../types'; import type {Joi} from '@docusaurus/utils-validation'; -import {posixPath} from '@docusaurus/utils'; +import {posixPath, getFileCommitDate} from '@docusaurus/utils'; import type { PluginOptions, EditUrlFunction, @@ -425,14 +424,15 @@ describe('loadBlog', () => { ); const blogPosts = await getBlogPosts(siteDir); const noDateSource = path.posix.join('@site', PluginPath, 'no date.md'); - const noDateSourceBirthTime = ( - await fs.stat(noDateSource.replace('@site', siteDir)) - ).birthtime; + const noDateSourceFile = path.posix.join(siteDir, PluginPath, 'no date.md'); + // we know the file exist and we know we have git + const result = getFileCommitDate(noDateSourceFile, {age: 'oldest'}); + const noDateSourceTime = result.date; const formattedDate = Intl.DateTimeFormat('en', { day: 'numeric', month: 'long', year: 'numeric', - }).format(noDateSourceBirthTime); + }).format(noDateSourceTime); expect({ ...getByTitle(blogPosts, 'no date').metadata, @@ -445,7 +445,7 @@ describe('loadBlog', () => { title: 'no date', description: `no date`, authors: [], - date: noDateSourceBirthTime, + date: noDateSourceTime, formattedDate, frontMatter: {}, tags: [], diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 254fb12cd9..9c082b3e44 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -27,6 +27,7 @@ import { Globby, normalizeFrontMatterTags, groupTaggedItems, + getFileCommitDate, getContentPathList, } from '@docusaurus/utils'; import type {LoadContext} from '@docusaurus/types'; @@ -242,8 +243,17 @@ async function processBlogSourceFile( } else if (parsedBlogFileName.date) { return parsedBlogFileName.date; } - // Fallback to file create time - return (await fs.stat(blogSourceAbsolute)).birthtime; + + try { + const result = getFileCommitDate(blogSourceAbsolute, { + age: 'oldest', + includeAuthor: false, + }); + return result.date; + } catch (e) { + logger.error(e); + return (await fs.stat(blogSourceAbsolute)).birthtime; + } } const date = await getDate(); diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index 12602c682a..305ae16ff5 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -34,7 +34,6 @@ "js-yaml": "^4.0.0", "lodash": "^4.17.20", "remark-admonitions": "^1.2.1", - "shelljs": "^0.8.4", "tslib": "^2.3.1", "utility-types": "^3.10.0", "webpack": "^5.68.0" @@ -47,6 +46,7 @@ "commander": "^5.1.0", "escape-string-regexp": "^4.0.0", "picomatch": "^2.1.1", + "shelljs": "^0.8.4", "utility-types": "^3.10.0" }, "peerDependencies": { diff --git a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts index 9fdf2ecba7..9114bb3d9e 100644 --- a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts +++ b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts @@ -5,14 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import shell from 'shelljs'; import logger from '@docusaurus/logger'; -import path from 'path'; +import {getFileCommitDate, GitNotFoundError} from '@docusaurus/utils'; type FileLastUpdateData = {timestamp?: number; author?: string}; -const GIT_COMMIT_TIMESTAMP_AUTHOR_REGEX = /^(?\d+),(?.+)$/; - let showedGitRequirementError = false; export async function getFileLastUpdate( @@ -21,53 +18,22 @@ export async function getFileLastUpdate( if (!filePath) { return null; } - function getTimestampAndAuthor(str: string): FileLastUpdateData | null { - if (!str) { - return null; - } - - const temp = str.match(GIT_COMMIT_TIMESTAMP_AUTHOR_REGEX)?.groups; - return temp - ? {timestamp: Number(temp.timestamp), author: temp.author} - : null; - } // Wrap in try/catch in case the shell commands fail // (e.g. project doesn't use Git, etc). try { - if (!shell.which('git')) { - if (!showedGitRequirementError) { - showedGitRequirementError = true; - logger.warn('Sorry, the docs plugin last update options require Git.'); - } - - return null; - } - - if (!shell.test('-f', filePath)) { - throw new Error( - `Retrieval of git history failed at "${filePath}" because the file does not exist.`, - ); - } - - const fileBasename = path.basename(filePath); - const fileDirname = path.dirname(filePath); - const result = shell.exec( - `git log --max-count=1 --format=%ct,%an -- "${fileBasename}"`, - { - cwd: fileDirname, // this is needed: https://github.com/facebook/docusaurus/pull/5048 - silent: true, - }, - ); - if (result.code !== 0) { - throw new Error( - `Retrieval of git history failed at "${filePath}" with exit code ${result.code}: ${result.stderr}`, - ); - } - return getTimestampAndAuthor(result.stdout.trim()); + const result = getFileCommitDate(filePath, { + age: 'newest', + includeAuthor: true, + }); + return {timestamp: result.timestamp, author: result.author}; } catch (e) { - logger.error(e); + if (e instanceof GitNotFoundError && !showedGitRequirementError) { + logger.warn('Sorry, the docs plugin last update options require Git.'); + showedGitRequirementError = true; + } else { + logger.error(e); + } + return null; } - - return null; } diff --git a/packages/docusaurus-utils/package.json b/packages/docusaurus-utils/package.json index 05e6b2b399..86ecac7814 100644 --- a/packages/docusaurus-utils/package.json +++ b/packages/docusaurus-utils/package.json @@ -29,6 +29,7 @@ "lodash": "^4.17.20", "micromatch": "^4.0.4", "resolve-pathname": "^3.0.0", + "shelljs": "^0.8.4", "tslib": "^2.3.1", "url-loader": "^4.1.1", "webpack": "^5.68.0" diff --git a/packages/docusaurus-utils/src/gitUtils.ts b/packages/docusaurus-utils/src/gitUtils.ts new file mode 100644 index 0000000000..0d368972fc --- /dev/null +++ b/packages/docusaurus-utils/src/gitUtils.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import path from 'path'; +import shell from 'shelljs'; + +export class GitNotFoundError extends Error {} + +export const getFileCommitDate = ( + file: string, + { + age = 'oldest', + includeAuthor = false, + }: { + age?: 'oldest' | 'newest'; + includeAuthor?: boolean; + }, +): { + date: Date; + timestamp: number; + author?: string; +} => { + if (!shell.which('git')) { + throw new GitNotFoundError( + `Failed to retrieve git history for "${file}" because git is not installed.`, + ); + } + + if (!shell.test('-f', file)) { + throw new Error( + `Failed to retrieve git history for "${file}" because the file does not exist.`, + ); + } + + const fileBasename = path.basename(file); + const fileDirname = path.dirname(file); + + let formatArg = '--format=%ct'; + if (includeAuthor) { + formatArg += ',%an'; + } + + let extraArgs = '--max-count=1'; + if (age === 'oldest') { + // --follow is necessary to follow file renames + // --diff-filter=A ensures we only get the commit which (A)dded the file + extraArgs += ' --follow --diff-filter=A'; + } + + const result = shell.exec( + `git log ${extraArgs} ${formatArg} -- "${fileBasename}"`, + { + // cwd is important, see: https://github.com/facebook/docusaurus/pull/5048 + cwd: fileDirname, + silent: true, + }, + ); + if (result.code !== 0) { + throw new Error( + `Failed to retrieve the git history for file "${file}" with exit code ${result.code}: ${result.stderr}`, + ); + } + let regex = /^(?\d+)$/; + if (includeAuthor) { + regex = /^(?\d+),(?.+)$/; + } + + const output = result.stdout.trim(); + const match = output.match(regex); + + if ( + !match || + !match.groups || + !match.groups.timestamp || + (includeAuthor && !match.groups.author) + ) { + throw new Error( + `Failed to retrieve the git history for file "${file}" with unexpected output: ${output}`, + ); + } + + const timestamp = Number(match.groups.timestamp); + const date = new Date(timestamp * 1000); + + if (includeAuthor) { + return {date, timestamp, author: match.groups.author}; + } + return {date, timestamp}; +}; diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 730f908030..37405ee768 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -37,6 +37,7 @@ export { DEFAULT_PLUGIN_ID, WEBPACK_URL_LOADER_LIMIT, } from './constants'; +export {getFileCommitDate, GitNotFoundError} from './gitUtils'; export {normalizeUrl, getEditUrl} from './urlUtils'; export { type Tag,