mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-30 08:28:00 +02:00
feat(content-blog): infer blog post date from git history (#6593)
This commit is contained in:
parent
665d164351
commit
6996ed2f2f
7 changed files with 127 additions and 57 deletions
|
@ -5,14 +5,13 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import pluginContentBlog from '../index';
|
import pluginContentBlog from '../index';
|
||||||
import type {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types';
|
import type {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types';
|
||||||
import {PluginOptionSchema} from '../pluginOptionSchema';
|
import {PluginOptionSchema} from '../pluginOptionSchema';
|
||||||
import type {BlogPost} from '../types';
|
import type {BlogPost} from '../types';
|
||||||
import type {Joi} from '@docusaurus/utils-validation';
|
import type {Joi} from '@docusaurus/utils-validation';
|
||||||
import {posixPath} from '@docusaurus/utils';
|
import {posixPath, getFileCommitDate} from '@docusaurus/utils';
|
||||||
import type {
|
import type {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
EditUrlFunction,
|
EditUrlFunction,
|
||||||
|
@ -425,14 +424,15 @@ describe('loadBlog', () => {
|
||||||
);
|
);
|
||||||
const blogPosts = await getBlogPosts(siteDir);
|
const blogPosts = await getBlogPosts(siteDir);
|
||||||
const noDateSource = path.posix.join('@site', PluginPath, 'no date.md');
|
const noDateSource = path.posix.join('@site', PluginPath, 'no date.md');
|
||||||
const noDateSourceBirthTime = (
|
const noDateSourceFile = path.posix.join(siteDir, PluginPath, 'no date.md');
|
||||||
await fs.stat(noDateSource.replace('@site', siteDir))
|
// we know the file exist and we know we have git
|
||||||
).birthtime;
|
const result = getFileCommitDate(noDateSourceFile, {age: 'oldest'});
|
||||||
|
const noDateSourceTime = result.date;
|
||||||
const formattedDate = Intl.DateTimeFormat('en', {
|
const formattedDate = Intl.DateTimeFormat('en', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
}).format(noDateSourceBirthTime);
|
}).format(noDateSourceTime);
|
||||||
|
|
||||||
expect({
|
expect({
|
||||||
...getByTitle(blogPosts, 'no date').metadata,
|
...getByTitle(blogPosts, 'no date').metadata,
|
||||||
|
@ -445,7 +445,7 @@ describe('loadBlog', () => {
|
||||||
title: 'no date',
|
title: 'no date',
|
||||||
description: `no date`,
|
description: `no date`,
|
||||||
authors: [],
|
authors: [],
|
||||||
date: noDateSourceBirthTime,
|
date: noDateSourceTime,
|
||||||
formattedDate,
|
formattedDate,
|
||||||
frontMatter: {},
|
frontMatter: {},
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
Globby,
|
Globby,
|
||||||
normalizeFrontMatterTags,
|
normalizeFrontMatterTags,
|
||||||
groupTaggedItems,
|
groupTaggedItems,
|
||||||
|
getFileCommitDate,
|
||||||
getContentPathList,
|
getContentPathList,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import type {LoadContext} from '@docusaurus/types';
|
import type {LoadContext} from '@docusaurus/types';
|
||||||
|
@ -242,9 +243,18 @@ async function processBlogSourceFile(
|
||||||
} else if (parsedBlogFileName.date) {
|
} else if (parsedBlogFileName.date) {
|
||||||
return parsedBlogFileName.date;
|
return parsedBlogFileName.date;
|
||||||
}
|
}
|
||||||
// Fallback to file create time
|
|
||||||
|
try {
|
||||||
|
const result = getFileCommitDate(blogSourceAbsolute, {
|
||||||
|
age: 'oldest',
|
||||||
|
includeAuthor: false,
|
||||||
|
});
|
||||||
|
return result.date;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
return (await fs.stat(blogSourceAbsolute)).birthtime;
|
return (await fs.stat(blogSourceAbsolute)).birthtime;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const date = await getDate();
|
const date = await getDate();
|
||||||
const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
|
const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
|
||||||
|
|
|
@ -34,7 +34,6 @@
|
||||||
"js-yaml": "^4.0.0",
|
"js-yaml": "^4.0.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"remark-admonitions": "^1.2.1",
|
"remark-admonitions": "^1.2.1",
|
||||||
"shelljs": "^0.8.4",
|
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"utility-types": "^3.10.0",
|
"utility-types": "^3.10.0",
|
||||||
"webpack": "^5.68.0"
|
"webpack": "^5.68.0"
|
||||||
|
@ -47,6 +46,7 @@
|
||||||
"commander": "^5.1.0",
|
"commander": "^5.1.0",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
"picomatch": "^2.1.1",
|
"picomatch": "^2.1.1",
|
||||||
|
"shelljs": "^0.8.4",
|
||||||
"utility-types": "^3.10.0"
|
"utility-types": "^3.10.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
@ -5,14 +5,11 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import shell from 'shelljs';
|
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import path from 'path';
|
import {getFileCommitDate, GitNotFoundError} from '@docusaurus/utils';
|
||||||
|
|
||||||
type FileLastUpdateData = {timestamp?: number; author?: string};
|
type FileLastUpdateData = {timestamp?: number; author?: string};
|
||||||
|
|
||||||
const GIT_COMMIT_TIMESTAMP_AUTHOR_REGEX = /^(?<timestamp>\d+),(?<author>.+)$/;
|
|
||||||
|
|
||||||
let showedGitRequirementError = false;
|
let showedGitRequirementError = false;
|
||||||
|
|
||||||
export async function getFileLastUpdate(
|
export async function getFileLastUpdate(
|
||||||
|
@ -21,53 +18,22 @@ export async function getFileLastUpdate(
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return null;
|
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
|
// Wrap in try/catch in case the shell commands fail
|
||||||
// (e.g. project doesn't use Git, etc).
|
// (e.g. project doesn't use Git, etc).
|
||||||
try {
|
try {
|
||||||
if (!shell.which('git')) {
|
const result = getFileCommitDate(filePath, {
|
||||||
if (!showedGitRequirementError) {
|
age: 'newest',
|
||||||
showedGitRequirementError = true;
|
includeAuthor: true,
|
||||||
logger.warn('Sorry, the docs plugin last update options require Git.');
|
});
|
||||||
}
|
return {timestamp: result.timestamp, author: result.author};
|
||||||
|
|
||||||
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());
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e instanceof GitNotFoundError && !showedGitRequirementError) {
|
||||||
|
logger.warn('Sorry, the docs plugin last update options require Git.');
|
||||||
|
showedGitRequirementError = true;
|
||||||
|
} else {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"micromatch": "^4.0.4",
|
"micromatch": "^4.0.4",
|
||||||
"resolve-pathname": "^3.0.0",
|
"resolve-pathname": "^3.0.0",
|
||||||
|
"shelljs": "^0.8.4",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^5.68.0"
|
"webpack": "^5.68.0"
|
||||||
|
|
92
packages/docusaurus-utils/src/gitUtils.ts
Normal file
92
packages/docusaurus-utils/src/gitUtils.ts
Normal file
|
@ -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 = /^(?<timestamp>\d+)$/;
|
||||||
|
if (includeAuthor) {
|
||||||
|
regex = /^(?<timestamp>\d+),(?<author>.+)$/;
|
||||||
|
}
|
||||||
|
|
||||||
|
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};
|
||||||
|
};
|
|
@ -37,6 +37,7 @@ export {
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
WEBPACK_URL_LOADER_LIMIT,
|
WEBPACK_URL_LOADER_LIMIT,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
export {getFileCommitDate, GitNotFoundError} from './gitUtils';
|
||||||
export {normalizeUrl, getEditUrl} from './urlUtils';
|
export {normalizeUrl, getEditUrl} from './urlUtils';
|
||||||
export {
|
export {
|
||||||
type Tag,
|
type Tag,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue