Skip to content

Commit

Permalink
Use spawnSync instead of execaSync in git.ts (#1347)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Swithinbank <[email protected]>
  • Loading branch information
kevinzunigacuellar and delucis authored Jan 9, 2024
1 parent bbf9998 commit 8994d00
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 79 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-carrots-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/starlight': patch
---

Refactor `getLastUpdated` to use `node:child_process` instead of `execa`.
117 changes: 117 additions & 0 deletions packages/starlight/__tests__/basics/git.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { mkdtempSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { describe, expect, test } from 'vitest';
import { getNewestCommitDate } from '../../utils/git';

describe('getNewestCommitDate', () => {
const { commitAllChanges, getFilePath, writeFile } = makeTestRepo();

test('returns the newest commit date', () => {
const file = 'updated.md';
const lastCommitDate = '2023-06-25';

writeFile(file, 'content 0');
commitAllChanges('add updated.md', '2023-06-21');
writeFile(file, 'content 1');
commitAllChanges('update updated.md', lastCommitDate);

expectCommitDateToEqual(getNewestCommitDate(getFilePath(file)), lastCommitDate);
});

test('returns the initial commit date for a file never updated', () => {
const file = 'added.md';
const commitDate = '2022-09-18';

writeFile(file, 'content');
commitAllChanges('add added.md', commitDate);

expectCommitDateToEqual(getNewestCommitDate(getFilePath(file)), commitDate);
});

test('returns the newest commit date for a file with a name that contains a space', () => {
const file = 'updated with space.md';
const lastCommitDate = '2021-01-02';

writeFile(file, 'content 0');
commitAllChanges('add updated.md', '2021-01-01');
writeFile(file, 'content 1');
commitAllChanges('update updated.md', lastCommitDate);

expectCommitDateToEqual(getNewestCommitDate(getFilePath(file)), lastCommitDate);
});

test('returns the newest commit date for a file updated the same day', () => {
const file = 'updated-same-day.md';
const lastCommitDate = '2023-06-25T14:22:35Z';

writeFile(file, 'content 0');
commitAllChanges('add updated.md', '2023-06-25T12:34:56Z');
writeFile(file, 'content 1');
commitAllChanges('update updated.md', lastCommitDate);

expectCommitDateToEqual(getNewestCommitDate(getFilePath(file)), lastCommitDate);
});

test('throws when failing to retrieve the git history for a file', () => {
expect(() => getNewestCommitDate(getFilePath('../not-a-starlight-test-repo/test.md'))).toThrow(
/^Failed to retrieve the git history for file "[/\\-\w ]+\/test\.md"/
);
});

test('throws when trying to get the history of a non-existing or untracked file', () => {
const expectedError =
/^Failed to validate the timestamp for file "[/\\-\w ]+\/(?:unknown|untracked)\.md"$/;
writeFile('untracked.md', 'content');

expect(() => getNewestCommitDate(getFilePath('unknown.md'))).toThrow(expectedError);
expect(() => getNewestCommitDate(getFilePath('untracked.md'))).toThrow(expectedError);
});
});

function expectCommitDateToEqual(commitDate: CommitDate, expectedDateStr: ISODate) {
const expectedDate = new Date(expectedDateStr);
expect(commitDate).toStrictEqual(expectedDate);
}

function makeTestRepo() {
const repoPath = mkdtempSync(join(tmpdir(), 'starlight-test-git-'));

function runInRepo(command: string, args: string[], env: NodeJS.ProcessEnv = {}) {
const result = spawnSync(command, args, { cwd: repoPath, env });

if (result.status !== 0) {
throw new Error(`Failed to execute test repository command: '${command} ${args.join(' ')}'`);
}
}

// Configure git specifically for this test repository.
runInRepo('git', ['init']);
runInRepo('git', ['config', 'user.name', 'starlight-test']);
runInRepo('git', ['config', 'user.email', '[email protected]']);
runInRepo('git', ['config', 'commit.gpgsign', 'false']);

return {
// The `dateStr` argument should be in the `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ` format.
commitAllChanges(message: string, dateStr: ISODate) {
const date = dateStr.endsWith('Z') ? dateStr : `${dateStr}T00:00:00Z`;

runInRepo('git', ['add', '-A']);
// This sets both the author and committer dates to the provided date.
runInRepo('git', ['commit', '-m', message, '--date', date], { GIT_COMMITTER_DATE: date });
},
getFilePath(name: string) {
return join(repoPath, name);
},
writeFile(name: string, content: string) {
writeFileSync(join(repoPath, name), content);
},
};
}

type ISODate =
| `${number}-${number}-${number}`
| `${number}-${number}-${number}T${number}:${number}:${number}Z`;

type CommitDate = ReturnType<typeof getNewestCommitDate>;
8 changes: 4 additions & 4 deletions packages/starlight/__tests__/i18n-root-locale/routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ test('fallback routes use their own locale data', () => {
});

test('fallback routes use fallback entry last updated dates', () => {
const getFileCommitDate = vi.spyOn(git, 'getFileCommitDate');
const getNewestCommitDate = vi.spyOn(git, 'getNewestCommitDate');
const route = routes.find((route) => route.entry.id === routes[4]!.id && route.locale === 'en');
assert(route, 'Expected to find English fallback route for `guides/authoring-content.md`.');

Expand All @@ -80,11 +80,11 @@ test('fallback routes use fallback entry last updated dates', () => {
url: new URL('https://example.com/en'),
});

expect(getFileCommitDate).toHaveBeenCalledOnce();
expect(getFileCommitDate.mock.lastCall?.[0]).toMatch(
expect(getNewestCommitDate).toHaveBeenCalledOnce();
expect(getNewestCommitDate.mock.lastCall?.[0]).toMatch(
/src\/content\/docs\/guides\/authoring-content.md$/
// ^ no `en/` prefix
);

getFileCommitDate.mockRestore();
getNewestCommitDate.mockRestore();
});
1 change: 0 additions & 1 deletion packages/starlight/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@
"@types/mdast": "^4.0.3",
"astro-expressive-code": "^0.30.1",
"bcp-47": "^2.1.0",
"execa": "^8.0.1",
"hast-util-select": "^6.0.2",
"hastscript": "^8.0.0",
"mdast-util-directive": "^3.0.0",
Expand Down
74 changes: 12 additions & 62 deletions packages/starlight/utils/git.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,24 @@
/**
* Heavily inspired by
* https://github.com/facebook/docusaurus/blob/46d2aa231ddb18ed67311b6195260af46d7e8bdc/packages/docusaurus-utils/src/gitUtils.ts
*
* 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 { basename, dirname } from 'node:path';
import { execaSync } from 'execa';
import { spawnSync } from 'node:child_process';

/** Custom error thrown when the current file is not tracked by git. */
class FileNotTrackedError extends Error {}
export function getNewestCommitDate(file: string) {
const result = spawnSync('git', ['log', '--format=%ct', '--max-count=1', basename(file)], {
cwd: dirname(file),
encoding: 'utf-8',
});

/**
* Fetches the git history of a file and returns a relevant commit date.
* It gets the commit date instead of author date so that amended commits
* can have their dates updated.
*
* @throws {FileNotTrackedError} If the current file is not tracked by git.
* @throws Also throws when `git log` exited with non-zero, or when it outputs
* unexpected text.
*/
export function getFileCommitDate(
file: string,
age: 'oldest' | 'newest' = 'oldest'
): {
date: Date;
timestamp: number;
} {
const result = execaSync(
'git',
[
'log',
`--format=%ct`,
'--max-count=1',
...(age === 'oldest' ? ['--follow', '--diff-filter=A'] : []),
'--',
basename(file),
],
{
cwd: dirname(file),
}
);
if (result.exitCode !== 0) {
throw new Error(
`Failed to retrieve the git history for file "${file}" with exit code ${result.exitCode}: ${result.stderr}`
);
if (result.error) {
throw new Error(`Failed to retrieve the git history for file "${file}"`);
}

const output = result.stdout.trim();

if (!output) {
throw new FileNotTrackedError(
`Failed to retrieve the git history for file "${file}" because the file is not tracked by git.`
);
}

const regex = /^(?<timestamp>\d+)$/;
const match = output.match(regex);

if (!match) {
throw new Error(
`Failed to retrieve the git history for file "${file}" with unexpected output: ${output}`
);
if (!match?.groups?.timestamp) {
throw new Error(`Failed to validate the timestamp for file "${file}"`);
}

const timestamp = Number(match.groups!.timestamp);
const timestamp = Number(match.groups.timestamp);
const date = new Date(timestamp * 1000);

return { date, timestamp };
return date;
}
23 changes: 14 additions & 9 deletions packages/starlight/utils/route-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url';
import project from 'virtual:starlight/project-context';
import config from 'virtual:starlight/user-config';
import { generateToC, type TocItem } from './generateToC';
import { getFileCommitDate } from './git';
import { getNewestCommitDate } from './git';
import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
import { ensureTrailingSlash } from './path';
import type { Route } from './routing';
Expand Down Expand Up @@ -70,17 +70,22 @@ function getToC({ entry, locale, headings }: PageProps) {
}

function getLastUpdated({ entry }: PageProps): Date | undefined {
if (entry.data.lastUpdated ?? config.lastUpdated) {
const { lastUpdated: frontmatterLastUpdated } = entry.data;
const { lastUpdated: configLastUpdated } = config;

if (frontmatterLastUpdated ?? configLastUpdated) {
const currentFilePath = fileURLToPath(new URL('src/content/docs/' + entry.id, project.root));
let date = typeof entry.data.lastUpdated !== 'boolean' ? entry.data.lastUpdated : undefined;
if (!date) {
try {
({ date } = getFileCommitDate(currentFilePath, 'newest'));
} catch {}
try {
return frontmatterLastUpdated instanceof Date
? frontmatterLastUpdated
: getNewestCommitDate(currentFilePath);
} catch {
// If the git command fails, ignore the error.
return undefined;
}
return date;
}
return;

return undefined;
}

function getEditUrl({ entry, id, isFallback }: PageProps): URL | undefined {
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 comment on commit 8994d00

@vercel
Copy link

@vercel vercel bot commented on 8994d00 Jan 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.