diff --git a/packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts b/packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts index 1766e6ff3a..3767dbd3ad 100644 --- a/packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts +++ b/packages/docusaurus-utils/src/vcs/__tests__/gitUtils.test.ts @@ -17,6 +17,7 @@ import { getGitCreation, getGitRepoRoot, getGitSuperProjectRoot, + getGitSubmodulePaths, } from '../gitUtils'; class Git { @@ -488,4 +489,63 @@ describe('submodules APIs', () => { `); }); }); + + describe('getGitSubmodulePaths', () => { + it('returns submodules for cwd=superproject', async () => { + const repo = await initTestRepo(); + const cwd = path.join(repo.superproject.repoDir); + await expect(getGitSubmodulePaths(cwd)).resolves.toEqual([ + 'submodules/submodule1', + 'submodules/submodule2', + ]); + }); + + it('returns submodules for cwd=superproject/website/docs', async () => { + const repo = await initTestRepo(); + const cwd = path.join(repo.superproject.repoDir, 'website', 'docs'); + await expect(getGitSubmodulePaths(cwd)).resolves.toEqual([ + // The returned paths are relative to CWD, + // Not sure if it's the best behavior. + // But you'd rather call this with the superproject root as CWD anyway! + '../../submodules/submodule1', + '../../submodules/submodule2', + ]); + }); + + it('returns [] for cwd=submodules/submodule1', async () => { + const repo = await initTestRepo(); + const cwd = path.join( + repo.superproject.repoDir, + 'submodules', + 'submodule1', + ); + await expect(getGitSubmodulePaths(cwd)).resolves.toEqual([]); + }); + + it('returns [] for cwd=submodules/submodule2/subDir', async () => { + const repo = await initTestRepo(); + const cwd = path.join( + repo.superproject.repoDir, + 'submodules', + 'submodule2', + 'subDir', + ); + await expect(getGitSubmodulePaths(cwd)).resolves.toEqual([]); + }); + + it('rejects for cwd=doesNotExist', async () => { + const repo = await initTestRepo(); + const cwd = path.join(repo.superproject.repoDir, 'doesNotExist'); + await expect(getGitSubmodulePaths(cwd)).rejects.toThrow( + /Couldn't read the list of git submodules/, + ); + }); + + it('rejects for cwd=notTracked', async () => { + const cwd = await os.tmpdir(); + await expect(getGitSubmodulePaths(cwd)).rejects.toThrow( + /Couldn't read the list of git submodules/, + ); + }); + }); }); diff --git a/packages/docusaurus-utils/src/vcs/gitUtils.ts b/packages/docusaurus-utils/src/vcs/gitUtils.ts index 55f9e7a2dc..7a468a3b36 100644 --- a/packages/docusaurus-utils/src/vcs/gitUtils.ts +++ b/packages/docusaurus-utils/src/vcs/gitUtils.ts @@ -338,3 +338,55 @@ The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue( } return getGitRepoRoot(cwd); } + +// See https://git-scm.com/book/en/v2/Git-Tools-Submodules +export async function getGitSubmodulePaths(cwd: string): Promise { + const createErrorMessageBase = () => { + return `Couldn't read the list of git submodules +Failure while running ${logger.code( + 'git submodule status', + )} from cwd=${logger.path(cwd)}`; + }; + + const result = await execa('git', ['submodule', 'status'], { + cwd, + }).catch((error) => { + // We enter this rejection when cwd is not a dir for example + throw new Error( + `${createErrorMessageBase()} +The command executed throws an error: ${error.message}`, + {cause: error}, + ); + }); + + if (result.exitCode !== 0) { + throw new Error( + `${createErrorMessageBase()} +The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue( + result.stderr, + )}`, + ); + } + + const output = result.stdout.trim(); + + if (!output) { + return []; + } + + /* The output may contain a space/-/+/U prefix, for example + 1234567e3e35d1f5b submodules/foo (heads/main) + -9ab1f1d3a2d77b0a4 submodules/bar (heads/dev) + +f00ba42e1b3ddead submodules/baz (remotes/origin/main) + Udeadbeefcafe1234 submodules/qux + */ + const getSubmodulePath = async (line: string) => { + const submodulePath = line.substring(1).split(' ')[1]; + if (!submodulePath) { + throw new Error(`Failed to parse git submodule line: ${line}`); + } + return submodulePath; + }; + + return Promise.all(output.split('\n').map(getSubmodulePath)); +}