add getGitSubmodulePaths API

This commit is contained in:
sebastien 2025-11-07 15:49:07 +01:00
parent 9162f1baa4
commit d243079036
2 changed files with 112 additions and 0 deletions

View File

@ -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/,
);
});
});
});

View File

@ -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<string[]> {
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));
}