feat(core): introduce nx import (#26847)
## Current Behavior <!-- This is the behavior we have today --> Importing other projects/ repositories into an Nx workspace is a natural part of the Nx adoption story. However, there is no easy built in way of handling this while maintaining `git` history. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> `nx import` is a new command which allows teams to merge code from other repositories into a Nx workspace. https://asciinema.org/a/oQiA9qOvA2z85AQvVJ5QRVTp1 ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
903c4607d9
commit
c72ba9b504
89
e2e/nx/src/import.test.ts
Normal file
89
e2e/nx/src/import.test.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
checkFilesExist,
|
||||||
|
cleanupProject,
|
||||||
|
getSelectedPackageManager,
|
||||||
|
newProject,
|
||||||
|
runCLI,
|
||||||
|
updateJson,
|
||||||
|
updateFile,
|
||||||
|
e2eCwd,
|
||||||
|
} from '@nx/e2e/utils';
|
||||||
|
import { mkdirSync, rmdirSync } from 'fs';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
describe('Nx Import', () => {
|
||||||
|
let proj: string;
|
||||||
|
const tempImportE2ERoot = join(e2eCwd, 'nx-import');
|
||||||
|
beforeAll(() => {
|
||||||
|
proj = newProject({
|
||||||
|
packages: ['@nx/js'],
|
||||||
|
unsetProjectNameAndRootFormat: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getSelectedPackageManager() === 'pnpm') {
|
||||||
|
updateFile(
|
||||||
|
'pnpm-workspace.yaml',
|
||||||
|
`packages:
|
||||||
|
- 'projects/*'
|
||||||
|
`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updateJson('package.json', (json) => {
|
||||||
|
json.workspaces = ['projects/*'];
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rmdirSync(join(tempImportE2ERoot));
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
afterAll(() => cleanupProject());
|
||||||
|
|
||||||
|
it('should be able to import a vite app', () => {
|
||||||
|
mkdirSync(join(tempImportE2ERoot), { recursive: true });
|
||||||
|
const tempViteProjectName = 'created-vite-app';
|
||||||
|
execSync(
|
||||||
|
`npx create-vite@latest ${tempViteProjectName} --template react-ts`,
|
||||||
|
{
|
||||||
|
cwd: tempImportE2ERoot,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const tempViteProjectPath = join(tempImportE2ERoot, tempViteProjectName);
|
||||||
|
execSync(`git init`, {
|
||||||
|
cwd: tempViteProjectPath,
|
||||||
|
});
|
||||||
|
execSync(`git add .`, {
|
||||||
|
cwd: tempViteProjectPath,
|
||||||
|
});
|
||||||
|
execSync(`git commit -am "initial commit"`, {
|
||||||
|
cwd: tempViteProjectPath,
|
||||||
|
});
|
||||||
|
execSync(`git checkout -b main`, {
|
||||||
|
cwd: tempViteProjectPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const remote = tempViteProjectPath;
|
||||||
|
const ref = 'main';
|
||||||
|
const source = '.';
|
||||||
|
const directory = 'projects/vite-app';
|
||||||
|
|
||||||
|
runCLI(
|
||||||
|
`import ${remote} ${directory} --ref ${ref} --source ${source} --no-interactive`,
|
||||||
|
{
|
||||||
|
verbose: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
checkFilesExist(
|
||||||
|
'projects/vite-app/.gitignore',
|
||||||
|
'projects/vite-app/package.json',
|
||||||
|
'projects/vite-app/index.html',
|
||||||
|
'projects/vite-app/vite.config.ts',
|
||||||
|
'projects/vite-app/src/main.tsx',
|
||||||
|
'projects/vite-app/src/App.tsx'
|
||||||
|
);
|
||||||
|
runCLI(`vite:build created-vite-app`);
|
||||||
|
});
|
||||||
|
});
|
||||||
48
packages/nx/src/command-line/import/command-object.ts
Normal file
48
packages/nx/src/command-line/import/command-object.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { CommandModule } from 'yargs';
|
||||||
|
import { linkToNxDevAndExamples } from '../yargs-utils/documentation';
|
||||||
|
import { withVerbose } from '../yargs-utils/shared-options';
|
||||||
|
import { handleErrors } from '../../utils/params';
|
||||||
|
|
||||||
|
export const yargsImportCommand: CommandModule = {
|
||||||
|
command: 'import [sourceRemoteUrl] [destination]',
|
||||||
|
describe: false,
|
||||||
|
builder: (yargs) =>
|
||||||
|
linkToNxDevAndExamples(
|
||||||
|
withVerbose(
|
||||||
|
yargs
|
||||||
|
.positional('sourceRemoteUrl', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The remote URL of the source to import',
|
||||||
|
})
|
||||||
|
.positional('destination', {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'The directory in the current workspace to import into',
|
||||||
|
})
|
||||||
|
.option('source', {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'The directory in the source repository to import from',
|
||||||
|
})
|
||||||
|
.option('ref', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The branch from the source repository to import',
|
||||||
|
})
|
||||||
|
.option('interactive', {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Interactive mode',
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
'import'
|
||||||
|
),
|
||||||
|
handler: async (args) => {
|
||||||
|
const exitCode = await handleErrors(
|
||||||
|
(args.verbose as boolean) ?? process.env.NX_VERBOSE_LOGGING === 'true',
|
||||||
|
async () => {
|
||||||
|
return (await import('./import')).importHandler(args as any);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
process.exit(exitCode);
|
||||||
|
},
|
||||||
|
};
|
||||||
276
packages/nx/src/command-line/import/import.ts
Normal file
276
packages/nx/src/command-line/import/import.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import { join, relative, resolve } from 'path';
|
||||||
|
import { cloneFromUpstream, GitRepository } from '../../utils/git-utils';
|
||||||
|
import { stat, mkdir, rm } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'tmp';
|
||||||
|
import { prompt } from 'enquirer';
|
||||||
|
import { output } from '../../utils/output';
|
||||||
|
import * as createSpinner from 'ora';
|
||||||
|
import { detectPlugins, installPlugins } from '../init/init-v2';
|
||||||
|
import { readNxJson } from '../../config/nx-json';
|
||||||
|
import { workspaceRoot } from '../../utils/workspace-root';
|
||||||
|
import {
|
||||||
|
detectPackageManager,
|
||||||
|
getPackageManagerCommand,
|
||||||
|
} from '../../utils/package-manager';
|
||||||
|
import { resetWorkspaceContext } from '../../utils/workspace-context';
|
||||||
|
import { runInstall } from '../init/implementation/utils';
|
||||||
|
import { getBaseRef } from '../../utils/command-line-utils';
|
||||||
|
import { prepareSourceRepo } from './utils/prepare-source-repo';
|
||||||
|
import { mergeRemoteSource } from './utils/merge-remote-source';
|
||||||
|
import {
|
||||||
|
getPackagesInPackageManagerWorkspace,
|
||||||
|
needsInstall,
|
||||||
|
} from './utils/needs-install';
|
||||||
|
|
||||||
|
const importRemoteName = '__tmp_nx_import__';
|
||||||
|
|
||||||
|
export interface ImportOptions {
|
||||||
|
/**
|
||||||
|
* The remote URL of the repository to import
|
||||||
|
*/
|
||||||
|
sourceRemoteUrl: string;
|
||||||
|
/**
|
||||||
|
* The branch or reference to import
|
||||||
|
*/
|
||||||
|
ref: string;
|
||||||
|
/**
|
||||||
|
* The directory in the source repo to import
|
||||||
|
*/
|
||||||
|
source: string;
|
||||||
|
/**
|
||||||
|
* The directory in the destination repo to import into
|
||||||
|
*/
|
||||||
|
destination: string;
|
||||||
|
|
||||||
|
verbose: boolean;
|
||||||
|
interactive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importHandler(options: ImportOptions) {
|
||||||
|
let { sourceRemoteUrl, ref, source, destination } = options;
|
||||||
|
|
||||||
|
output.log({
|
||||||
|
title:
|
||||||
|
'Nx will walk you through the process of importing code from another repository into this workspace:',
|
||||||
|
bodyLines: [
|
||||||
|
`1. Nx will clone the other repository into a temporary directory`,
|
||||||
|
`2. Code to be imported will be moved to the same directory it will be imported into on a temporary branch`,
|
||||||
|
`3. The code will be merged into the current branch in this workspace`,
|
||||||
|
`4. Nx will recommend plugins to integrate tools used in the imported code with Nx`,
|
||||||
|
`5. The code will be successfully imported into this workspace`,
|
||||||
|
'',
|
||||||
|
`Git history will be preserved during this process`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const tempImportDirectory = join(tmpdir, 'nx-import');
|
||||||
|
|
||||||
|
if (!sourceRemoteUrl) {
|
||||||
|
sourceRemoteUrl = (
|
||||||
|
await prompt<{ sourceRemoteUrl: string }>([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'sourceRemoteUrl',
|
||||||
|
message:
|
||||||
|
'What is the URL of the repository you want to import? (This can be a local git repository or a git remote URL)',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).sourceRemoteUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const maybeLocalDirectory = await stat(sourceRemoteUrl);
|
||||||
|
if (maybeLocalDirectory.isDirectory()) {
|
||||||
|
sourceRemoteUrl = resolve(sourceRemoteUrl);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// It's a remote url
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceRepoPath = join(tempImportDirectory, 'repo');
|
||||||
|
const spinner = createSpinner(
|
||||||
|
`Cloning ${sourceRemoteUrl} into a temporary directory: ${sourceRepoPath}`
|
||||||
|
).start();
|
||||||
|
try {
|
||||||
|
await rm(tempImportDirectory, { recursive: true });
|
||||||
|
} catch {}
|
||||||
|
await mkdir(tempImportDirectory, { recursive: true });
|
||||||
|
|
||||||
|
let sourceGitClient: GitRepository;
|
||||||
|
try {
|
||||||
|
sourceGitClient = await cloneFromUpstream(sourceRemoteUrl, sourceRepoPath, {
|
||||||
|
originName: importRemoteName,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
spinner.fail(`Failed to clone ${sourceRemoteUrl} into ${sourceRepoPath}`);
|
||||||
|
let errorMessage = `Failed to clone ${sourceRemoteUrl} into ${sourceRepoPath}. Please double check the remote and try again.\n${e.message}`;
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
spinner.succeed(`Cloned into ${sourceRepoPath}`);
|
||||||
|
|
||||||
|
if (!ref) {
|
||||||
|
const branchChoices = await sourceGitClient.listBranches();
|
||||||
|
ref = (
|
||||||
|
await prompt<{ ref: string }>([
|
||||||
|
{
|
||||||
|
type: 'autocomplete',
|
||||||
|
name: 'ref',
|
||||||
|
message: `Which branch do you want to import?`,
|
||||||
|
choices: branchChoices,
|
||||||
|
/**
|
||||||
|
* Limit the number of choices so that it fits on screen
|
||||||
|
*/
|
||||||
|
limit: process.stdout.rows - 3,
|
||||||
|
required: true,
|
||||||
|
} as any,
|
||||||
|
])
|
||||||
|
).ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
source = (
|
||||||
|
await prompt<{ source: string }>([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'source',
|
||||||
|
message: `Which directory do you want to import into this workspace? (leave blank to import the entire repository)`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).source;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!destination) {
|
||||||
|
destination = (
|
||||||
|
await prompt<{ destination: string }>([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'destination',
|
||||||
|
message: 'Where in this workspace should the code be imported into?',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absSource = join(sourceRepoPath, source);
|
||||||
|
const absDestination = join(process.cwd(), destination);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stat(absSource);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`The source directory ${source} does not exist in ${sourceRemoteUrl}. Please double check to make sure it exists.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const destinationGitClient = new GitRepository(process.cwd());
|
||||||
|
await assertDestinationEmpty(destinationGitClient, absDestination);
|
||||||
|
|
||||||
|
const tempImportBranch = getTempImportBranch(ref);
|
||||||
|
|
||||||
|
const packageManager = detectPackageManager(workspaceRoot);
|
||||||
|
|
||||||
|
const originalPackageWorkspaces = await getPackagesInPackageManagerWorkspace(
|
||||||
|
packageManager
|
||||||
|
);
|
||||||
|
|
||||||
|
const relativeDestination = relative(
|
||||||
|
destinationGitClient.root,
|
||||||
|
absDestination
|
||||||
|
);
|
||||||
|
await prepareSourceRepo(
|
||||||
|
sourceGitClient,
|
||||||
|
ref,
|
||||||
|
source,
|
||||||
|
relativeDestination,
|
||||||
|
tempImportBranch,
|
||||||
|
sourceRemoteUrl,
|
||||||
|
importRemoteName
|
||||||
|
);
|
||||||
|
|
||||||
|
await createTemporaryRemote(
|
||||||
|
destinationGitClient,
|
||||||
|
join(sourceRepoPath, '.git'),
|
||||||
|
importRemoteName
|
||||||
|
);
|
||||||
|
|
||||||
|
await mergeRemoteSource(
|
||||||
|
destinationGitClient,
|
||||||
|
sourceRemoteUrl,
|
||||||
|
tempImportBranch,
|
||||||
|
destination,
|
||||||
|
importRemoteName,
|
||||||
|
ref
|
||||||
|
);
|
||||||
|
|
||||||
|
spinner.start('Cleaning up temporary files and remotes');
|
||||||
|
await rm(tempImportDirectory, { recursive: true });
|
||||||
|
await destinationGitClient.deleteGitRemote(importRemoteName);
|
||||||
|
spinner.succeed('Cleaned up temporary files and remotes');
|
||||||
|
|
||||||
|
const pmc = getPackageManagerCommand();
|
||||||
|
const nxJson = readNxJson(workspaceRoot);
|
||||||
|
|
||||||
|
resetWorkspaceContext();
|
||||||
|
|
||||||
|
const { plugins, updatePackageScripts } = await detectPlugins(
|
||||||
|
nxJson,
|
||||||
|
options.interactive
|
||||||
|
);
|
||||||
|
|
||||||
|
if (plugins.length > 0) {
|
||||||
|
output.log({ title: 'Installing Plugins' });
|
||||||
|
installPlugins(workspaceRoot, plugins, pmc, updatePackageScripts);
|
||||||
|
|
||||||
|
await destinationGitClient.amendCommit();
|
||||||
|
} else if (await needsInstall(packageManager, originalPackageWorkspaces)) {
|
||||||
|
output.log({
|
||||||
|
title: 'Installing dependencies for imported code',
|
||||||
|
});
|
||||||
|
|
||||||
|
runInstall(workspaceRoot, getPackageManagerCommand(packageManager));
|
||||||
|
|
||||||
|
await destinationGitClient.amendCommit();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(await destinationGitClient.showStat());
|
||||||
|
|
||||||
|
output.log({
|
||||||
|
title: `Merging these changes into ${getBaseRef(nxJson)}`,
|
||||||
|
bodyLines: [
|
||||||
|
`MERGE these changes when merging these changes.`,
|
||||||
|
`Do NOT squash and do NOT rebase these changes when merging these changes.`,
|
||||||
|
`If you would like to UNDO these changes, run "git reset HEAD~1 --hard"`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertDestinationEmpty(
|
||||||
|
gitClient: GitRepository,
|
||||||
|
absDestination: string
|
||||||
|
) {
|
||||||
|
const files = await gitClient.getGitFiles(absDestination);
|
||||||
|
if (files.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Destination directory ${absDestination} is not empty. Please make sure it is empty before importing into it.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTempImportBranch(sourceBranch: string) {
|
||||||
|
return `__nx_tmp_import__/${sourceBranch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTemporaryRemote(
|
||||||
|
destinationGitClient: GitRepository,
|
||||||
|
sourceRemoteUrl: string,
|
||||||
|
remoteName: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await destinationGitClient.deleteGitRemote(remoteName);
|
||||||
|
} catch {}
|
||||||
|
await destinationGitClient.addGitRemote(remoteName, sourceRemoteUrl);
|
||||||
|
await destinationGitClient.fetch(remoteName);
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { GitRepository } from '../../../utils/git-utils';
|
||||||
|
import * as createSpinner from 'ora';
|
||||||
|
|
||||||
|
export async function mergeRemoteSource(
|
||||||
|
destinationGitClient: GitRepository,
|
||||||
|
sourceRemoteUrl: string,
|
||||||
|
tempBranch: string,
|
||||||
|
destination: string,
|
||||||
|
remoteName: string,
|
||||||
|
branchName: string
|
||||||
|
) {
|
||||||
|
const spinner = createSpinner();
|
||||||
|
spinner.start(
|
||||||
|
`Merging ${branchName} from ${sourceRemoteUrl} into ${destination}`
|
||||||
|
);
|
||||||
|
|
||||||
|
spinner.start(`Fetching ${tempBranch} from ${remoteName}`);
|
||||||
|
await destinationGitClient.fetch(remoteName, tempBranch);
|
||||||
|
spinner.succeed(`Fetched ${tempBranch} from ${remoteName}`);
|
||||||
|
|
||||||
|
spinner.start(
|
||||||
|
`Merging files and git history from ${branchName} from ${sourceRemoteUrl} into ${destination}`
|
||||||
|
);
|
||||||
|
await destinationGitClient.mergeUnrelatedHistories(
|
||||||
|
`${remoteName}/${tempBranch}`,
|
||||||
|
`feat(repo): merge ${branchName} from ${sourceRemoteUrl}`
|
||||||
|
);
|
||||||
|
|
||||||
|
spinner.succeed(
|
||||||
|
`Merged files and git history from ${branchName} from ${sourceRemoteUrl} into ${destination}`
|
||||||
|
);
|
||||||
|
}
|
||||||
44
packages/nx/src/command-line/import/utils/needs-install.ts
Normal file
44
packages/nx/src/command-line/import/utils/needs-install.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
isWorkspacesEnabled,
|
||||||
|
PackageManager,
|
||||||
|
} from '../../../utils/package-manager';
|
||||||
|
import { workspaceRoot } from '../../../utils/workspace-root';
|
||||||
|
import { getGlobPatternsFromPackageManagerWorkspaces } from '../../../plugins/package-json';
|
||||||
|
import { globWithWorkspaceContext } from '../../../utils/workspace-context';
|
||||||
|
|
||||||
|
export async function getPackagesInPackageManagerWorkspace(
|
||||||
|
packageManager: PackageManager
|
||||||
|
) {
|
||||||
|
if (!isWorkspacesEnabled(packageManager, workspaceRoot)) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
const patterns = getGlobPatternsFromPackageManagerWorkspaces(workspaceRoot);
|
||||||
|
return new Set(await globWithWorkspaceContext(workspaceRoot, patterns));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function needsInstall(
|
||||||
|
packageManager: PackageManager,
|
||||||
|
originalPackagesInPackageManagerWorkspaces: Set<string>
|
||||||
|
) {
|
||||||
|
if (!isWorkspacesEnabled(packageManager, workspaceRoot)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPackagesInPackageManagerWorkspaces =
|
||||||
|
await getPackagesInPackageManagerWorkspace(packageManager);
|
||||||
|
|
||||||
|
if (
|
||||||
|
updatedPackagesInPackageManagerWorkspaces.size !==
|
||||||
|
originalPackagesInPackageManagerWorkspaces.size
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pkg of updatedPackagesInPackageManagerWorkspaces) {
|
||||||
|
if (!originalPackagesInPackageManagerWorkspaces.has(pkg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
133
packages/nx/src/command-line/import/utils/prepare-source-repo.ts
Normal file
133
packages/nx/src/command-line/import/utils/prepare-source-repo.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import * as createSpinner from 'ora';
|
||||||
|
import { basename, dirname, join, relative } from 'path';
|
||||||
|
import { copyFile, mkdir, rm } from 'node:fs/promises';
|
||||||
|
import { GitRepository } from '../../../utils/git-utils';
|
||||||
|
|
||||||
|
export async function prepareSourceRepo(
|
||||||
|
gitClient: GitRepository,
|
||||||
|
ref: string,
|
||||||
|
source: string,
|
||||||
|
relativeDestination: string,
|
||||||
|
tempImportBranch: string,
|
||||||
|
sourceRemoteUrl: string,
|
||||||
|
originName: string
|
||||||
|
) {
|
||||||
|
const spinner = createSpinner().start(
|
||||||
|
`Fetching ${ref} from ${sourceRemoteUrl}`
|
||||||
|
);
|
||||||
|
await gitClient.addFetchRemote(originName, ref);
|
||||||
|
await gitClient.fetch(originName, ref);
|
||||||
|
spinner.succeed(`Fetched ${ref} from ${sourceRemoteUrl}`);
|
||||||
|
spinner.start(
|
||||||
|
`Checking out a temporary branch, ${tempImportBranch} based on ${ref}`
|
||||||
|
);
|
||||||
|
await gitClient.checkout(tempImportBranch, {
|
||||||
|
new: true,
|
||||||
|
base: `${originName}/${ref}`,
|
||||||
|
});
|
||||||
|
spinner.succeed(`Created a ${tempImportBranch} branch based on ${ref}`);
|
||||||
|
const relativeSourceDir = relative(
|
||||||
|
gitClient.root,
|
||||||
|
join(gitClient.root, source)
|
||||||
|
);
|
||||||
|
|
||||||
|
const destinationInSource = join(gitClient.root, relativeDestination);
|
||||||
|
spinner.start(`Moving files and git history to ${destinationInSource}`);
|
||||||
|
if (relativeSourceDir === '') {
|
||||||
|
const files = await gitClient.getGitFiles('.');
|
||||||
|
try {
|
||||||
|
await rm(destinationInSource, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
await mkdir(destinationInSource, { recursive: true });
|
||||||
|
const gitignores = new Set<string>();
|
||||||
|
for (const file of files) {
|
||||||
|
if (basename(file) === '.gitignore') {
|
||||||
|
gitignores.add(file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
spinner.start(
|
||||||
|
`Moving files and git history to ${destinationInSource}: ${file}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const newPath = join(destinationInSource, file);
|
||||||
|
|
||||||
|
await mkdir(dirname(newPath), { recursive: true });
|
||||||
|
try {
|
||||||
|
await gitClient.move(file, newPath);
|
||||||
|
} catch {
|
||||||
|
await wait(100);
|
||||||
|
await gitClient.move(file, newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await gitClient.commit(
|
||||||
|
`chore(repo): move ${source} to ${relativeDestination} to prepare to be imported`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const gitignore of gitignores) {
|
||||||
|
await gitClient.move(gitignore, join(destinationInSource, gitignore));
|
||||||
|
}
|
||||||
|
await gitClient.amendCommit();
|
||||||
|
for (const gitignore of gitignores) {
|
||||||
|
await copyFile(
|
||||||
|
join(destinationInSource, gitignore),
|
||||||
|
join(gitClient.root, gitignore)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let needsSquash = false;
|
||||||
|
const needsMove = destinationInSource !== join(gitClient.root, source);
|
||||||
|
if (needsMove) {
|
||||||
|
try {
|
||||||
|
await rm(destinationInSource, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
await gitClient.commit('chore(repo): prepare for import');
|
||||||
|
needsSquash = true;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
await mkdir(destinationInSource, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await gitClient.getGitFiles('.');
|
||||||
|
for (const file of files) {
|
||||||
|
if (file === '.gitignore') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
spinner.start(
|
||||||
|
`Moving files and git history to ${destinationInSource}: ${file}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!relative(source, file).startsWith('..')) {
|
||||||
|
if (needsMove) {
|
||||||
|
const newPath = join(destinationInSource, file);
|
||||||
|
|
||||||
|
await mkdir(dirname(newPath), { recursive: true });
|
||||||
|
try {
|
||||||
|
await gitClient.move(file, newPath);
|
||||||
|
} catch {
|
||||||
|
await wait(100);
|
||||||
|
await gitClient.move(file, newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await rm(join(gitClient.root, file), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await gitClient.commit('chore(repo): prepare for import 2');
|
||||||
|
if (needsSquash) {
|
||||||
|
await gitClient.squashLastTwoCommits();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spinner.succeed(
|
||||||
|
`${sourceRemoteUrl} has been prepared to be imported into this workspace on a temporary branch: ${tempImportBranch} in ${gitClient.root}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
@ -2,7 +2,10 @@ import { existsSync } from 'fs';
|
|||||||
import { PackageJson } from '../../utils/package-json';
|
import { PackageJson } from '../../utils/package-json';
|
||||||
import { prerelease } from 'semver';
|
import { prerelease } from 'semver';
|
||||||
import { output } from '../../utils/output';
|
import { output } from '../../utils/output';
|
||||||
import { getPackageManagerCommand } from '../../utils/package-manager';
|
import {
|
||||||
|
getPackageManagerCommand,
|
||||||
|
PackageManagerCommands,
|
||||||
|
} from '../../utils/package-manager';
|
||||||
import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts';
|
import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts';
|
||||||
import { runNxSync } from '../../utils/child-process';
|
import { runNxSync } from '../../utils/child-process';
|
||||||
import { readJsonFile } from '../../utils/fileutils';
|
import { readJsonFile } from '../../utils/fileutils';
|
||||||
@ -23,6 +26,8 @@ import { globWithWorkspaceContext } from '../../utils/workspace-context';
|
|||||||
import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud';
|
import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud';
|
||||||
import { addNxToNpmRepo } from './implementation/add-nx-to-npm-repo';
|
import { addNxToNpmRepo } from './implementation/add-nx-to-npm-repo';
|
||||||
import { addNxToMonorepo } from './implementation/add-nx-to-monorepo';
|
import { addNxToMonorepo } from './implementation/add-nx-to-monorepo';
|
||||||
|
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
|
||||||
|
import { getPackageNameFromImportPath } from '../../utils/get-package-name-from-import-path';
|
||||||
|
|
||||||
export interface InitArgs {
|
export interface InitArgs {
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
@ -31,6 +36,34 @@ export interface InitArgs {
|
|||||||
integrated?: boolean; // For Angular projects only
|
integrated?: boolean; // For Angular projects only
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function installPlugins(
|
||||||
|
repoRoot: string,
|
||||||
|
plugins: string[],
|
||||||
|
pmc: PackageManagerCommands,
|
||||||
|
updatePackageScripts: boolean
|
||||||
|
) {
|
||||||
|
if (plugins.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDepsToPackageJson(repoRoot, plugins);
|
||||||
|
|
||||||
|
runInstall(repoRoot, pmc);
|
||||||
|
|
||||||
|
output.log({ title: '🔨 Configuring plugins' });
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
execSync(
|
||||||
|
`${pmc.exec} nx g ${plugin}:init --keepExistingVersions ${
|
||||||
|
updatePackageScripts ? '--updatePackageScripts' : ''
|
||||||
|
} --no-interactive`,
|
||||||
|
{
|
||||||
|
stdio: [0, 1, 2],
|
||||||
|
cwd: repoRoot,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function initHandler(options: InitArgs): Promise<void> {
|
export async function initHandler(options: InitArgs): Promise<void> {
|
||||||
process.env.NX_RUNNING_NX_INIT = 'true';
|
process.env.NX_RUNNING_NX_INIT = 'true';
|
||||||
const version =
|
const version =
|
||||||
@ -50,7 +83,8 @@ export async function initHandler(options: InitArgs): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
generateDotNxSetup(version);
|
generateDotNxSetup(version);
|
||||||
const { plugins } = await detectPlugins();
|
const nxJson = readNxJson(process.cwd());
|
||||||
|
const { plugins } = await detectPlugins(nxJson, options.interactive);
|
||||||
plugins.forEach((plugin) => {
|
plugins.forEach((plugin) => {
|
||||||
runNxSync(`add ${plugin}`, {
|
runNxSync(`add ${plugin}`, {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
@ -75,10 +109,6 @@ export async function initHandler(options: InitArgs): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
output.log({ title: '🧐 Checking dependencies' });
|
|
||||||
|
|
||||||
const { plugins, updatePackageScripts } = await detectPlugins();
|
|
||||||
|
|
||||||
const packageJson: PackageJson = readJsonFile('package.json');
|
const packageJson: PackageJson = readJsonFile('package.json');
|
||||||
if (isMonorepo(packageJson)) {
|
if (isMonorepo(packageJson)) {
|
||||||
await addNxToMonorepo({
|
await addNxToMonorepo({
|
||||||
@ -104,26 +134,18 @@ export async function initHandler(options: InitArgs): Promise<void> {
|
|||||||
createNxJsonFile(repoRoot, [], [], {});
|
createNxJsonFile(repoRoot, [], [], {});
|
||||||
updateGitIgnore(repoRoot);
|
updateGitIgnore(repoRoot);
|
||||||
|
|
||||||
addDepsToPackageJson(repoRoot, plugins);
|
const nxJson = readNxJson(repoRoot);
|
||||||
|
|
||||||
|
output.log({ title: '🧐 Checking dependencies' });
|
||||||
|
|
||||||
|
const { plugins, updatePackageScripts } = await detectPlugins(
|
||||||
|
nxJson,
|
||||||
|
options.interactive
|
||||||
|
);
|
||||||
|
|
||||||
output.log({ title: '📦 Installing Nx' });
|
output.log({ title: '📦 Installing Nx' });
|
||||||
|
|
||||||
runInstall(repoRoot, pmc);
|
installPlugins(repoRoot, plugins, pmc, updatePackageScripts);
|
||||||
|
|
||||||
if (plugins.length > 0) {
|
|
||||||
output.log({ title: '🔨 Configuring plugins' });
|
|
||||||
for (const plugin of plugins) {
|
|
||||||
execSync(
|
|
||||||
`${pmc.exec} nx g ${plugin}:init --keepExistingVersions ${
|
|
||||||
updatePackageScripts ? '--updatePackageScripts' : ''
|
|
||||||
} --no-interactive`,
|
|
||||||
{
|
|
||||||
stdio: [0, 1, 2],
|
|
||||||
cwd: repoRoot,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useNxCloud) {
|
if (useNxCloud) {
|
||||||
output.log({ title: '🛠️ Setting up Nx Cloud' });
|
output.log({ title: '🛠️ Setting up Nx Cloud' });
|
||||||
@ -157,7 +179,10 @@ const npmPackageToPluginMap: Record<string, string> = {
|
|||||||
'@remix-run/dev': '@nx/remix',
|
'@remix-run/dev': '@nx/remix',
|
||||||
};
|
};
|
||||||
|
|
||||||
async function detectPlugins(): Promise<{
|
export async function detectPlugins(
|
||||||
|
nxJson: NxJsonConfiguration,
|
||||||
|
interactive: boolean
|
||||||
|
): Promise<{
|
||||||
plugins: string[];
|
plugins: string[];
|
||||||
updatePackageScripts: boolean;
|
updatePackageScripts: boolean;
|
||||||
}> {
|
}> {
|
||||||
@ -165,6 +190,13 @@ async function detectPlugins(): Promise<{
|
|||||||
await globWithWorkspaceContext(process.cwd(), ['**/*/package.json'])
|
await globWithWorkspaceContext(process.cwd(), ['**/*/package.json'])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const currentPlugins = new Set(
|
||||||
|
(nxJson.plugins ?? []).map((p) => {
|
||||||
|
const plugin = typeof p === 'string' ? p : p.plugin;
|
||||||
|
return getPackageNameFromImportPath(plugin);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const detectedPlugins = new Set<string>();
|
const detectedPlugins = new Set<string>();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!existsSync(file)) continue;
|
if (!existsSync(file)) continue;
|
||||||
@ -192,6 +224,13 @@ async function detectPlugins(): Promise<{
|
|||||||
detectedPlugins.add('@nx/gradle');
|
detectedPlugins.add('@nx/gradle');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove existing plugins
|
||||||
|
for (const plugin of detectedPlugins) {
|
||||||
|
if (currentPlugins.has(plugin)) {
|
||||||
|
detectedPlugins.delete(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const plugins = Array.from(detectedPlugins);
|
const plugins = Array.from(detectedPlugins);
|
||||||
|
|
||||||
if (plugins.length === 0) {
|
if (plugins.length === 0) {
|
||||||
@ -201,6 +240,20 @@ async function detectPlugins(): Promise<{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!interactive) {
|
||||||
|
output.log({
|
||||||
|
title: `Recommended Plugins:`,
|
||||||
|
bodyLines: [
|
||||||
|
`Adding these Nx plugins to integrate with the tools used in your workspace:`,
|
||||||
|
...plugins.map((p) => `- ${p}`),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
plugins,
|
||||||
|
updatePackageScripts: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
output.log({
|
output.log({
|
||||||
title: `Recommended Plugins:`,
|
title: `Recommended Plugins:`,
|
||||||
bodyLines: [
|
bodyLines: [
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
yargsFormatWriteCommand,
|
yargsFormatWriteCommand,
|
||||||
} from './format/command-object';
|
} from './format/command-object';
|
||||||
import { yargsGenerateCommand } from './generate/command-object';
|
import { yargsGenerateCommand } from './generate/command-object';
|
||||||
|
import { yargsImportCommand } from './import/command-object';
|
||||||
import { yargsInitCommand } from './init/command-object';
|
import { yargsInitCommand } from './init/command-object';
|
||||||
import { yargsListCommand } from './list/command-object';
|
import { yargsListCommand } from './list/command-object';
|
||||||
import {
|
import {
|
||||||
@ -74,6 +75,7 @@ export const commandsObject = yargs
|
|||||||
.command(yargsFormatCheckCommand)
|
.command(yargsFormatCheckCommand)
|
||||||
.command(yargsFormatWriteCommand)
|
.command(yargsFormatWriteCommand)
|
||||||
.command(yargsGenerateCommand)
|
.command(yargsGenerateCommand)
|
||||||
|
.command(yargsImportCommand)
|
||||||
.command(yargsInitCommand)
|
.command(yargsInitCommand)
|
||||||
.command(yargsInternalMigrateCommand)
|
.command(yargsInternalMigrateCommand)
|
||||||
.command(yargsListCommand)
|
.command(yargsListCommand)
|
||||||
|
|||||||
@ -124,7 +124,7 @@ export function withConfiguration(yargs: Argv) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withVerbose(yargs: Argv) {
|
export function withVerbose<T>(yargs: Argv<T>) {
|
||||||
return yargs
|
return yargs
|
||||||
.option('verbose', {
|
.option('verbose', {
|
||||||
describe:
|
describe:
|
||||||
|
|||||||
37
packages/nx/src/utils/__snapshots__/git-utils.spec.ts.snap
Normal file
37
packages/nx/src/utils/__snapshots__/git-utils.spec.ts.snap
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`git utils tests updateRebaseFile should squash the last 2 commits 1`] = `
|
||||||
|
"pick 6a642190 chore(repo): hi
|
||||||
|
pick 022528d9 chore(repo): prepare for import
|
||||||
|
fixup 84ef7741 feat(repo): complete import of git@github.com:FrozenPandaz/created-vite-app.git
|
||||||
|
|
||||||
|
# Rebase 3441f39e..84ef7741 onto 3441f39e (3 commands)
|
||||||
|
#
|
||||||
|
# Commands:
|
||||||
|
# p, pick <commit> = use commit
|
||||||
|
# r, reword <commit> = use commit, but edit the commit message
|
||||||
|
# e, edit <commit> = use commit, but stop for amending
|
||||||
|
# s, squash <commit> = use commit, but meld into previous commit
|
||||||
|
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
|
||||||
|
# commit's log message, unless -C is used, in which case
|
||||||
|
# keep only this commit's message; -c is same as -C but
|
||||||
|
# opens the editor
|
||||||
|
# x, exec <command> = run command (the rest of the line) using shell
|
||||||
|
# b, break = stop here (continue rebase later with 'git rebase --continue')
|
||||||
|
# d, drop <commit> = remove commit
|
||||||
|
# l, label <label> = label current HEAD with a name
|
||||||
|
# t, reset <label> = reset HEAD to a label
|
||||||
|
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
|
||||||
|
# create a merge commit using the original merge commit's
|
||||||
|
# message (or the oneline, if no original merge commit was
|
||||||
|
# specified); use -c <commit> to reword the commit message
|
||||||
|
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
|
||||||
|
# to this position in the new commits. The <ref> is
|
||||||
|
# updated at the end of the rebase
|
||||||
|
#
|
||||||
|
# These lines can be re-ordered; they are executed from top to bottom.
|
||||||
|
#
|
||||||
|
# If you remove a line here THAT COMMIT WILL BE LOST.
|
||||||
|
#
|
||||||
|
# However, if you remove everything, the rebase will be aborted."
|
||||||
|
`;
|
||||||
@ -56,6 +56,10 @@ export function createOverrides(__overrides_unparsed__: string[] = []) {
|
|||||||
return overrides;
|
return overrides;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBaseRef(nxJson: NxJsonConfiguration) {
|
||||||
|
return nxJson.defaultBase ?? nxJson.affected?.defaultBase ?? 'main';
|
||||||
|
}
|
||||||
|
|
||||||
export function splitArgsIntoNxArgsAndOverrides(
|
export function splitArgsIntoNxArgsAndOverrides(
|
||||||
args: { [k: string]: any },
|
args: { [k: string]: any },
|
||||||
mode: 'run-one' | 'run-many' | 'affected' | 'print-affected',
|
mode: 'run-one' | 'run-many' | 'affected' | 'print-affected',
|
||||||
@ -143,8 +147,7 @@ export function splitArgsIntoNxArgsAndOverrides(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!nxArgs.base) {
|
if (!nxArgs.base) {
|
||||||
nxArgs.base =
|
nxArgs.base = getBaseRef(nxJson);
|
||||||
nxJson.defaultBase ?? nxJson.affected?.defaultBase ?? 'main';
|
|
||||||
|
|
||||||
// No user-provided arguments to set the affected criteria, so inform the user of the defaults being used
|
// No user-provided arguments to set the affected criteria, so inform the user of the defaults being used
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
extractUserAndRepoFromGitHubUrl,
|
extractUserAndRepoFromGitHubUrl,
|
||||||
getGithubSlugOrNull,
|
getGithubSlugOrNull,
|
||||||
|
updateRebaseFile,
|
||||||
} from './git-utils';
|
} from './git-utils';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
@ -220,4 +221,48 @@ describe('git utils tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateRebaseFile', () => {
|
||||||
|
let rebaseFileContents;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rebaseFileContents = `pick 6a642190 chore(repo): hi
|
||||||
|
pick 022528d9 chore(repo): prepare for import
|
||||||
|
pick 84ef7741 feat(repo): complete import of git@github.com:FrozenPandaz/created-vite-app.git
|
||||||
|
|
||||||
|
# Rebase 3441f39e..84ef7741 onto 3441f39e (3 commands)
|
||||||
|
#
|
||||||
|
# Commands:
|
||||||
|
# p, pick <commit> = use commit
|
||||||
|
# r, reword <commit> = use commit, but edit the commit message
|
||||||
|
# e, edit <commit> = use commit, but stop for amending
|
||||||
|
# s, squash <commit> = use commit, but meld into previous commit
|
||||||
|
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
|
||||||
|
# commit's log message, unless -C is used, in which case
|
||||||
|
# keep only this commit's message; -c is same as -C but
|
||||||
|
# opens the editor
|
||||||
|
# x, exec <command> = run command (the rest of the line) using shell
|
||||||
|
# b, break = stop here (continue rebase later with 'git rebase --continue')
|
||||||
|
# d, drop <commit> = remove commit
|
||||||
|
# l, label <label> = label current HEAD with a name
|
||||||
|
# t, reset <label> = reset HEAD to a label
|
||||||
|
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
|
||||||
|
# create a merge commit using the original merge commit's
|
||||||
|
# message (or the oneline, if no original merge commit was
|
||||||
|
# specified); use -c <commit> to reword the commit message
|
||||||
|
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
|
||||||
|
# to this position in the new commits. The <ref> is
|
||||||
|
# updated at the end of the rebase
|
||||||
|
#
|
||||||
|
# These lines can be re-ordered; they are executed from top to bottom.
|
||||||
|
#
|
||||||
|
# If you remove a line here THAT COMMIT WILL BE LOST.
|
||||||
|
#
|
||||||
|
# However, if you remove everything, the rebase will be aborted.`;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should squash the last 2 commits', () => {
|
||||||
|
expect(updateRebaseFile(rebaseFileContents)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,162 @@
|
|||||||
import { execSync } from 'child_process';
|
import { exec, ExecOptions, execSync, ExecSyncOptions } from 'child_process';
|
||||||
import { logger } from '../devkit-exports';
|
import { logger } from '../devkit-exports';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
|
const SQUASH_EDITOR = join(__dirname, 'squash.js');
|
||||||
|
|
||||||
|
function execAsync(command: string, execOptions: ExecOptions) {
|
||||||
|
return new Promise<string>((res, rej) => {
|
||||||
|
exec(command, execOptions, (err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
return rej(err);
|
||||||
|
}
|
||||||
|
res(stdout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cloneFromUpstream(
|
||||||
|
url: string,
|
||||||
|
destination: string,
|
||||||
|
{ originName } = { originName: 'origin' }
|
||||||
|
) {
|
||||||
|
await execAsync(
|
||||||
|
`git clone ${url} ${destination} --depth 1 --origin ${originName}`,
|
||||||
|
{
|
||||||
|
cwd: dirname(destination),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return new GitRepository(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GitRepository {
|
||||||
|
public root = this.getGitRootPath(this.directory);
|
||||||
|
constructor(private directory: string) {}
|
||||||
|
|
||||||
|
getGitRootPath(cwd: string) {
|
||||||
|
return execSync('git rev-parse --show-toplevel', {
|
||||||
|
cwd,
|
||||||
|
})
|
||||||
|
.toString()
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
addFetchRemote(remoteName: string, branch: string) {
|
||||||
|
return this.execAsync(
|
||||||
|
`git config --add remote.${remoteName}.fetch "+refs/heads/${branch}:refs/remotes/${remoteName}/${branch}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private execAsync(command: string) {
|
||||||
|
return execAsync(command, {
|
||||||
|
cwd: this.root,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async showStat() {
|
||||||
|
return await this.execAsync(`git show --stat`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listBranches() {
|
||||||
|
return (await this.execAsync(`git ls-remote --heads --quiet`))
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((s) =>
|
||||||
|
s
|
||||||
|
.trim()
|
||||||
|
.substring(s.indexOf('\t') + 1)
|
||||||
|
.replace('refs/heads/', '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGitFiles(path: string) {
|
||||||
|
return (await this.execAsync(`git ls-files ${path}`))
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reset(ref: string) {
|
||||||
|
return this.execAsync(`git reset ${ref} --hard`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async squashLastTwoCommits() {
|
||||||
|
return this.execAsync(
|
||||||
|
`git -c core.editor="node ${SQUASH_EDITOR}" rebase --interactive --no-autosquash HEAD~2`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async mergeUnrelatedHistories(ref: string, message: string) {
|
||||||
|
return this.execAsync(
|
||||||
|
`git merge ${ref} -X ours --allow-unrelated-histories -m "${message}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async fetch(remote: string, ref?: string) {
|
||||||
|
return this.execAsync(`git fetch ${remote}${ref ? ` ${ref}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkout(
|
||||||
|
branch: string,
|
||||||
|
opts: {
|
||||||
|
new: boolean;
|
||||||
|
base: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return this.execAsync(
|
||||||
|
`git checkout ${opts.new ? '-b ' : ' '}${branch}${
|
||||||
|
opts.base ? ' ' + opts.base : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async move(path: string, destination: string) {
|
||||||
|
return this.execAsync(`git mv ${path} ${destination}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async push(ref: string, remoteName: string) {
|
||||||
|
return this.execAsync(`git push -u -f ${remoteName} ${ref}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async commit(message: string) {
|
||||||
|
return this.execAsync(`git commit -am "${message}"`);
|
||||||
|
}
|
||||||
|
async amendCommit() {
|
||||||
|
return this.execAsync(`git commit --amend -a --no-edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteGitRemote(name: string) {
|
||||||
|
return this.execAsync(`git remote rm ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteBranch(branch: string) {
|
||||||
|
return this.execAsync(`git branch -D ${branch}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
addGitRemote(name: string, url: string) {
|
||||||
|
return this.execAsync(`git remote add ${name} ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used by the squash editor script to update the rebase file.
|
||||||
|
*/
|
||||||
|
export function updateRebaseFile(contents: string): string {
|
||||||
|
const lines = contents.split('\n');
|
||||||
|
const lastCommitIndex = lines.findIndex((line) => line === '') - 1;
|
||||||
|
|
||||||
|
lines[lastCommitIndex] = lines[lastCommitIndex].replace('pick', 'fixup');
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchGitRemote(
|
||||||
|
name: string,
|
||||||
|
branch: string,
|
||||||
|
execOptions: ExecSyncOptions
|
||||||
|
) {
|
||||||
|
return execSync(`git fetch ${name} ${branch} --depth 1`, execOptions);
|
||||||
|
}
|
||||||
|
|
||||||
export function getGithubSlugOrNull(): string | null {
|
export function getGithubSlugOrNull(): string | null {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export function isWorkspacesEnabled(
|
|||||||
return existsSync(join(root, 'pnpm-workspace.yaml'));
|
return existsSync(join(root, 'pnpm-workspace.yaml'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// yarn and pnpm both use the same 'workspaces' property in package.json
|
// yarn and npm both use the same 'workspaces' property in package.json
|
||||||
const packageJson: PackageJson = readPackageJson();
|
const packageJson: PackageJson = readPackageJson();
|
||||||
return !!packageJson?.workspaces;
|
return !!packageJson?.workspaces;
|
||||||
}
|
}
|
||||||
|
|||||||
14
packages/nx/src/utils/squash.ts
Normal file
14
packages/nx/src/utils/squash.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { updateRebaseFile } from './git-utils';
|
||||||
|
|
||||||
|
// This script is used as an editor for git rebase -i
|
||||||
|
|
||||||
|
// This is the file which git creates. When this script exits, the updates should be written to this file.
|
||||||
|
const filePath = process.argv[2];
|
||||||
|
|
||||||
|
// Change the second commit from pick to fixup
|
||||||
|
const contents = readFileSync(filePath).toString();
|
||||||
|
const newContents = updateRebaseFile(contents);
|
||||||
|
|
||||||
|
// Write the updated contents back to the file
|
||||||
|
writeFileSync(filePath, newContents);
|
||||||
Loading…
x
Reference in New Issue
Block a user