feat(core): move git commit from generators to create-nx-workspace (#11633)

* fix(core): move git commit to create-nx-workspace

* fix(core): add git init to create-nx-plugin
This commit is contained in:
Miroslav Jonaš 2022-08-19 00:40:46 +02:00 committed by GitHub
parent 657b2bff5a
commit 15d83258fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 295 additions and 258 deletions

View File

@ -47,6 +47,26 @@ Choices: [nx, angular]
CLI to power the Nx workspace
### commit.email
Type: string
E-mail of the committer
### commit.message
Type: string
Default: Initial commit
Commit message
### commit.name
Type: string
Name of the committer
### defaultBase
Type: string
@ -95,6 +115,14 @@ Type: string
Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular", "angular-nest", "react", "react-express", "react-native", "next", "nest", "express"]. To build your own see https://nx.dev/packages/nx-plugin#preset
### skipGit
Type: boolean
Default: false
Skip initializing a git repository.
### style
Type: string

View File

@ -10,7 +10,7 @@
"name": "create-nx-workspace",
"id": "create-nx-workspace",
"file": "generated/cli/create-nx-workspace",
"content": "---\ntitle: 'create-nx-workspace - CLI command'\ndescription: 'Create a new Nx workspace'\n---\n\n# create-nx-workspace\n\nCreate a new Nx workspace\n\n## Usage\n\n```bash\ncreate-nx-workspace [name] [options]\n```\n\nInstall `create-nx-workspace` globally to invoke the command directly, or use `npx create-nx-workspace`, `yarn create nx-workspace`, or `pnpx create-nx-workspace`.\n\n## Options\n\n### allPrompts\n\nType: boolean\n\nDefault: false\n\nShow all prompts\n\n### appName\n\nType: string\n\nThe name of the application when a preset with pregenerated app is selected\n\n### ci\n\nType: string\n\nChoices: [github, circleci, azure]\n\nGenerate a CI workflow file\n\n### cli\n\nType: string\n\nChoices: [nx, angular]\n\nCLI to power the Nx workspace\n\n### defaultBase\n\nType: string\n\nDefault: main\n\nDefault base to use for new projects\n\n### help\n\nType: boolean\n\nShow help\n\n### interactive\n\nType: boolean\n\nEnable interactive mode with presets\n\n### name\n\nType: string\n\nWorkspace name (e.g. org name)\n\n### nxCloud\n\nType: boolean\n\nSet up distributed caching using Nx Cloud (It's free and doesn't require registration.)\n\n### packageManager\n\nType: string\n\nChoices: [npm, pnpm, yarn]\n\nDefault: npm\n\nPackage manager to use\n\n### preset\n\nType: string\n\nCustomizes the initial content of your workspace. Default presets include: [\"apps\", \"empty\", \"core\", \"npm\", \"ts\", \"web-components\", \"angular\", \"angular-nest\", \"react\", \"react-express\", \"react-native\", \"next\", \"nest\", \"express\"]. To build your own see https://nx.dev/packages/nx-plugin#preset\n\n### style\n\nType: string\n\nStyle option to be used when a preset with pregenerated app is selected\n\n### version\n\nType: boolean\n\nShow version number\n"
"content": "---\ntitle: 'create-nx-workspace - CLI command'\ndescription: 'Create a new Nx workspace'\n---\n\n# create-nx-workspace\n\nCreate a new Nx workspace\n\n## Usage\n\n```bash\ncreate-nx-workspace [name] [options]\n```\n\nInstall `create-nx-workspace` globally to invoke the command directly, or use `npx create-nx-workspace`, `yarn create nx-workspace`, or `pnpx create-nx-workspace`.\n\n## Options\n\n### allPrompts\n\nType: boolean\n\nDefault: false\n\nShow all prompts\n\n### appName\n\nType: string\n\nThe name of the application when a preset with pregenerated app is selected\n\n### ci\n\nType: string\n\nChoices: [github, circleci, azure]\n\nGenerate a CI workflow file\n\n### cli\n\nType: string\n\nChoices: [nx, angular]\n\nCLI to power the Nx workspace\n\n### commit.email\n\nType: string\n\nE-mail of the committer\n\n### commit.message\n\nType: string\n\nDefault: Initial commit\n\nCommit message\n\n### commit.name\n\nType: string\n\nName of the committer\n\n### defaultBase\n\nType: string\n\nDefault: main\n\nDefault base to use for new projects\n\n### help\n\nType: boolean\n\nShow help\n\n### interactive\n\nType: boolean\n\nEnable interactive mode with presets\n\n### name\n\nType: string\n\nWorkspace name (e.g. org name)\n\n### nxCloud\n\nType: boolean\n\nSet up distributed caching using Nx Cloud (It's free and doesn't require registration.)\n\n### packageManager\n\nType: string\n\nChoices: [npm, pnpm, yarn]\n\nDefault: npm\n\nPackage manager to use\n\n### preset\n\nType: string\n\nCustomizes the initial content of your workspace. Default presets include: [\"apps\", \"empty\", \"core\", \"npm\", \"ts\", \"web-components\", \"angular\", \"angular-nest\", \"react\", \"react-express\", \"react-native\", \"next\", \"nest\", \"express\"]. To build your own see https://nx.dev/packages/nx-plugin#preset\n\n### skipGit\n\nType: boolean\n\nDefault: false\n\nSkip initializing a git repository.\n\n### style\n\nType: string\n\nStyle option to be used when a preset with pregenerated app is selected\n\n### version\n\nType: boolean\n\nShow version number\n"
},
{
"name": "init",

View File

@ -79,28 +79,6 @@
"type": "boolean",
"default": false
},
"skipGit": {
"description": "Skip initializing a git repository.",
"type": "boolean",
"default": false,
"alias": "g"
},
"commit": {
"description": "Initial repository commit information.",
"oneOf": [
{ "type": "boolean" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"message": { "type": "string" }
},
"required": ["name", "email"]
}
],
"default": true
},
"packageManager": {
"description": "The package manager used to install dependencies.",
"type": "string",
@ -342,28 +320,6 @@
"type": "boolean",
"default": false
},
"skipGit": {
"description": "Skip initializing a git repository.",
"type": "boolean",
"default": false,
"alias": "g"
},
"commit": {
"description": "Initial repository commit information.",
"oneOf": [
{ "type": "boolean" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"message": { "type": "string" }
},
"required": ["name", "email"]
}
],
"default": true
},
"preset": {
"description": "What to create in the new workspace.",
"type": "string"

View File

@ -13,6 +13,7 @@ describe('create-nx-plugin', () => {
it('should be able to create a plugin repo and run plugin e2e', () => {
const wsName = uniq('ws-plugin');
const pluginName = uniq('plugin');
runCreatePlugin(wsName, {
packageManager,
pluginName,

View File

@ -12,7 +12,7 @@ import { execSync } from 'child_process';
import { removeSync } from 'fs-extra';
import * as path from 'path';
import { dirSync } from 'tmp';
import { showNxWarning } from './shared';
import { initializeGitRepo, showNxWarning } from './shared';
import {
detectInvokedPackageManager,
PackageManager,
@ -123,17 +123,6 @@ function updateWorkspace(workspaceName: string) {
removeSync(path.join(workspaceName, 'libs'));
}
function commitChanges(workspaceName) {
execSync('git add .', {
cwd: workspaceName,
stdio: 'ignore',
});
execSync('git commit --amend --no-edit', {
cwd: workspaceName,
stdio: 'ignore',
});
}
function determineWorkspaceName(parsedArgs: any): Promise<string> {
const workspaceName: string = parsedArgs._[2];
@ -215,7 +204,8 @@ determineWorkspaceName(parsedArgs).then((workspaceName) => {
createWorkspace(tmpDir, packageManager, parsedArgs, workspaceName);
updateWorkspace(workspaceName);
createNxPlugin(workspaceName, pluginName, packageManager, parsedArgs);
commitChanges(workspaceName);
showNxWarning(workspaceName);
return initializeGitRepo(workspaceName).then(() => {
showNxWarning(workspaceName);
});
});
});

View File

@ -1,5 +1,5 @@
import * as path from 'path';
import { execSync } from 'child_process';
import { execSync, spawn, SpawnOptions } from 'child_process';
import { output } from '@nrwl/devkit';
export function showNxWarning(workspaceName: string) {
@ -21,3 +21,86 @@ export function showNxWarning(workspaceName: string) {
});
}
}
/*
* Because we don't want to depend on @nrwl/workspace
* we duplicate the helper functions from @nrwl/workspace in this file.
*/
export function deduceDefaultBase(): string {
const nxDefaultBase = 'main';
try {
return (
execSync('git config --get init.defaultBranch').toString().trim() ||
nxDefaultBase
);
} catch {
return nxDefaultBase;
}
}
function checkGitVersion(): string | null {
try {
let gitVersionOutput = execSync('git --version').toString().trim();
return gitVersionOutput.match(/[0-9]+\.[0-9]+\.+[0-9]+/)[0];
} catch {
return null;
}
}
/*
* Because we don't want to depend on create-nx-workspace
* we duplicate the helper functions from create-nx-workspace in this file.
*/
export async function initializeGitRepo(directory: string) {
const execute = (args: ReadonlyArray<string>, ignoreErrorStream = false) => {
const errorStream = ignoreErrorStream ? 'ignore' : process.stderr;
const spawnOptions: SpawnOptions = {
stdio: [process.stdin, 'ignore', errorStream],
shell: true,
cwd: directory,
env: process.env,
};
return new Promise<void>((resolve, reject) => {
spawn('git', args, spawnOptions).on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(code);
}
});
});
};
const gitVersion = checkGitVersion();
if (!gitVersion) {
return;
}
const insideRepo = await execute(
['rev-parse', '--is-inside-work-tree'],
true
).then(
() => true,
() => false
);
if (insideRepo) {
output.log({
title:
'Directory is already under version control. Skipping initialization of git.',
});
return;
}
const defaultBase = deduceDefaultBase();
const [gitMajor, gitMinor] = gitVersion.split('.');
if (+gitMajor > 2 || (+gitMajor === 2 && +gitMinor >= 28)) {
await execute(['init', '-b', defaultBase]);
} else {
await execute(['init']);
await execute(['checkout', '-b', defaultBase]); // Git < 2.28 doesn't support -b on git init.
}
await execute(['add', '.']);
const message = 'Initial commit';
await execute(['commit', `-m "${message}"`]);
output.log({
title: 'Successfully initialized git.',
});
}

View File

@ -21,6 +21,8 @@ import { getFileName, stringifyCollection } from './utils';
import { yargsDecorator } from './decorator';
import chalk = require('chalk');
import { ciList } from './ci';
import { join } from 'path';
import { initializeGitRepo } from './git';
type Arguments = {
name: string;
@ -33,6 +35,12 @@ type Arguments = {
packageManager: PackageManager;
defaultBase: string;
ci: string;
skipGit: boolean;
commit: {
message: string;
name: string;
email: string;
};
};
enum Preset {
@ -121,7 +129,7 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
.wrap(yargs.terminalWidth())
.parserConfiguration({
'strip-dashed': true,
'dot-notation': false,
'dot-notation': true,
})
.command(
// this is the default and only command
@ -187,6 +195,25 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
defaultDescription: 'main',
describe: chalk.dim`Default base to use for new projects`,
type: 'string',
})
.option('skipGit', {
describe: chalk.dim`Skip initializing a git repository.`,
type: 'boolean',
default: false,
alias: 'g',
})
.option('commit.name', {
describe: chalk.dim`Name of the committer`,
type: 'string',
})
.option('commit.email', {
describe: chalk.dim`E-mail of the committer`,
type: 'string',
})
.option('commit.message', {
describe: chalk.dim`Commit message`,
type: 'string',
default: 'Initial commit',
}),
async (argv: yargs.ArgumentsCamelCase<Arguments>) => {
await main(argv).catch((error) => {
@ -218,6 +245,8 @@ async function main(parsedArgs: yargs.Arguments<Arguments>) {
packageManager,
defaultBase,
ci,
skipGit,
commit,
} = parsedArgs;
output.log({
@ -230,15 +259,20 @@ async function main(parsedArgs: yargs.Arguments<Arguments>) {
const tmpDir = await createSandbox(packageManager);
await createApp(tmpDir, name, packageManager as PackageManager, {
...parsedArgs,
cli,
preset,
appName,
style,
nxCloud,
defaultBase,
});
const directory = await createApp(
tmpDir,
name,
packageManager as PackageManager,
{
...parsedArgs,
cli,
preset,
appName,
style,
nxCloud,
defaultBase,
}
);
let nxCloudInstallRes;
if (nxCloud) {
@ -255,6 +289,16 @@ async function main(parsedArgs: yargs.Arguments<Arguments>) {
nxCloud && nxCloudInstallRes.code === 0
);
}
if (!skipGit) {
try {
await initializeGitRepo(directory, { defaultBase, commit });
} catch (e) {
output.error({
title: 'Could not initialize git repository',
bodyLines: [e.message],
});
}
}
showNxWarning(name);
pointToTutorialAndCourse(preset as Preset);
@ -745,7 +789,7 @@ async function createApp(
name: string,
packageManager: PackageManager,
parsedArgs: any
) {
): Promise<string> {
const { _, cli, ...restArgs } = parsedArgs;
// Ensure to use packageManager for args
@ -760,7 +804,8 @@ async function createApp(
const command = `new ${name} ${args} --collection=@nrwl/workspace/generators.json --cli=${cli}`;
let nxWorkspaceRoot = `"${process.cwd().replace(/\\/g, '/')}"`;
const workingDir = process.cwd().replace(/\\/g, '/');
let nxWorkspaceRoot = `"${workingDir}"`;
// If path contains spaces there is a problem in Windows for npm@6.
// In this case we have to escape the wrapping quotes.
@ -791,6 +836,7 @@ async function createApp(
} finally {
workspaceSetupSpinner.stop();
}
return join(workingDir, name);
}
async function setupNxCloud(name: string, packageManager: PackageManager) {

View File

@ -0,0 +1,28 @@
import { checkGitVersion } from './git';
import * as cp from 'child_process';
describe('checkGitVersion', () => {
const execSyncSpy = jest.spyOn(cp, 'execSync');
afterEach(() => {
jest.resetAllMocks();
});
it('should work with text before semver', () => {
execSyncSpy.mockReturnValue(Buffer.from(`git version 2.33.0`));
const result = checkGitVersion();
expect(result).toEqual('2.33.0');
});
it('should work with 4 digit versions', () => {
execSyncSpy.mockReturnValue(Buffer.from(`git version 2.33.0.5`));
const result = checkGitVersion();
expect(result).toEqual('2.33.0');
});
it('should work with msysgit versions', () => {
execSyncSpy.mockReturnValue(Buffer.from(`git version 1.8.3.msysgit.0`));
const result = checkGitVersion();
expect(result).toEqual('1.8.3');
});
});

View File

@ -0,0 +1,89 @@
import { execSync, spawn, SpawnOptions } from 'child_process';
import { deduceDefaultBase } from './default-base';
import { output } from './output';
export function checkGitVersion(): string | null {
try {
let gitVersionOutput = execSync('git --version').toString().trim();
return gitVersionOutput.match(/[0-9]+\.[0-9]+\.+[0-9]+/)[0];
} catch {
return null;
}
}
export async function initializeGitRepo(
directory: string,
options: {
defaultBase: string;
commit: { message: string; name: string; email: string };
}
) {
const execute = (args: ReadonlyArray<string>, ignoreErrorStream = false) => {
const outputStream = 'ignore';
const errorStream = ignoreErrorStream ? 'ignore' : process.stderr;
const spawnOptions: SpawnOptions = {
stdio: [process.stdin, outputStream, errorStream],
shell: true,
cwd: directory,
env: {
...process.env,
...(options.commit.name
? {
GIT_AUTHOR_NAME: options.commit.name,
GIT_COMMITTER_NAME: options.commit.name,
}
: {}),
...(options.commit.email
? {
GIT_AUTHOR_EMAIL: options.commit.email,
GIT_COMMITTER_EMAIL: options.commit.email,
}
: {}),
},
};
return new Promise<void>((resolve, reject) => {
spawn('git', args, spawnOptions).on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(code);
}
});
});
};
const gitVersion = checkGitVersion();
if (!gitVersion) {
return;
}
const insideRepo = await execute(
['rev-parse', '--is-inside-work-tree'],
true
).then(
() => true,
() => false
);
if (insideRepo) {
output.log({
title:
'Directory is already under version control. Skipping initialization of git.',
});
return;
}
const defaultBase = options.defaultBase || deduceDefaultBase();
const [gitMajor, gitMinor] = gitVersion.split('.');
if (+gitMajor > 2 || (+gitMajor === 2 && +gitMinor >= 28)) {
await execute(['init', '-b', defaultBase]);
} else {
await execute(['init']);
await execute(['checkout', '-b', defaultBase]); // Git < 2.28 doesn't support -b on git init.
}
await execute(['add', '.']);
if (options.commit) {
const message = options.commit.message || 'initial commit';
await execute(['commit', `-m "${message}"`]);
}
output.log({
title: 'Successfully initialized git.',
});
}

View File

@ -8,7 +8,6 @@ const defaultOptions: Omit<Schema, 'name' | 'directory' | 'appName'> = {
cli: 'nx',
preset: Preset.Apps,
skipInstall: false,
skipGit: false,
linter: Linter.EsLint,
defaultBase: 'main',
};

View File

@ -13,17 +13,10 @@ import {
import { join } from 'path';
import * as yargsParser from 'yargs-parser';
import { spawn, SpawnOptions } from 'child_process';
import { gte } from 'semver';
import { spawn } from 'child_process';
import { workspaceGenerator } from '../workspace/workspace';
import { nxVersion } from '../../utils/versions';
import { Preset } from '../utils/presets';
import {
checkGitVersion,
deduceDefaultBase,
} from '../../utilities/default-base';
import { getNpmPackageVersion } from '../utils/get-npm-package-version';
export interface Schema {
@ -33,11 +26,9 @@ export interface Schema {
appName: string;
npmScope?: string;
skipInstall?: boolean;
skipGit?: boolean;
style?: string;
nxCloud?: boolean;
preset: string;
commit?: { name: string; email: string; message?: string };
defaultBase: string;
linter: 'tslint' | 'eslint';
packageManager?: PackageManager;
@ -116,76 +107,6 @@ function generatePreset(host: Tree, opts: NormalizedSchema) {
}
}
async function initializeGitRepo(
host: Tree,
rootDirectory: string,
options: Schema
) {
const execute = (args: ReadonlyArray<string>, ignoreErrorStream = false) => {
const outputStream = 'ignore';
const errorStream = ignoreErrorStream ? 'ignore' : process.stderr;
const spawnOptions: SpawnOptions = {
stdio: [process.stdin, outputStream, errorStream],
shell: true,
cwd: join(host.root, rootDirectory),
env: {
...process.env,
...(options.commit.name
? {
GIT_AUTHOR_NAME: options.commit.name,
GIT_COMMITTER_NAME: options.commit.name,
}
: {}),
...(options.commit.email
? {
GIT_AUTHOR_EMAIL: options.commit.email,
GIT_COMMITTER_EMAIL: options.commit.email,
}
: {}),
},
};
return new Promise<void>((resolve, reject) => {
spawn('git', args, spawnOptions).on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(code);
}
});
});
};
const gitVersion = checkGitVersion();
if (!gitVersion) {
return;
}
const insideRepo = await execute(
['rev-parse', '--is-inside-work-tree'],
true
).then(
() => true,
() => false
);
if (insideRepo) {
console.info(
`Directory is already under version control. Skipping initialization of git.`
);
return;
}
const defaultBase = options.defaultBase || deduceDefaultBase();
if (gte(gitVersion, '2.28.0')) {
await execute(['init', '-b', defaultBase]);
} else {
await execute(['init']);
await execute(['checkout', '-b', defaultBase]); // Git < 2.28 doesn't support -b on git init.
}
await execute(['add', '.']);
if (options.commit) {
const message = options.commit.message || 'initial commit';
await execute(['commit', `-m "${message}"`]);
}
console.info('Successfully initialized git.');
}
export async function newGenerator(host: Tree, options: Schema) {
if (
options.skipInstall &&
@ -229,15 +150,6 @@ export async function newGenerator(host: Tree, options: Schema) {
return async () => {
installPackagesTask(host, false, options.directory, options.packageManager);
await generatePreset(host, options);
if (!options.skipGit) {
try {
await initializeGitRepo(host, options.directory, options);
} catch (e) {
console.error(
`Could not initialize git repository. Error: ${e.message}`
);
}
}
};
}

View File

@ -39,37 +39,6 @@
"type": "boolean",
"default": false
},
"skipGit": {
"description": "Skip initializing a git repository.",
"type": "boolean",
"default": false,
"alias": "g"
},
"commit": {
"description": "Initial repository commit information.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
},
"message": {
"type": "string"
}
},
"required": ["name", "email"]
}
],
"default": true
},
"preset": {
"description": "What to create in the new workspace.",
"type": "string"

View File

@ -62,35 +62,6 @@
"type": "boolean",
"default": false
},
"skipGit": {
"description": "Skip initializing a git repository.",
"type": "boolean",
"default": false,
"alias": "g"
},
"commit": {
"description": "Initial repository commit information.",
"oneOf": [
{ "type": "boolean" },
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
},
"message": {
"type": "string"
}
},
"required": ["name", "email"]
}
],
"default": true
},
"packageManager": {
"description": "The package manager used to install dependencies.",
"type": "string",

View File

@ -1,5 +1,5 @@
import * as cp from 'child_process';
import { checkGitVersion, deduceDefaultBase } from './default-base';
import { deduceDefaultBase } from './default-base';
describe('deduceDefaultBase', () => {
const execSyncSpy = jest.spyOn(cp, 'execSync');
@ -29,29 +29,3 @@ describe('deduceDefaultBase', () => {
expect(result).toEqual('some-other-default-branch');
});
});
describe('checkGitVersion', () => {
const execSyncSpy = jest.spyOn(cp, 'execSync');
afterEach(() => {
jest.resetAllMocks();
});
it('should work with text before semver', () => {
execSyncSpy.mockReturnValue(Buffer.from(`git version 2.33.0`));
const result = checkGitVersion();
expect(result).toEqual('2.33.0');
});
it('should work with 4 digit versions', () => {
execSyncSpy.mockReturnValue(Buffer.from(`git version 2.33.0.5`));
const result = checkGitVersion();
expect(result).toEqual('2.33.0');
});
it('should work with msysgit versions', () => {
execSyncSpy.mockReturnValue(Buffer.from(`git version 1.8.3.msysgit.0`));
const result = checkGitVersion();
expect(result).toEqual('1.8.3');
});
});

View File

@ -11,12 +11,3 @@ export function deduceDefaultBase(): string {
return nxDefaultBase;
}
}
export function checkGitVersion(): string | null {
try {
let gitVersionOutput = execSync('git --version').toString().trim();
return gitVersionOutput.match(/[0-9]+\.[0-9]+\.+[0-9]+/)[0];
} catch {
return null;
}
}