mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 23:57:22 +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.
|
||||
*/
|
||||
|
||||
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: [],
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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 = /^(?<timestamp>\d+),(?<author>.+)$/;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
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,
|
||||
WEBPACK_URL_LOADER_LIMIT,
|
||||
} from './constants';
|
||||
export {getFileCommitDate, GitNotFoundError} from './gitUtils';
|
||||
export {normalizeUrl, getEditUrl} from './urlUtils';
|
||||
export {
|
||||
type Tag,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue