Initializes git repository when creating an app

Signed-off-by: Leonardo Maier <leonarmaier@gmail.com>
This commit is contained in:
Leonardo Maier
2022-09-07 10:55:31 -03:00
parent 51af8361de
commit 7c6306fc8a
6 changed files with 171 additions and 19 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': minor
---
Initializes a git repository when creating an app using @packages/create-app
@@ -32,6 +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 createTemporaryAppFolderMock = jest.spyOn(
tasks,
'createTemporaryAppFolderTask',
@@ -67,6 +68,7 @@ describe('command entrypoint', () => {
await createApp(cmd);
expect(checkAppExistsMock).toHaveBeenCalled();
expect(createTemporaryAppFolderMock).toHaveBeenCalled();
expect(initGitRepositoryMock).toHaveBeenCalled();
expect(templatingMock).toHaveBeenCalled();
expect(moveAppMock).toHaveBeenCalled();
expect(buildAppMock).toHaveBeenCalled();
@@ -76,6 +78,7 @@ describe('command entrypoint', () => {
const cmd = { path: 'myDirectory' } as unknown as Command;
await createApp(cmd);
expect(checkPathExistsMock).toHaveBeenCalled();
expect(initGitRepositoryMock).toHaveBeenCalled();
expect(templatingMock).toHaveBeenCalled();
expect(buildAppMock).toHaveBeenCalled();
});
+7
View File
@@ -28,6 +28,7 @@ import {
createTemporaryAppFolderTask,
moveAppTask,
templatingTask,
initGitRepository,
} from './lib/tasks';
export default async (opts: OptionValues): Promise<void> => {
@@ -79,6 +80,9 @@ export default async (opts: OptionValues): Promise<void> => {
Task.section('Checking that supplied path exists');
await checkPathExistsTask(appDir);
Task.section('Initializing git repository');
await initGitRepository(appDir, answers);
Task.section('Preparing files');
await templatingTask(templateDir, opts.path, answers);
} else {
@@ -90,6 +94,9 @@ export default async (opts: OptionValues): Promise<void> => {
Task.section('Creating a temporary app directory');
await createTemporaryAppFolderTask(tempDir);
Task.section('Initializing git repository');
await initGitRepository(tempDir, answers);
Task.section('Preparing files');
await templatingTask(templateDir, tempDir, answers);
+122 -16
View File
@@ -24,6 +24,7 @@ import {
checkAppExistsTask,
checkPathExistsTask,
createTemporaryAppFolderTask,
initGitRepository,
moveAppTask,
templatingTask,
} from './tasks';
@@ -86,28 +87,100 @@ jest.mock('./versions', () => ({
},
}));
describe('tasks', () => {
beforeEach(() => {
mockFs({
'projects/my-module.ts': '',
'projects/dir/my-file.txt': '',
'tmp/mockApp/.gitignore': '',
'tmp/mockApp/package.json': '',
'tmp/mockApp/packages/app/package.json': '',
// load templates into mock filesystem
'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')),
});
const mockExec = child_process.exec as unknown as jest.MockedFunction<
(
command: string,
callback: (error: null, stdout: any, stderr: any) => void,
) => void
>;
beforeEach(() => {
mockFs({
'projects/my-module.ts': '',
'projects/dir/my-file.txt': '',
'tmp/mockApp/.gitignore': '',
'tmp/mockApp/package.json': '',
'tmp/mockApp/packages/app/package.json': '',
// load templates into mock filesystem
'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')),
});
});
afterEach(() => {
mockExec.mockRestore();
mockFs.restore();
});
describe('checkAppExistsTask', () => {
it('should do nothing if the directory does not exist', async () => {
const dir = 'projects/';
const name = 'MyNewApp';
await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow();
});
afterEach(() => {
mockFs.restore();
});
describe('checkAppExistsTask', () => {
it('should do nothing if the directory does not exist', async () => {
const dir = 'projects/';
const name = 'MyNewApp';
await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow();
it('should throw an error when a directory of the same name exists', async () => {
const dir = 'projects/';
const name = 'dir';
await expect(checkAppExistsTask(dir, name)).rejects.toThrow(
'already exists',
);
});
});
describe('checkPathExistsTask', () => {
it('should create a directory at the given path', async () => {
const appDir = 'projects/newProject';
await expect(checkPathExistsTask(appDir)).resolves.not.toThrow();
expect(fs.existsSync(appDir)).toBe(true);
});
it('should do nothing if a directory of the same name exists', async () => {
const appDir = 'projects/dir';
await expect(checkPathExistsTask(appDir)).resolves.not.toThrow();
expect(fs.existsSync(appDir)).toBe(true);
});
it('should fail if a file of the same name exists', async () => {
await expect(checkPathExistsTask('projects/my-module.ts')).rejects.toThrow(
'already exists',
);
});
});
describe('createTemporaryAppFolderTask', () => {
it('should create a directory at a given path', async () => {
const tempDir = 'projects/tmpFolder';
await expect(createTemporaryAppFolderTask(tempDir)).resolves.not.toThrow();
expect(fs.existsSync(tempDir)).toBe(true);
});
it('should fail if a directory of the same name exists', async () => {
const tempDir = 'projects/dir';
await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow(
'file already exists',
);
});
it('should fail if a file of the same name exists', async () => {
const tempDir = 'projects/dir/my-file.txt';
await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow(
'file already exists',
);
});
});
describe('buildAppTask', () => {
it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => {
const mockChdir = jest.spyOn(process, 'chdir');
// requires callback implementation to support `promisify` wrapper
// https://stackoverflow.com/a/60579617/10044859
mockExec.mockImplementation((_command, callback) => {
callback(null, 'standard out', 'standard error');
});
it('should throw an error when a file of the same name exists', async () => {
@@ -274,3 +347,36 @@ describe('tasks', () => {
});
});
});
describe('initGitRepository', () => {
it('should initialize a git repository at the given path', async () => {
const destinationDir = 'tmp/mockApp/';
const context = {
defaultBranch: '',
};
mockExec.mockImplementation((_command, callback) => {
callback(null, { stdout: 'main' }, 'standard error');
});
await initGitRepository(destinationDir, context);
expect(context.defaultBranch).toBe('main');
expect(mockExec).toHaveBeenCalledTimes(3);
expect(mockExec).toHaveBeenNthCalledWith(
1,
'git init',
expect.any(Function),
);
expect(mockExec).toHaveBeenNthCalledWith(
2,
'git commit --allow-empty -m "Initial commit"',
expect.any(Function),
);
expect(mockExec).toHaveBeenNthCalledWith(
3,
'git branch --format="%(refname:short)"',
expect.any(Function),
);
});
});
+31
View File
@@ -236,3 +236,34 @@ export async function moveAppTask(
});
});
}
/**
* Initializes a git repository in the destination folder
*
* @param dir - source path to initialize git repository in
* @param context - template parameters
* @throws if `exec` fails
*/
export async function initGitRepository(dir: string, context: any) {
const runCmd = async (cmd: string) => {
let cmdResponse = { stdout: '', stderr: '' };
await Task.forItem('executing', cmd, async () => {
process.chdir(dir);
cmdResponse = await exec(cmd).catch(error => {
process.stdout.write(error.stderr);
process.stdout.write(error.stdout);
throw new Error(`Could not execute command ${chalk.cyan(cmd)}`);
});
});
return cmdResponse;
};
await runCmd('git init');
await runCmd('git commit --allow-empty -m "Initial commit"');
const defaultBranch = await runCmd('git branch --format="%(refname:short)"');
context.defaultBranch = defaultBranch.stdout
? defaultBranch.stdout.trim()
: 'master';
}
@@ -14,9 +14,9 @@
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli repo clean",
"test": "backstage-cli repo test",
"test:all": "backstage-cli repo test --coverage",
"lint": "backstage-cli repo lint --since origin/master",
"test": "backstage-cli test",
"test:all": "lerna run test -- --coverage",
"lint": "backstage-cli repo lint --since origin/{{defaultBranch}}",
"lint:all": "backstage-cli repo lint",
"prettier:check": "prettier --check .",
"create-plugin": "backstage-cli create-plugin --scope internal",