feat(content-blog): infer blog post date from git history (#6593)

This commit is contained in:
Felipe Santos 2022-02-09 13:18:32 -03:00 committed by GitHub
parent 665d164351
commit 6996ed2f2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 127 additions and 57 deletions

View file

@ -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: [],

View file

@ -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);

View file

@ -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": {

View file

@ -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;
} }
}

View file

@ -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"

View 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};
};

View file

@ -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,