create-app: refactor git init

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-10-21 17:42:49 +02:00
parent 9124416926
commit 80bfac5266
7 changed files with 112 additions and 118 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Updated the create-app command to no longer require Git to be installed and configured. A git repository will only be initialized if possible and if not already in an git repository.
-1
View File
@@ -34,7 +34,6 @@
"dependencies": {
"@backstage/cli-common": "workspace:^",
"chalk": "^4.0.0",
"command-exists": "^1.2.9",
"commander": "^9.1.0",
"fs-extra": "10.1.0",
"handlebars": "^4.7.3",
+5 -7
View File
@@ -32,7 +32,7 @@ const promptMock = jest.spyOn(inquirer, 'prompt');
const checkPathExistsMock = jest.spyOn(tasks, 'checkPathExistsTask');
const templatingMock = jest.spyOn(tasks, 'templatingTask');
const checkAppExistsMock = jest.spyOn(tasks, 'checkAppExistsTask');
const initGitRepositoryMock = jest.spyOn(tasks, 'initGitRepository');
const tryInitGitRepositoryMock = jest.spyOn(tasks, 'tryInitGitRepository');
const readGitConfig = jest.spyOn(tasks, 'readGitConfig');
const createTemporaryAppFolderMock = jest.spyOn(
tasks,
@@ -59,8 +59,6 @@ describe('command entrypoint', () => {
dbType: 'PostgreSQL',
});
readGitConfig.mockResolvedValue({
name: 'git-user',
email: 'git-email',
defaultBranch: 'git-default-branch',
});
});
@@ -74,7 +72,7 @@ describe('command entrypoint', () => {
await createApp(cmd);
expect(checkAppExistsMock).toHaveBeenCalled();
expect(createTemporaryAppFolderMock).toHaveBeenCalled();
expect(initGitRepositoryMock).toHaveBeenCalled();
expect(tryInitGitRepositoryMock).toHaveBeenCalled();
expect(templatingMock).toHaveBeenCalled();
expect(moveAppMock).toHaveBeenCalled();
expect(buildAppMock).toHaveBeenCalled();
@@ -84,7 +82,7 @@ describe('command entrypoint', () => {
const cmd = { path: 'myDirectory' } as unknown as Command;
await createApp(cmd);
expect(checkPathExistsMock).toHaveBeenCalled();
expect(initGitRepositoryMock).toHaveBeenCalled();
expect(tryInitGitRepositoryMock).toHaveBeenCalled();
expect(templatingMock).toHaveBeenCalled();
expect(buildAppMock).toHaveBeenCalled();
});
@@ -97,8 +95,8 @@ describe('command entrypoint', () => {
it('should not call `initGitRepository` when `gitConfig` is undefined', async () => {
const cmd = {} as unknown as Command;
readGitConfig.mockResolvedValue({});
readGitConfig.mockResolvedValue(undefined);
await createApp(cmd);
expect(initGitRepositoryMock).not.toHaveBeenCalled();
expect(tryInitGitRepositoryMock).not.toHaveBeenCalled();
});
});
+8 -5
View File
@@ -28,7 +28,7 @@ import {
createTemporaryAppFolderTask,
moveAppTask,
templatingTask,
initGitRepository,
tryInitGitRepository,
readGitConfig,
} from './lib/tasks';
@@ -109,13 +109,16 @@ export default async (opts: OptionValues): Promise<void> => {
await moveAppTask(tempDir, appDir, answers.name);
}
if (gitConfig?.name && gitConfig?.email) {
Task.section('Initializing git repository');
await initGitRepository(appDir);
if (gitConfig) {
if (await tryInitGitRepository(appDir)) {
// Since we don't know whether we were able to init git before we
// try, we can't track the actual task execution
Task.forItem('init', 'git repository', async () => {});
}
}
if (!opts.skipInstall) {
Task.section('Building the app');
Task.section('Installing dependencies');
await buildAppTask(appDir);
}
+62 -57
View File
@@ -27,12 +27,10 @@ import {
createTemporaryAppFolderTask,
moveAppTask,
templatingTask,
initGitRepository,
tryInitGitRepository,
readGitConfig,
} from './tasks';
const commandExists = jest.fn();
jest.spyOn(Task, 'log').mockReturnValue(undefined);
jest.spyOn(Task, 'error').mockReturnValue(undefined);
jest.spyOn(Task, 'section').mockReturnValue(undefined);
@@ -41,12 +39,6 @@ jest
.mockImplementation((_a, _b, taskFunc) => taskFunc());
jest.mock('child_process');
jest.mock(
'command-exists',
() =>
(...args: any[]) =>
commandExists(...args),
);
// By mocking this the filesystem mocks won't mess with reading all of the package.jsons
jest.mock('./versions', () => ({
@@ -102,7 +94,10 @@ describe('tasks', () => {
(
command: string,
options: any,
callback: (error: null, stdout: any, stderr: any) => void,
callback: (
error: Error | null,
result: { stdout: string; stderr: string },
) => void,
) => void
>;
@@ -293,29 +288,15 @@ describe('tasks', () => {
it('should return git config if git package is installed and git credentials are set', async () => {
mockExec.mockImplementation((_command, _options, callback) => {
callback(null, { stdout: 'main' }, 'standard error');
callback(null, { stdout: 'main', stderr: '' });
});
commandExists.mockResolvedValue(true);
const gitConfig = await readGitConfig();
expect(gitConfig).toBeTruthy();
expect(gitConfig).toEqual({
name: 'main',
email: 'main',
defaultBranch: 'main',
});
expect(mockExec).toHaveBeenCalledWith(
'git config user.name',
{ cwd: tmpDir },
expect.any(Function),
);
expect(mockExec).toHaveBeenCalledWith(
'git config user.email',
{ cwd: tmpDir },
expect.any(Function),
);
expect(mockExec).toHaveBeenCalledTimes(3);
expect(mockExec).toHaveBeenCalledWith(
'git init',
{ cwd: tmpDir },
@@ -326,58 +307,82 @@ describe('tasks', () => {
{ cwd: tmpDir },
expect.any(Function),
);
expect(mockExec).toHaveBeenCalledWith(
'git branch --format="%(refname:short)"',
{ cwd: tmpDir },
expect.any(Function),
);
});
it('should return false if git package is not installed', async () => {
commandExists.mockResolvedValue(false);
const gitConfig = await readGitConfig();
expect(gitConfig).toEqual({});
});
it('should return false if git package is installed but git credentials are not set', async () => {
it('should return false if git config is invalid', async () => {
mockExec.mockImplementation((_command, _options, callback) => {
callback(null, { stdout: null }, 'standard error');
callback(null, { stdout: '', stderr: '' });
});
commandExists.mockResolvedValue(true);
const gitConfig = await readGitConfig();
expect(gitConfig).toEqual({});
expect(mockExec).toHaveBeenCalledWith(
'git config user.name',
{ cwd: tmpDir },
expect.any(Function),
);
expect(mockExec).toHaveBeenCalledWith(
'git config user.email',
{ cwd: tmpDir },
expect.any(Function),
);
expect(gitConfig).toEqual({
defaultBranch: undefined,
});
expect(mockExec).toHaveBeenCalledTimes(3);
});
});
describe('initGitRepository', () => {
describe('tryInitGitRepository', () => {
it('should initialize a git repository at the given path', async () => {
const destinationDir = 'tmp/mockApp/';
const destinationDir = 'tmp/mockApp';
mockExec.mockImplementation((_command, callback) => {
callback(null, { stdout: 'main' }, 'standard error');
mockExec.mockImplementation((command, _opts, callback) => {
if (command.startsWith('git rev-parse')) {
callback(new Error('not a git repo'), { stdout: '', stderr: '' });
} else {
callback(null, { stdout: '', stderr: '' });
}
});
await initGitRepository(destinationDir);
await expect(tryInitGitRepository(destinationDir)).resolves.toBe(true);
expect(mockExec).toHaveBeenCalledTimes(2);
expect(mockExec).toHaveBeenCalledTimes(4);
expect(mockExec).toHaveBeenNthCalledWith(
1,
'git init',
'git rev-parse --is-inside-work-tree',
{ cwd: destinationDir },
expect.any(Function),
);
expect(mockExec).toHaveBeenNthCalledWith(
2,
'git commit --allow-empty -m "Initial commit"',
'git init',
{ cwd: destinationDir },
expect.any(Function),
);
expect(mockExec).toHaveBeenNthCalledWith(
3,
'git add .',
{ cwd: destinationDir },
expect.any(Function),
);
expect(mockExec).toHaveBeenNthCalledWith(
4,
'git commit -m "Initial commit"',
{ cwd: destinationDir },
expect.any(Function),
);
});
it('should not initialize a git repository if in one already', async () => {
const destinationDir = 'tmp/mockApp';
mockExec.mockImplementation((_command, _opts, callback) => {
callback(null, { stdout: '', stderr: '' });
});
await expect(tryInitGitRepository(destinationDir)).resolves.toBe(false);
expect(mockExec).toHaveBeenCalledTimes(1);
expect(mockExec).toHaveBeenNthCalledWith(
1,
'git rev-parse --is-inside-work-tree',
{ cwd: destinationDir },
expect.any(Function),
);
});
+32 -47
View File
@@ -28,15 +28,12 @@ import {
import { exec as execCb } from 'child_process';
import { packageVersions } from './versions';
import { promisify } from 'util';
import commandExists from 'command-exists';
import os from 'os';
const TASK_NAME_MAX_LENGTH = 14;
const exec = promisify(execCb);
export type GitConfig = {
name?: string;
email?: string;
defaultBranch?: string;
};
@@ -250,71 +247,59 @@ export async function moveAppTask(
*
* @throws if `exec` fails
*/
export async function readGitConfig(): Promise<GitConfig> {
export async function readGitConfig(): Promise<GitConfig | undefined> {
const tempDir = resolvePath(os.tmpdir(), 'git-temp-dir');
const runCmd = (cmd: string) =>
exec(cmd, { cwd: tempDir }).catch(error => {
process.stdout.write(error.stderr);
process.stdout.write(error.stdout);
throw new Error(`Could not execute command ${chalk.cyan(cmd)}`);
});
const isGitAvailable = await commandExists('git').catch(() => false);
if (!isGitAvailable) return {};
try {
await fs.mkdir(tempDir);
const [gitUsername, gitEmail] = await Promise.all([
runCmd('git config user.name'),
runCmd('git config user.email'),
]);
await exec('git init', { cwd: tempDir });
await exec('git commit --allow-empty -m "Initial commit"', {
cwd: tempDir,
});
const gitCredentials = Boolean(
gitUsername.stdout?.trim() && gitEmail.stdout?.trim(),
);
if (!gitCredentials) return {};
await runCmd('git init');
await runCmd('git commit --allow-empty -m "Initial commit"');
const gitDefaultBranch = await runCmd(
const getDefaultBranch = await exec(
'git branch --format="%(refname:short)"',
{ cwd: tempDir },
);
return {
name: gitUsername.stdout?.trim(),
email: gitEmail.stdout?.trim(),
defaultBranch: gitDefaultBranch.stdout?.trim(),
defaultBranch: getDefaultBranch.stdout?.trim() || undefined,
};
} catch (error) {
throw new Error(`Failed to read git config, ${error}`);
return undefined;
} finally {
await fs.rm(tempDir, { recursive: true });
}
}
/**
* Initializes a git repository in the destination folder
* Initializes a git repository in the destination folder if possible
*
* @param dir - source path to initialize git repository in
* @throws if `exec` fails
* @returns true if git repository was initialized
*/
export async function initGitRepository(dir: string) {
const runCmd = (cmd: string) =>
exec(cmd).catch(error => {
process.stdout.write(error.stderr);
process.stdout.write(error.stdout);
throw new Error(`Could not execute command ${chalk.cyan(cmd)}`);
});
export async function tryInitGitRepository(dir: string) {
try {
// Check if we're already in a git repo
await exec('git rev-parse --is-inside-work-tree', { cwd: dir });
return false;
} catch {
/* ignored */
}
await Task.forItem('init', 'git repository', async () => {
process.chdir(dir);
try {
await exec('git init', { cwd: dir });
await exec('git add .', { cwd: dir });
await exec('git commit -m "Initial commit"', { cwd: dir });
return true;
} catch (error) {
try {
await fs.rm(resolvePath(dir, '.git'), { recursive: true, force: true });
} catch {
throw new Error('Failed to remove .git folder');
}
await runCmd('git init');
await runCmd('git commit --allow-empty -m "Initial commit"');
});
return false;
}
}
-1
View File
@@ -3674,7 +3674,6 @@ __metadata:
"@types/node": ^16.11.26
"@types/recursive-readdir": ^2.2.0
chalk: ^4.0.0
command-exists: ^1.2.9
commander: ^9.1.0
fs-extra: 10.1.0
handlebars: ^4.7.3