From 80bfac5266ea31e1c95420921dc0fdf1ff9e70af Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 21 Oct 2022 17:42:49 +0200 Subject: [PATCH] create-app: refactor git init Signed-off-by: Patrik Oldsberg --- .changeset/shiny-beers-relax.md | 5 + packages/create-app/package.json | 1 - packages/create-app/src/createApp.test.ts | 12 +-- packages/create-app/src/createApp.ts | 13 ++- packages/create-app/src/lib/tasks.test.ts | 119 +++++++++++----------- packages/create-app/src/lib/tasks.ts | 79 ++++++-------- yarn.lock | 1 - 7 files changed, 112 insertions(+), 118 deletions(-) create mode 100644 .changeset/shiny-beers-relax.md diff --git a/.changeset/shiny-beers-relax.md b/.changeset/shiny-beers-relax.md new file mode 100644 index 0000000000..be8e9b6fb7 --- /dev/null +++ b/.changeset/shiny-beers-relax.md @@ -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. diff --git a/packages/create-app/package.json b/packages/create-app/package.json index a6a62fbbf0..61384b5d0b 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -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", diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index ec8d15ead1..f4c94d337a 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -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(); }); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 182e043565..6267081c8b 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -28,7 +28,7 @@ import { createTemporaryAppFolderTask, moveAppTask, templatingTask, - initGitRepository, + tryInitGitRepository, readGitConfig, } from './lib/tasks'; @@ -109,13 +109,16 @@ export default async (opts: OptionValues): Promise => { 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); } diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index b918fc0180..8dc351ac97 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -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), ); }); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 1e30f9d33a..ff519912a8 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -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 { +export async function readGitConfig(): Promise { 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; + } } diff --git a/yarn.lock b/yarn.lock index e9f94623ae..71a2ade870 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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