diff --git a/e2e/harmony/lanes/merge-lanes-from-scope.e2e.ts b/e2e/harmony/lanes/merge-lanes-from-scope.e2e.ts index 5a1b8967226b..62b1f971ad0e 100644 --- a/e2e/harmony/lanes/merge-lanes-from-scope.e2e.ts +++ b/e2e/harmony/lanes/merge-lanes-from-scope.e2e.ts @@ -588,4 +588,43 @@ describe('merge lanes from scope', function () { expect(obj.dependencies).to.have.lengthOf(0); }); }); + describe('merge from scope lane to main when it is not up to date with deps changes', () => { + let bareMerge; + before(() => { + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.fs.outputFile('comp/index.js', `require('@${helper.scopes.remote}/dep1');\nrequire('@${helper.scopes.remote}/dep2');`); + helper.fs.outputFile('dep1/index.js', 'console.log("hello");'); + helper.fs.outputFile('dep2/index.js', 'console.log("hello");'); + helper.command.addComponent('comp'); + helper.command.addComponent('dep1'); + helper.command.addComponent('dep2'); + helper.command.tagAllWithoutBuild(); + helper.command.export(); + + helper.command.createLane(); + helper.fs.outputFile('dep1/index.js', 'console.log("hello-from-lane");'); + helper.command.snapComponentWithoutBuild('dep1'); + helper.command.export(); + + helper.command.switchLocalLane('main', '-x'); + helper.fs.outputFile('dep2/index.js', 'console.log("hello-from-main");'); + helper.command.tagAllWithoutBuild(); + helper.command.export(); + + bareMerge = helper.scopeHelper.getNewBareScope('-bare-merge'); + helper.scopeHelper.addRemoteScope(helper.scopes.remotePath, bareMerge.scopePath); + }); + it('should throw without --allow-outdated-deps flag', () => { + const mergeFunc = () => helper.command.mergeLaneFromScope(bareMerge.scopePath, `${helper.scopes.remote}/dev`); + expect(mergeFunc).to.throw('unable to merge, the following components are not up-to-date'); + }); + it('should succeed with --allow-outdated-deps flag', () => { + const mergeFunc = () => helper.command.mergeLaneFromScope( + bareMerge.scopePath, + `${helper.scopes.remote}/dev`, + '--allow-outdated-deps --no-squash' + ); + expect(mergeFunc).to.not.throw(); + }); + }); }); diff --git a/scopes/lanes/lanes/lanes.main.runtime.ts b/scopes/lanes/lanes/lanes.main.runtime.ts index f8e891a244f5..c9a1b9ac3162 100644 --- a/scopes/lanes/lanes/lanes.main.runtime.ts +++ b/scopes/lanes/lanes/lanes.main.runtime.ts @@ -887,15 +887,16 @@ please create a new lane instead, which will include all components of this lane changed.push(ChangeType.SOURCE_CODE); } - if (compare.fields.length > 0) { - changed.push(ChangeType.ASPECTS); - } - const depsFields = ['dependencies', 'devDependencies', 'extensionDependencies']; if (compare.fields.some((field) => depsFields.includes(field.fieldName))) { changed.push(ChangeType.DEPENDENCY); } + const compareFieldsWithoutDeps = compare.fields.filter((field) => !depsFields.includes(field.fieldName)); + if (compareFieldsWithoutDeps.length > 0) { + changed.push(ChangeType.ASPECTS); + } + return changed; }; diff --git a/scopes/lanes/merge-lanes/merge-lane-from-scope.cmd.ts b/scopes/lanes/merge-lanes/merge-lane-from-scope.cmd.ts index c151efeba850..bb08035a4c42 100644 --- a/scopes/lanes/merge-lanes/merge-lane-from-scope.cmd.ts +++ b/scopes/lanes/merge-lanes/merge-lane-from-scope.cmd.ts @@ -15,6 +15,7 @@ type Flags = { title?: string; titleBase64?: string; reMerge?: boolean; + allowOutdatedDeps?: boolean; }; /** @@ -50,6 +51,7 @@ the lane must be up-to-date with the other lane, otherwise, conflicts might occu ['', 'no-squash', 'relevant for merging lanes into main, which by default squash.'], ['', 'include-deps', 'relevant for "--pattern". merge also dependencies of the given components'], ['', 're-merge', 'helpful when last merge failed during export. do not skip components that seemed to be merged'], + ['', 'allow-outdated-deps', 'if a component is out of date but has only dependencies changes, allow the merge'], ['j', 'json', 'output as json format'], ] as CommandOptions; loader = true; @@ -69,6 +71,7 @@ the lane must be up-to-date with the other lane, otherwise, conflicts might occu title, titleBase64, reMerge, + allowOutdatedDeps, }: Flags ): Promise { if (includeDeps && !pattern) { @@ -91,6 +94,7 @@ the lane must be up-to-date with the other lane, otherwise, conflicts might occu includeDeps, snapMessage: titleBase64Decoded || title, reMerge, + throwIfNotUpToDateForCodeChanges: allowOutdatedDeps } ); diff --git a/scopes/lanes/merge-lanes/merge-lanes.main.runtime.ts b/scopes/lanes/merge-lanes/merge-lanes.main.runtime.ts index 618560c14b50..118332583386 100644 --- a/scopes/lanes/merge-lanes/merge-lanes.main.runtime.ts +++ b/scopes/lanes/merge-lanes/merge-lanes.main.runtime.ts @@ -36,6 +36,7 @@ import { MergeAbortLaneCmd, MergeAbortOpts } from './merge-abort.cmd'; import { LastMerged } from './last-merged'; import { MergeMoveLaneCmd } from './merge-move.cmd'; import { DETACH_HEAD, isFeatureEnabled } from '@teambit/harmony.modules.feature-toggle'; +import { ChangeType } from '@teambit/lanes.entities.lane-diff'; export type MergeLaneOptions = { mergeStrategy: MergeStrategy; @@ -59,6 +60,7 @@ export type MergeLaneOptions = { excludeNonLaneComps?: boolean; shouldIncludeUpdateDependents?: boolean; throwIfNotUpToDate?: boolean; // relevant when merging from a scope + throwIfNotUpToDateForCodeChanges?: boolean; // relevant when merging from a scope fetchCurrent?: boolean; // needed when merging from a bare-scope (because it's empty) detachHead?: boolean; }; @@ -129,6 +131,7 @@ export class MergeLanesMain { excludeNonLaneComps, shouldIncludeUpdateDependents, throwIfNotUpToDate, + throwIfNotUpToDateForCodeChanges, fetchCurrent, detachHead, } = options; @@ -201,7 +204,9 @@ export class MergeLanesMain { }; const idsToMerge = await getBitIds(); - if (throwIfNotUpToDate) await this.throwIfNotUpToDate(otherLaneId, currentLaneId); + if (throwIfNotUpToDate || throwIfNotUpToDateForCodeChanges) { + await this.throwIfNotUpToDate(otherLaneId, currentLaneId, throwIfNotUpToDateForCodeChanges); + } this.logger.debug(`merging the following ids: ${idsToMerge.toString()}`); @@ -498,13 +503,26 @@ export class MergeLanesMain { mergeSnapError, }; } - private async throwIfNotUpToDate(fromLaneId: LaneId, toLaneId: LaneId) { - const status = await this.lanes.diffStatus(fromLaneId, toLaneId, { skipChanges: true }); + private async throwIfNotUpToDate(fromLaneId: LaneId, toLaneId: LaneId, throwForCodeChangesOnly?: boolean) { + // if "throwForCodeChangesOnly" is true, we have to check what type of change is it. if it is a dependency change, + // it's fine. if it's a code change, throw. + const status = await this.lanes.diffStatus(fromLaneId, toLaneId, { skipChanges: !throwForCodeChangesOnly }); const compsNotUpToDate = status.componentsStatus.filter((s) => !s.upToDate); - if (compsNotUpToDate.length) { + if (!compsNotUpToDate.length) { + return; + } + if (!throwForCodeChangesOnly) { throw new Error(`unable to merge, the following components are not up-to-date: ${compsNotUpToDate.map((s) => s.componentId.toString()).join('\n')}`); } + const codeChanges = compsNotUpToDate.filter((s) => { + return s.changes?.includes(ChangeType.SOURCE_CODE) || s.changes?.includes(ChangeType.ASPECTS); + }); + + if (codeChanges.length) { + throw new Error(`unable to merge, the following components are not up-to-date and have code/aspects changes: +${codeChanges.map((s) => s.componentId.toString()).join('\n')}`); + } } static slots = [];