From 30d36ef3ba11dbcaf2e89df309e4d9b31e315d5e Mon Sep 17 00:00:00 2001 From: David First Date: Thu, 13 Jul 2023 15:28:44 -0400 Subject: [PATCH 1/2] feat(revert): introduce a new command "bit revert" --- e2e/harmony/revert.e2e.ts | 33 ++++++++++++++ scopes/component/checkout/checkout-cmd.ts | 3 ++ .../checkout/checkout.main.runtime.ts | 16 +++++-- scopes/component/checkout/revert-cmd.ts | 45 +++++++++++++++++++ src/e2e-helper/e2e-command-helper.ts | 4 ++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 e2e/harmony/revert.e2e.ts create mode 100644 scopes/component/checkout/revert-cmd.ts diff --git a/e2e/harmony/revert.e2e.ts b/e2e/harmony/revert.e2e.ts new file mode 100644 index 000000000000..0dd3c658512d --- /dev/null +++ b/e2e/harmony/revert.e2e.ts @@ -0,0 +1,33 @@ +import chai, { expect } from 'chai'; +import Helper from '../../src/e2e-helper/e2e-helper'; + +chai.use(require('chai-fs')); + +describe('bit revert command', function () { + this.timeout(0); + let helper: Helper; + before(() => { + helper = new Helper(); + }); + after(() => { + helper.scopeHelper.destroy(); + }); + describe('basic revert', () => { + before(() => { + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.fixtures.populateComponents(1, false); + helper.command.tagAllWithoutBuild(); + helper.fixtures.populateComponents(1, false, 'v2'); + helper.command.tagAllWithoutBuild(); + helper.command.revert('comp1', '0.0.1', '-x'); + }); + it('should change the code to the specified version', () => { + const content = helper.fs.readFile('comp1/index.js'); + expect(content).to.not.have.string('v2'); + }); + it('should keep the version in .bitmap intact', () => { + const bitmap = helper.bitMap.read(); + expect(bitmap.comp1.version).to.equal('0.0.2'); + }); + }); +}); diff --git a/scopes/component/checkout/checkout-cmd.ts b/scopes/component/checkout/checkout-cmd.ts index 0fdaef0b0580..fb1c333a8064 100644 --- a/scopes/component/checkout/checkout-cmd.ts +++ b/scopes/component/checkout/checkout-cmd.ts @@ -68,6 +68,7 @@ export class CheckoutCmd implements Command { workspaceOnly = false, verbose = false, skipDependencyInstallation = false, + revert = false, }: { interactiveMerge?: boolean; ours?: boolean; @@ -77,6 +78,7 @@ export class CheckoutCmd implements Command { workspaceOnly?: boolean; verbose?: boolean; skipDependencyInstallation?: boolean; + revert?: boolean; } ) { const checkoutProps: CheckoutProps = { @@ -87,6 +89,7 @@ export class CheckoutCmd implements Command { isLane: false, skipNpmInstall: skipDependencyInstallation, workspaceOnly, + revert, }; const { components, diff --git a/scopes/component/checkout/checkout.main.runtime.ts b/scopes/component/checkout/checkout.main.runtime.ts index 98d5a3bc2fe3..77381d4407e1 100644 --- a/scopes/component/checkout/checkout.main.runtime.ts +++ b/scopes/component/checkout/checkout.main.runtime.ts @@ -32,17 +32,20 @@ import { deleteFilesIfNeeded, ComponentStatusBase, } from './checkout-version'; +import { RevertCmd } from './revert-cmd'; export type CheckoutProps = { version?: string; // if reset/head/latest is true, the version is undefined ids?: ComponentID[]; head?: boolean; latest?: boolean; + main?: boolean; // relevant for "revert" only promptMergeOptions?: boolean; mergeStrategy?: MergeStrategy | null; verbose?: boolean; skipNpmInstall?: boolean; reset?: boolean; // remove local changes. if set, the version is undefined. + revert?: boolean; all?: boolean; // checkout all ids isLane?: boolean; workspaceOnly?: boolean; @@ -202,7 +205,8 @@ export class CheckoutMain { componentPattern: string, checkoutProps: CheckoutProps ): Promise { - this.logger.setStatusLine(BEFORE_CHECKOUT); + const { revert } = checkoutProps; + this.logger.setStatusLine(revert ? 'reverting components...' : BEFORE_CHECKOUT); if (!this.workspace) throw new OutsideWorkspaceError(); const consumer = this.workspace.consumer; await this.importer.importCurrentObjects(); // important. among others, it fetches the remote lane object and its new components. @@ -237,6 +241,7 @@ export class CheckoutMain { if (to === HEAD) checkoutProps.head = true; else if (to === LATEST) checkoutProps.latest = true; else if (to === 'reset') checkoutProps.reset = true; + else if (to === 'main') checkoutProps.main = true; else { if (!BitId.isValidVersion(to)) throw new BitError(`the specified version "${to}" is not a valid version`); checkoutProps.version = to; @@ -262,6 +267,9 @@ export class CheckoutMain { if (checkoutProps.workspaceOnly && !checkoutProps.head) { throw new BitError(`--workspace-only flag can only be used with "head" (bit checkout head --workspace-only)`); } + if (checkoutProps.revert) { + checkoutProps.skipUpdatingBitmap = true; + } const idsOnWorkspace = componentPattern ? await this.workspace.idsByPattern(componentPattern) : await this.workspace.listIds(); @@ -297,7 +305,7 @@ export class CheckoutMain { checkoutProps: CheckoutProps ): Promise { const consumer = this.workspace.consumer; - const { version, head: headVersion, reset, latest: latestVersion, versionPerId } = checkoutProps; + const { version, head: headVersion, reset, revert, latest: latestVersion, versionPerId } = checkoutProps; const repo = consumer.scope.objects; const componentModel = await consumer.scope.getModelComponentIfExist(component.id); const componentStatus: ComponentStatusBeforeMergeAttempt = { id: component.id }; @@ -374,7 +382,7 @@ export class CheckoutMain { const newId = component.id.changeVersion(newVersion); - if (reset || !isModified) { + if (reset || !isModified || revert) { // if the component is not modified, no need to try merge the files, they will be written later on according to the // checked out version. same thing when no version is specified, it'll be reset to the model-version later. return { currentComponent: component, componentFromModel: componentVersion, id: newId }; @@ -439,7 +447,7 @@ export class CheckoutMain { ]) { const logger = loggerMain.createLogger(CheckoutAspect.id); const checkoutMain = new CheckoutMain(workspace, logger, compWriter, importer, remove); - cli.register(new CheckoutCmd(checkoutMain)); + cli.register(new CheckoutCmd(checkoutMain), new RevertCmd(checkoutMain)); return checkoutMain; } } diff --git a/scopes/component/checkout/revert-cmd.ts b/scopes/component/checkout/revert-cmd.ts new file mode 100644 index 000000000000..bcea5502cb6e --- /dev/null +++ b/scopes/component/checkout/revert-cmd.ts @@ -0,0 +1,45 @@ +import { Command, CommandOptions } from '@teambit/cli'; +import { COMPONENT_PATTERN_HELP } from '@teambit/legacy/dist/constants'; +import { CheckoutMain } from './checkout.main.runtime'; +import { CheckoutCmd } from './checkout-cmd'; + +export class RevertCmd implements Command { + name = 'revert '; + arguments = [ + { + name: 'component-pattern', + description: COMPONENT_PATTERN_HELP, + }, + { + name: 'to', + description: "permitted values: [main, specific-version]. 'main' - head version on main.", + }, + ]; + description = 'replace the current component files by the specified version, leave the version intact'; + group = 'development'; + alias = ''; + options = [ + ['v', 'verbose', 'showing verbose output for inspection'], + ['x', 'skip-dependency-installation', 'do not install packages of the imported components'], + ] as CommandOptions; + loader = true; + + constructor(private checkout: CheckoutMain) {} + + async report( + [componentPattern, to]: [string, string], + { + verbose = false, + skipDependencyInstallation = false, + }: { + verbose?: boolean; + skipDependencyInstallation?: boolean; + } + ) { + return new CheckoutCmd(this.checkout).report([to, componentPattern], { + verbose, + skipDependencyInstallation, + revert: true, + }); + } +} diff --git a/src/e2e-helper/e2e-command-helper.ts b/src/e2e-helper/e2e-command-helper.ts index 143df0673f48..db33f31b5566 100644 --- a/src/e2e-helper/e2e-command-helper.ts +++ b/src/e2e-helper/e2e-command-helper.ts @@ -522,6 +522,10 @@ export default class CommandHelper { return JSON.parse(status); } + revert(pattern: string, to: string, flags = '') { + return this.runCmd(`bit revert ${pattern} ${to} ${flags}`); + } + stash() { return this.runCmd('bit stash'); } From cb4ac8601e0fbf7ecec41f5b5235d36a5dd1bfb8 Mon Sep 17 00:00:00 2001 From: David First Date: Thu, 13 Jul 2023 16:04:33 -0400 Subject: [PATCH 2/2] implement bit revert main --- e2e/harmony/revert.e2e.ts | 39 +++++++++++++++++++ scopes/component/checkout/checkout-cmd.ts | 11 ++++-- .../checkout/checkout.main.runtime.ts | 14 ++++--- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/e2e/harmony/revert.e2e.ts b/e2e/harmony/revert.e2e.ts index 0dd3c658512d..96934522af9b 100644 --- a/e2e/harmony/revert.e2e.ts +++ b/e2e/harmony/revert.e2e.ts @@ -13,12 +13,14 @@ describe('bit revert command', function () { helper.scopeHelper.destroy(); }); describe('basic revert', () => { + let beforeRevert: string; before(() => { helper.scopeHelper.setNewLocalAndRemoteScopes(); helper.fixtures.populateComponents(1, false); helper.command.tagAllWithoutBuild(); helper.fixtures.populateComponents(1, false, 'v2'); helper.command.tagAllWithoutBuild(); + beforeRevert = helper.scopeHelper.cloneLocalScope(); helper.command.revert('comp1', '0.0.1', '-x'); }); it('should change the code to the specified version', () => { @@ -29,5 +31,42 @@ describe('bit revert command', function () { const bitmap = helper.bitMap.read(); expect(bitmap.comp1.version).to.equal('0.0.2'); }); + describe('when the component is modified', () => { + before(() => { + helper.scopeHelper.getClonedLocalScope(beforeRevert); + helper.fixtures.populateComponents(1, false, 'v3'); + helper.command.revert('comp1', '0.0.1', '-x'); + }); + it('should still change the code to the specified version', () => { + const content = helper.fs.readFile('comp1/index.js'); + expect(content).to.not.have.string('v2'); + expect(content).to.not.have.string('v3'); + }); + }); + }); + describe('revert from lane to main', () => { + before(() => { + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.fixtures.populateComponents(2); + helper.command.tagAllWithoutBuild(); + helper.command.export(); + helper.command.createLane('lane-a'); + helper.fixtures.populateComponents(2, undefined, 'v2'); + helper.command.snapAllComponentsWithoutBuild(); + helper.command.export(); + + helper.command.revert('"**"', 'main'); + }); + it('should change the code according to main', () => { + const content1 = helper.fs.readFile('comp1/index.js'); + const content2 = helper.fs.readFile('comp2/index.js'); + expect(content1).to.not.have.string('v2'); + expect(content2).to.not.have.string('v2'); + }); + it('should keep the versions intact', () => { + const bitmap = helper.bitMap.read(); + expect(bitmap.comp1.version).to.not.equal('0.0.1'); + expect(bitmap.comp2.version).to.not.equal('0.0.1'); + }); }); }); diff --git a/scopes/component/checkout/checkout-cmd.ts b/scopes/component/checkout/checkout-cmd.ts index fb1c333a8064..94bac6f871d6 100644 --- a/scopes/component/checkout/checkout-cmd.ts +++ b/scopes/component/checkout/checkout-cmd.ts @@ -105,6 +105,7 @@ export class CheckoutCmd implements Command { const isHead = to === 'head'; const isReset = to === 'reset'; const isLatest = to === 'latest'; + const isMain = to === 'main'; // components that failed for no legitimate reason. e.g. merge-conflict. const realFailedComponents = (failedComponents || []).filter((f) => !f.unchangedLegitimately); // components that weren't checked out for legitimate reasons, e.g. up-to-date. @@ -144,12 +145,13 @@ once ready, snap/tag the components to persist the changes`; return chalk.underline(title) + conflictSummaryReport(components) + chalk.yellow(suggestion); }; const getSuccessfulOutput = () => { + const switchedOrReverted = revert ? 'reverted' : 'switched'; if (!components || !components.length) return ''; if (components.length === 1) { const component = components[0]; const componentName = isReset ? component.id.toString() : component.id.toStringWithoutVersion(); if (isReset) return `successfully reset ${chalk.bold(componentName)}\n`; - const title = `successfully switched ${chalk.bold(componentName)} to version ${chalk.bold( + const title = `successfully ${switchedOrReverted} ${chalk.bold(componentName)} to version ${chalk.bold( // @ts-ignore version is defined when !isReset isHead || isLatest ? component.id.version : version )}\n`; @@ -163,11 +165,12 @@ once ready, snap/tag the components to persist the changes`; const getVerOutput = () => { if (isHead) return 'their head version'; if (isLatest) return 'their latest version'; + if (isMain) return 'their main version'; // @ts-ignore version is defined when !isReset return `version ${chalk.bold(version)}`; }; const versionOutput = getVerOutput(); - const title = `successfully switched the following components to ${versionOutput}\n\n`; + const title = `successfully ${switchedOrReverted} the following components to ${versionOutput}\n\n`; const showVersion = isHead || isReset; const componentsStr = applyVersionReport(components, true, showVersion); return chalk.underline(title) + componentsStr; @@ -185,8 +188,8 @@ once ready, snap/tag the components to persist the changes`; const notCheckedOutLegitimately = notCheckedOutComponents.length; const failedToCheckOut = realFailedComponents.length; const newLines = '\n\n'; - const title = chalk.bold.underline('Checkout Summary'); - const checkedOutStr = `\nTotal CheckedOut: ${chalk.bold(checkedOut.toString())}`; + const title = chalk.bold.underline('Summary'); + const checkedOutStr = `\nTotal Changed: ${chalk.bold(checkedOut.toString())}`; const unchangedLegitimatelyStr = `\nTotal Unchanged: ${chalk.bold(notCheckedOutLegitimately.toString())}`; const failedToCheckOutStr = `\nTotal Failed: ${chalk.bold(failedToCheckOut.toString())}`; const newOnLaneNum = newFromLane?.length || 0; diff --git a/scopes/component/checkout/checkout.main.runtime.ts b/scopes/component/checkout/checkout.main.runtime.ts index 77381d4407e1..2a61dfafc7ee 100644 --- a/scopes/component/checkout/checkout.main.runtime.ts +++ b/scopes/component/checkout/checkout.main.runtime.ts @@ -62,7 +62,7 @@ export type ComponentStatusBeforeMergeAttempt = ComponentStatusBase & { }; }; -type CheckoutTo = 'head' | 'reset' | string; +type CheckoutTo = 'head' | 'reset' | 'main' | string; export class CheckoutMain { constructor( @@ -305,7 +305,7 @@ export class CheckoutMain { checkoutProps: CheckoutProps ): Promise { const consumer = this.workspace.consumer; - const { version, head: headVersion, reset, revert, latest: latestVersion, versionPerId } = checkoutProps; + const { version, head: headVersion, reset, revert, main, latest: latestVersion, versionPerId } = checkoutProps; const repo = consumer.scope.objects; const componentModel = await consumer.scope.getModelComponentIfExist(component.id); const componentStatus: ComponentStatusBeforeMergeAttempt = { id: component.id }; @@ -317,6 +317,9 @@ export class CheckoutMain { if (!componentModel) { return returnFailure(`component ${component.id.toString()} is new, no version to checkout`, true); } + if (main && !componentModel.head) { + return returnFailure(`component ${component.id.toString()} is not available on main`); + } const unmerged = repo.unmergedComponents.getEntry(component.name); if (!reset && unmerged) { return returnFailure( @@ -325,8 +328,9 @@ export class CheckoutMain { } const getNewVersion = async (): Promise => { if (reset) return component.id.version as string; - if (headVersion) return componentModel.headIncludeRemote(repo); + // we verified previously that head exists in case of "main" + if (main) return componentModel.head?.toString() as string; if (latestVersion) { const latest = componentModel.latestVersionIfExist(); return latest || componentModel.headIncludeRemote(repo); @@ -335,8 +339,8 @@ export class CheckoutMain { return versionPerId.find((id) => id._legacy.isEqualWithoutVersion(component.id))?.version as string; } - // @ts-ignore if !reset the version is defined - return version; + // if all above are false, the version is defined + return version as string; }; const newVersion = await getNewVersion(); if (version && !headVersion) {