add getGitSuperProjectRoot API

This commit is contained in:
sebastien 2025-11-07 15:19:09 +01:00
parent 3b0abf4d68
commit e5f67712ec
2 changed files with 187 additions and 4 deletions

View File

@ -16,6 +16,7 @@ import {
getGitLastUpdate,
getGitCreation,
getGitRepoRoot,
getGitSuperProjectRoot,
} from '../gitUtils';
class Git {
@ -95,6 +96,54 @@ class Git {
{env: {GIT_COMMITTER_DATE: `${date}T00:00:00Z`}},
);
}
async commitFile(
filePath: string,
{
fileContent,
commitMessage,
commitDate,
commitAuthor,
}: {
fileContent?: string;
commitMessage?: string;
commitDate?: string;
commitAuthor?: string;
} = {},
): Promise<void> {
await fs.ensureDir(path.join(this.dir, path.dirname(filePath)));
await fs.writeFile(
path.join(this.dir, filePath),
fileContent ?? `Content of ${filePath}`,
);
await this.commit(
commitMessage ?? `Create ${filePath}`,
commitDate ?? '2020-06-19',
commitAuthor ?? 'Seb <seb@example.com>',
);
}
async addSubmodule(name: string, repoPath: string): Promise<void> {
return this.runOptimisticGitCommand('git', [
'-c protocol.file.allow=always',
'submodule',
'add',
repoPath,
name,
]);
}
async defineSubmodules(submodules: {[name: string]: string}): Promise<void> {
for (const entry of Object.entries(submodules)) {
await this.addSubmodule(entry[0], entry[1]);
}
await this.runOptimisticGitCommand('git', [
'submodule',
'update',
'--init',
'--recursive',
]);
}
}
async function createGitRepoEmpty(): Promise<{repoDir: string; git: Git}> {
@ -348,3 +397,91 @@ describe('getGitRepoRoot', () => {
);
});
});
describe('submodules APIs', () => {
async function initTestRepo() {
const superproject = await createGitRepoEmpty();
await superproject.git.commitFile('README.md');
await superproject.git.commitFile('website/docs/myDoc.md');
const submodule1 = await createGitRepoEmpty();
await submodule1.git.commitFile('file1.txt');
const submodule2 = await createGitRepoEmpty();
await submodule2.git.commitFile('subDir/file2.txt');
await superproject.git.defineSubmodules({
'submodules/submodule1': submodule1.repoDir,
'submodules/submodule2': submodule2.repoDir,
});
return {superproject, submodule1, submodule2};
}
describe('getGitSuperProjectRoot', () => {
it('returns superproject dir for cwd=superproject', async () => {
const repo = await initTestRepo();
const cwd = path.join(repo.superproject.repoDir);
await expect(getGitSuperProjectRoot(cwd)).resolves.toEqual(
repo.superproject.repoDir,
);
});
it('returns superproject dir for cwd=superproject/submodules', async () => {
const repo = await initTestRepo();
const cwd = path.join(repo.superproject.repoDir, 'submodules');
await expect(getGitSuperProjectRoot(cwd)).resolves.toEqual(
repo.superproject.repoDir,
);
});
it('returns superproject dir for cwd=superproject/website/docs', async () => {
const repo = await initTestRepo();
const cwd = path.join(repo.superproject.repoDir, 'website/docs');
await expect(getGitSuperProjectRoot(cwd)).resolves.toEqual(
repo.superproject.repoDir,
);
});
it('returns superproject dir for cwd=submodule1', async () => {
const repo = await initTestRepo();
const cwd = path.join(repo.superproject.repoDir, 'submodules/submodule1');
await expect(getGitSuperProjectRoot(cwd)).resolves.toEqual(
repo.superproject.repoDir,
);
});
it('returns superproject dir for cwd=submodule2', async () => {
const repo = await initTestRepo();
const cwd = path.join(repo.superproject.repoDir, 'submodules/submodule2');
await expect(getGitSuperProjectRoot(cwd)).resolves.toEqual(
repo.superproject.repoDir,
);
});
it('returns superproject dir for cwd=submodule2/subDir', async () => {
const repo = await initTestRepo();
const cwd = path.join(
repo.superproject.repoDir,
'submodules/submodule2/subDir',
);
await expect(getGitSuperProjectRoot(cwd)).resolves.toEqual(
repo.superproject.repoDir,
);
});
it('rejects for cwd of untracked dir', async () => {
const cwd = await os.tmpdir();
// Do we really want this to throw?
// Not sure, and Git doesn't help us failsafe and return null...
await expect(getGitSuperProjectRoot(cwd)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Couldn't find the git superproject root directory
Failure while running \`git rev-parse --show-superproject-working-tree\` from cwd="<TEMP_DIR>"
The command executed throws an error: Command failed with exit code 128: git rev-parse --show-superproject-working-tree
fatal: not a git repository (or any of the parent directories): .git"
`);
});
});
});

View File

@ -265,9 +265,9 @@ export async function getGitCreation(
export async function getGitRepoRoot(cwd: string): Promise<string> {
const createErrorMessageBase = () => {
return `Couldn't find the git repository root directory
Running ${logger.code('git rev-parse --show-toplevel')} from cwd=${logger.path(
cwd,
)})`;
Failure while running ${logger.code(
'git rev-parse --show-toplevel',
)} from cwd=${logger.path(cwd)}`;
};
const result = await execa('git', ['rev-parse', '--show-toplevel'], {
@ -276,7 +276,7 @@ Running ${logger.code('git rev-parse --show-toplevel')} from cwd=${logger.path(
// We enter this rejection when cwd is not a dir for example
throw new Error(
`${createErrorMessageBase()}
The command executed throws an error`,
The command executed throws an error: ${error.message}`,
{cause: error},
);
});
@ -292,3 +292,49 @@ The command returned exit code ${logger.code(result.exitCode)}: ${logger.subdue(
return fs.realpath.native(result.stdout.trim());
}
// A Git "superproject" is a Git repository that contains submodules
// See https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt---show-superproject-working-tree
// See https://git-scm.com/book/en/v2/Git-Tools-Submodules
export async function getGitSuperProjectRoot(
cwd: string,
): Promise<string | null> {
const createErrorMessageBase = () => {
return `Couldn't find the git superproject root directory
Failure while running ${logger.code(
'git rev-parse --show-superproject-working-tree',
)} from cwd=${logger.path(cwd)}`;
};
const result = await execa(
'git',
['rev-parse', '--show-superproject-working-tree'],
{
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();
// this command only works when inside submodules
// otherwise it doesn't return anything when we are inside the main repo
if (output) {
return output;
}
return getGitRepoRoot(cwd);
}