Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/docusaurus-plugin-content-docs/src/lastUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileLastUpdateData | null> {
): Promise<{timestamp: number; author: string} | null> {
if (!filePath) {
return null;
}
Expand Down
170 changes: 170 additions & 0 deletions packages/docusaurus-utils/src/__tests__/gitUtils.test.ts
Original file line number Diff line number Diff line change
@@ -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 <caroline@jc-verse.com>',
);
fs.writeFileSync(path.join(repoDir, 'test.txt'), 'Updated content');
git.commit(
'Update test.txt',
'2020-06-20',
'Josh-Cena <josh-cena@jc-verse.com>',
);
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 <caroline@jc-verse.com>',
);
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 <josh-cena@jc-verse.com>',
);
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./,
);
});
});
39 changes: 23 additions & 16 deletions packages/docusaurus-utils/src/gitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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.`,
Expand All @@ -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';
Expand All @@ -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,
},
);
Expand All @@ -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};
};
}