create-app: refactor git init
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user