diff --git a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts index ae12fb244b..c77ccae4ef 100644 --- a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts +++ b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts @@ -12,14 +12,12 @@ import { GitNotFoundError, } from '@docusaurus/utils'; -type FileLastUpdateData = {timestamp?: number; author?: string}; - let showedGitRequirementError = false; let showedFileNotTrackedError = false; export async function getFileLastUpdate( filePath?: string, -): Promise { +): Promise<{timestamp: number; author: string} | null> { if (!filePath) { return null; } diff --git a/packages/docusaurus-utils/src/__tests__/gitUtils.test.ts b/packages/docusaurus-utils/src/__tests__/gitUtils.test.ts new file mode 100644 index 0000000000..1915ca2e6a --- /dev/null +++ b/packages/docusaurus-utils/src/__tests__/gitUtils.test.ts @@ -0,0 +1,170 @@ +/** + * 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 {FileNotTrackedError, getFileCommitDate} from '../gitUtils'; +import fs from 'fs-extra'; +import path from 'path'; +import os from 'os'; +import shell from 'shelljs'; + +// This function is sync so the same mock repo can be shared across tests +/* eslint-disable no-restricted-properties */ +function createTempRepo() { + const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'git-test-repo')); + class Git { + constructor(private dir: string) { + const res = shell.exec('git init', {cwd: dir, silent: true}); + if (res.code !== 0) { + throw new Error(`git init exited with code ${res.code}. +stderr: ${res.stderr} +stdout: ${res.stdout}`); + } + } + commit(msg: string, date: string, author: string) { + const addRes = shell.exec('git add .', {cwd: this.dir, silent: true}); + const commitRes = shell.exec( + `git commit -m "${msg}" --date "${date}T00:00:00Z" --author "${author}"`, + { + cwd: this.dir, + env: { + GIT_COMMITTER_DATE: `${date}T00:00:00Z`, + GIT_COMMITTER_NAME: author, + }, + silent: true, + }, + ); + if (addRes.code !== 0) { + throw new Error(`git add exited with code ${addRes.code}. +stderr: ${addRes.stderr} +stdout: ${addRes.stdout}`); + } + if (commitRes.code !== 0) { + throw new Error(`git commit exited with code ${commitRes.code}. +stderr: ${commitRes.stderr} +stdout: ${commitRes.stdout}`); + } + } + } + const git = new Git(repoDir); + fs.writeFileSync(path.join(repoDir, 'test.txt'), 'Some content'); + git.commit( + 'Create test.txt', + '2020-06-19', + 'Caroline ', + ); + fs.writeFileSync(path.join(repoDir, 'test.txt'), 'Updated content'); + git.commit( + 'Update test.txt', + '2020-06-20', + 'Josh-Cena ', + ); + fs.writeFileSync(path.join(repoDir, 'test.txt'), 'Updated content (2)'); + fs.writeFileSync(path.join(repoDir, 'moved.txt'), 'This file is moved'); + git.commit( + 'Update test.txt again, create moved.txt', + '2020-09-13', + 'Caroline ', + ); + fs.moveSync(path.join(repoDir, 'moved.txt'), path.join(repoDir, 'dest.txt')); + git.commit( + 'Rename moved.txt to dest.txt', + '2020-11-13', + 'Josh-Cena ', + ); + fs.writeFileSync(path.join(repoDir, 'untracked.txt'), "I'm untracked"); + return repoDir; +} + +describe('getFileCommitDate', () => { + const repoDir = createTempRepo(); + it('returns earliest commit date', async () => { + expect(getFileCommitDate(path.join(repoDir, 'test.txt'), {})).toEqual({ + date: new Date('2020-06-19'), + timestamp: new Date('2020-06-19').getTime() / 1000, + }); + expect(getFileCommitDate(path.join(repoDir, 'dest.txt'), {})).toEqual({ + date: new Date('2020-09-13'), + timestamp: new Date('2020-09-13').getTime() / 1000, + }); + }); + it('returns latest commit date', async () => { + expect( + getFileCommitDate(path.join(repoDir, 'test.txt'), {age: 'newest'}), + ).toEqual({ + date: new Date('2020-09-13'), + timestamp: new Date('2020-09-13').getTime() / 1000, + }); + expect( + getFileCommitDate(path.join(repoDir, 'dest.txt'), {age: 'newest'}), + ).toEqual({ + date: new Date('2020-11-13'), + timestamp: new Date('2020-11-13').getTime() / 1000, + }); + }); + it('returns latest commit date with author', async () => { + expect( + getFileCommitDate(path.join(repoDir, 'test.txt'), { + age: 'oldest', + includeAuthor: true, + }), + ).toEqual({ + date: new Date('2020-06-19'), + timestamp: new Date('2020-06-19').getTime() / 1000, + author: 'Caroline', + }); + expect( + getFileCommitDate(path.join(repoDir, 'dest.txt'), { + age: 'oldest', + includeAuthor: true, + }), + ).toEqual({ + date: new Date('2020-09-13'), + timestamp: new Date('2020-09-13').getTime() / 1000, + author: 'Caroline', + }); + }); + it('returns earliest commit date with author', async () => { + expect( + getFileCommitDate(path.join(repoDir, 'test.txt'), { + age: 'newest', + includeAuthor: true, + }), + ).toEqual({ + date: new Date('2020-09-13'), + timestamp: new Date('2020-09-13').getTime() / 1000, + author: 'Caroline', + }); + expect( + getFileCommitDate(path.join(repoDir, 'dest.txt'), { + age: 'newest', + includeAuthor: true, + }), + ).toEqual({ + date: new Date('2020-11-13'), + timestamp: new Date('2020-11-13').getTime() / 1000, + author: 'Josh-Cena', + }); + }); + it('throws custom error when file is not tracked', async () => { + expect(() => + getFileCommitDate(path.join(repoDir, 'untracked.txt'), { + age: 'newest', + includeAuthor: true, + }), + ).toThrowError(FileNotTrackedError); + }); + it('throws when file not found', async () => { + expect(() => + getFileCommitDate(path.join(repoDir, 'nonexistent.txt'), { + age: 'newest', + includeAuthor: true, + }), + ).toThrowError( + /Failed to retrieve git history for ".*nonexistent.txt" because the file does not exist./, + ); + }); +}); diff --git a/packages/docusaurus-utils/src/gitUtils.ts b/packages/docusaurus-utils/src/gitUtils.ts index 204b3f564d..06f0e8f295 100644 --- a/packages/docusaurus-utils/src/gitUtils.ts +++ b/packages/docusaurus-utils/src/gitUtils.ts @@ -12,7 +12,22 @@ export class GitNotFoundError extends Error {} export class FileNotTrackedError extends Error {} -export const getFileCommitDate = ( +export function getFileCommitDate( + file: string, + args: {age?: 'oldest' | 'newest'; includeAuthor?: false}, +): { + date: Date; + timestamp: number; +}; +export function getFileCommitDate( + file: string, + args: {age?: 'oldest' | 'newest'; includeAuthor: true}, +): { + date: Date; + timestamp: number; + author: string; +}; +export function getFileCommitDate( file: string, { age = 'oldest', @@ -25,7 +40,7 @@ export const getFileCommitDate = ( 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.`, @@ -38,9 +53,6 @@ export const getFileCommitDate = ( ); } - const fileBasename = path.basename(file); - const fileDirname = path.dirname(file); - let formatArg = '--format=%ct'; if (includeAuthor) { formatArg += ',%an'; @@ -54,10 +66,10 @@ export const getFileCommitDate = ( } const result = shell.exec( - `git log ${extraArgs} ${formatArg} -- "${fileBasename}"`, + `git log ${extraArgs} ${formatArg} -- "${path.basename(file)}"`, { // cwd is important, see: https://github.com/facebook/docusaurus/pull/5048 - cwd: fileDirname, + cwd: path.dirname(file), silent: true, }, ); @@ -81,22 +93,17 @@ export const getFileCommitDate = ( const match = output.match(regex); - if ( - !match || - !match.groups || - !match.groups.timestamp || - (includeAuthor && !match.groups.author) - ) { + if (!match) { throw new Error( `Failed to retrieve the git history for file "${file}" with unexpected output: ${output}`, ); } - const timestamp = Number(match.groups.timestamp); + const timestamp = Number(match.groups!.timestamp); const date = new Date(timestamp * 1000); if (includeAuthor) { - return {date, timestamp, author: match.groups.author}; + return {date, timestamp, author: match.groups!.author!}; } return {date, timestamp}; -}; +}