summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Zuniga Cuellar2024-01-09 14:15:55 -0500
committerGitHub2024-01-09 20:15:55 +0100
commit8994d007266e0bd8e6116b306ccd9e24c9710411 (patch)
tree6326af521596aad5ce292124921140b64a3c9899
parentbbf9998fc1ddbf6e17f5f41af1e251402c81322a (diff)
downloadIT.starlight-8994d007266e0bd8e6116b306ccd9e24c9710411.tar.gz
IT.starlight-8994d007266e0bd8e6116b306ccd9e24c9710411.tar.bz2
IT.starlight-8994d007266e0bd8e6116b306ccd9e24c9710411.zip
Use spawnSync instead of execaSync in `git.ts` (#1347)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r--.changeset/plenty-carrots-rescue.md5
-rw-r--r--packages/starlight/__tests__/basics/git.test.ts117
-rw-r--r--packages/starlight/__tests__/i18n-root-locale/routing.test.ts8
-rw-r--r--packages/starlight/package.json1
-rw-r--r--packages/starlight/utils/git.ts74
-rw-r--r--packages/starlight/utils/route-data.ts23
-rw-r--r--pnpm-lock.yaml3
7 files changed, 152 insertions, 79 deletions
diff --git a/.changeset/plenty-carrots-rescue.md b/.changeset/plenty-carrots-rescue.md
new file mode 100644
index 00000000..e256fc05
--- /dev/null
+++ b/.changeset/plenty-carrots-rescue.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight': patch
+---
+
+Refactor `getLastUpdated` to use `node:child_process` instead of `execa`.
diff --git a/packages/starlight/__tests__/basics/git.test.ts b/packages/starlight/__tests__/basics/git.test.ts
new file mode 100644
index 00000000..1f194f28
--- /dev/null
+++ b/packages/starlight/__tests__/basics/git.test.ts
@@ -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', 'starlight-test@example.com']);
+ 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>;
diff --git a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
index b7fbd020..c6824349 100644
--- a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
+++ b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
@@ -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`.');
@@ -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();
});
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index c6ff2ede..44d00414 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -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",
diff --git a/packages/starlight/utils/git.ts b/packages/starlight/utils/git.ts
index 15d802f7..d69bd259 100644
--- a/packages/starlight/utils/git.ts
+++ b/packages/starlight/utils/git.ts
@@ -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;
}
diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts
index 484fea08..b7a23e6c 100644
--- a/packages/starlight/utils/route-data.ts
+++ b/packages/starlight/utils/route-data.ts
@@ -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';
@@ -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 {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 07b41e11..b9203313 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -147,9 +147,6 @@ importers:
bcp-47:
specifier: ^2.1.0
version: 2.1.0
- execa:
- specifier: ^8.0.1
- version: 8.0.1
hast-util-select:
specifier: ^6.0.2
version: 6.0.2