Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support maintaining multiple versions/branches on main #9362

Merged
merged 20 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions components/legacy/component-list/components-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ export class ComponentsList {
const fromBitMap = this.bitMap.getAllIdsAvailableOnLaneIncludeRemoved();
const modelComponents = await this.getModelComponents();
const pendingExportComponents = await pFilter(modelComponents, async (component: ModelComponent) => {
if (!fromBitMap.searchWithoutVersion(component.toComponentId())) {
const foundInBitMap = fromBitMap.searchWithoutVersion(component.toComponentId());
if (!foundInBitMap) {
// it's not on the .bitmap only in the scope, as part of the out-of-sync feature, it should
// be considered as staged and should be exported. same for soft-removed components, which are on scope only.
// notice that we use `hasLocalChanges`
Expand All @@ -169,8 +170,7 @@ export class ComponentsList {
// be exported unexpectedly.
return component.isLocallyChangedRegardlessOfLanes();
}
await component.setDivergeData(this.scope.objects);
return component.isLocallyChanged(this.scope.objects, lane);
return component.isLocallyChanged(this.scope.objects, lane, foundInBitMap);
});
const ids = ComponentIdList.fromArray(pendingExportComponents.map((c) => c.toComponentId()));
return this.updateIdsFromModelIfTheyOutOfSync(ids);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@ export class ModelComponentMerger {
this.addOrphanedVersionFromIncoming();
this.setHead(locallyChanged);
this.deleteOrphanedVersionsOnExport();
this.mergeDetachedHeads();

return { mergedComponent: this.existingComponent, mergedVersions: this.mergedVersions };
}

private mergeDetachedHeads() {
this.existingComponent.detachedHeads.merge(this.incomingComponent.detachedHeads, this.isImport);
}

private deleteOrphanedVersionsOnExport() {
// makes sure that components received with orphanedVersions, this property won't be saved
if (this.isExport) this.existingComponent.orphanedVersions = {};
Expand Down
5 changes: 5 additions & 0 deletions components/legacy/scope/garbage-collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ export async function collectGarbage(thisScope: Scope, opts: GarbageCollectorOpt

await pMapSeries(compsOfThisScope, async (comp) => {
await processComponent(comp, comp.head, true);
const detachedHeads = comp.detachedHeads.getAllHeads();
if (!detachedHeads.length) return;
await pMapSeries(detachedHeads, async (head) => {
await processComponent(comp, head, true);
});
});

logger.console(`[*] completed processing local components. total ${refsWhiteList.size} refs in the white list`);
Expand Down
3 changes: 3 additions & 0 deletions components/legacy/scope/repositories/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ please either remove the component (bit remove) or remove the lane.`);
throw new Error(`fatal: "head" prop was removed from "${component.id()}", although it has versions`);
}

component.detachedHeads.removeLocalHeads(versionsRefs);

if (component.versionArray.length || component.hasHead() || component.laneHeadLocal) {
objectRepo.add(component); // add the modified component object
} else {
Expand Down Expand Up @@ -502,6 +504,7 @@ please either remove the component (bit remove) or remove the lane.`);
const existingHeadIsMissingInIncomingComponent = Boolean(
incomingComp.hasHead() &&
existingComponentHead &&
!incomingComp.detachedHeads.getCurrent() &&
!hashesOfHistoryGraph.find((ref) => ref.isEqual(existingComponentHead))
);
// currently it'll always be true. later, we might want to support exporting
Expand Down
42 changes: 41 additions & 1 deletion e2e/commands/reset.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from 'chai';
import { MissingBitMapComponent } from '@teambit/legacy.bit-map';
import { Helper } from '@teambit/legacy.e2e-helper';
import { DETACH_HEAD } from '@teambit/harmony.modules.feature-toggle';

describe('bit reset command', function () {
this.timeout(0);
Expand Down Expand Up @@ -305,7 +306,45 @@ describe('bit reset command', function () {
expect(stagedConfig).to.have.lengthOf(0);
});
});
describe('when checked out to a non-head version', () => {
describe('when checked out to a non-head version with detach-head functionality', () => {
before(() => {
helper.scopeHelper.setNewLocalAndRemoteScopes();
helper.command.setFeatures(DETACH_HEAD);
helper.fixtures.populateComponents(1, false);
helper.command.tagWithoutBuild();
helper.fixtures.populateComponents(1, false, 'version2');
helper.command.tagWithoutBuild();
helper.fixtures.populateComponents(1, false, 'version3');
helper.command.tagWithoutBuild();
helper.command.export();
helper.command.checkoutVersion('0.0.2', 'comp1', '-x');
helper.command.snapComponentWithoutBuild('comp1', '--unmodified --detach-head');

// an intermediate step, make sure the component is detached
const comp = helper.command.catComponent('comp1');
expect(comp).to.have.property('detachedHeads');
expect(comp.detachedHeads.current).to.not.be.undefined;

helper.command.resetAll();
});
after(() => {
helper.command.resetFeatures();
});
it('expect .bitmap to point to the same version as it was before the reset, and not the latest', () => {
const bitmap = helper.bitMap.read();
expect(bitmap.comp1.version).to.equal('0.0.2');
});
it('should not show the component as modified', () => {
const status = helper.command.statusJson();
expect(status.modifiedComponents).to.have.lengthOf(0);
});
it('should clear the detached head', () => {
const comp = helper.command.catComponent('comp1');
expect(comp).to.not.have.property('detachedHeads');
});
});
// todo: delete this test once detach-head is not under feature-toggle
describe('when checked out to a non-head version without detach-head', () => {
before(() => {
helper.scopeHelper.setNewLocalAndRemoteScopes();
helper.fixtures.populateComponents(1, false);
Expand All @@ -317,6 +356,7 @@ describe('bit reset command', function () {
helper.command.export();
helper.command.checkoutVersion('0.0.2', 'comp1', '-x');
helper.command.snapComponentWithoutBuild('comp1', '--unmodified');

helper.command.resetAll();
});
it('expect .bitmap to point to the same version as it was before the reset, and not the latest', () => {
Expand Down
48 changes: 48 additions & 0 deletions e2e/harmony/lanes/merge-lanes.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { uniq } from 'lodash';
import { DEFAULT_LANE } from '@teambit/lane-id';
import { Extensions, statusWorkspaceIsCleanMsg } from '@teambit/legacy.constants';
import { Helper, fixtures } from '@teambit/legacy.e2e-helper';
import { DETACH_HEAD } from '@teambit/harmony.modules.feature-toggle';

chai.use(require('chai-fs'));

Expand Down Expand Up @@ -1699,4 +1700,51 @@ describe('merge lanes', function () {
expect(() => helper.command.mergeLane('main', '--no-snap -x')).to.not.throw();
});
});
describe('merge with --detach-head', () => {
let commonSnap: string;
let headOnMain: string;
let firstSnapOnLane: string;
let headOnLane: string;
before(() => {
helper.scopeHelper.setNewLocalAndRemoteScopes();
helper.command.setFeatures(DETACH_HEAD);
helper.fixtures.populateComponents(2);
helper.command.tagAllWithoutBuild('--ver 1.0.0');
commonSnap = helper.command.getHead('comp1');
helper.command.tagAllWithoutBuild('--unmodified --ver 2.0.0');
helper.command.export();
headOnMain = helper.command.getHead('comp1');
helper.command.checkoutVersion('1.0.0', "'**' -x");
helper.command.createLane();
helper.command.snapAllComponentsWithoutBuild('--unmodified');
firstSnapOnLane = helper.command.getHeadOfLane('dev', 'comp1');
helper.command.snapAllComponentsWithoutBuild('--unmodified');
headOnLane = helper.command.getHeadOfLane('dev', 'comp1');
helper.command.export();
helper.command.switchLocalLane('main', '-x');
helper.command.mergeLane('dev', '--detach-head -x');
});
after(() => {
helper.command.resetFeatures();
});
it('should not change the head', () => {
const head = helper.command.getHead('comp1');
expect(head).to.equal(headOnMain);
expect(head).to.not.equal(headOnLane);
});
it('should save the detached head', () => {
const comp = helper.command.catComponent('comp1');
expect(comp.detachedHeads.current).to.equal(headOnLane);
});
it('should continue the history from the common snap, not from the head', () => {
const laneHeadVer = helper.command.catObject(headOnLane, true);
expect(laneHeadVer.parents).to.have.lengthOf(1);
expect(laneHeadVer.parents[0]).to.equal(commonSnap);
expect(laneHeadVer.parents[0]).to.not.equal(headOnMain);
});
it('should squash successfully', () => {
const laneHeadVer = helper.command.catObject(headOnLane, true);
expect(laneHeadVer.squashed.previousParents[0]).to.equal(firstSnapOnLane);
});
});
});
2 changes: 1 addition & 1 deletion e2e/harmony/tag-from-scope.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ describe('tag components on Harmony', function () {
},
];
// console.log('data', JSON.stringify(data));
helper.command.tagFromScope(bareTag.scopePath, data, '--push --ignore-newest-version');
helper.command.tagFromScope(bareTag.scopePath, data, '--push --override-head');
});
it('should tag and export with no errors and should set the parent to the previous head', () => {
const compOnRemote = helper.command.catComponent(
Expand Down
40 changes: 40 additions & 0 deletions e2e/harmony/tag-harmony.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { uniq } from 'lodash';
import { Extensions } from '@teambit/legacy.constants';
import { SchemaName } from '@teambit/legacy.consumer-component';
import { Helper } from '@teambit/legacy.e2e-helper';
import { DETACH_HEAD } from '@teambit/harmony.modules.feature-toggle';

chai.use(require('chai-fs'));

Expand Down Expand Up @@ -472,4 +473,43 @@ describe('tag components on Harmony', function () {
expect(tagOutput).to.have.string('[email protected]');
});
});
describe('maintain two main branches 1.x and 2.x, tagging the older branch 1.x with a patch', () => {
let ver2Head: string;
before(() => {
helper.scopeHelper.setNewLocalAndRemoteScopes();
helper.command.setFeatures(DETACH_HEAD);
helper.fixtures.populateComponents(1);
helper.command.tagAllWithoutBuild('--ver 1.0.0');
helper.fixtures.populateComponents(1, undefined, 'version2');
helper.command.tagAllWithoutBuild('--ver 2.0.0');
ver2Head = helper.command.getHead('comp1');
helper.command.export();
helper.command.checkoutVersion('1.0.0', 'comp1', '-x');
helper.fixtures.populateComponents(1, undefined, 'version101');
helper.command.tagAllWithoutBuild('--ver 1.0.1 --detach-head');
helper.command.export();
});
after(() => {
helper.command.resetFeatures();
});
it('should keep the head as 2.x and not change it to 1.0.1', () => {
const comp = helper.command.catComponent('comp1');
expect(comp.head).to.equal(ver2Head);
});
it('should update the .bitmap according to the patch version and not the head', () => {
const bitmap = helper.bitMap.read();
expect(bitmap.comp1.version).to.equal('1.0.1');
});
describe('importing the component to a new workspace', () => {
before(() => {
helper.scopeHelper.reInitLocalScope();
helper.scopeHelper.addRemoteScope();
helper.command.importComponent('comp1', '-x');
});
it('should import the latest: 2.x and not the patch 1.01', () => {
const bitmap = helper.bitMap.read();
expect(bitmap.comp1.version).to.equal('2.0.0');
});
});
});
});
12 changes: 12 additions & 0 deletions scopes/component/merging/merge-status-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type MergeStatusProviderOptions = {
ignoreConfigChanges?: boolean;
shouldSquash?: boolean;
handleTargetAheadAsDiverged?: boolean;
detachHead?: boolean;
};

export const compIsAlreadyMergedMsg = 'component is already merged';
Expand Down Expand Up @@ -300,6 +301,17 @@ other: ${otherLaneHead.toString()}`);
divergeData
);
}
if (this.options.detachHead && divergeData.commonSnapBeforeDiverge) {
// just override with the model data
const commonSnapId = id.changeVersion(divergeData.commonSnapBeforeDiverge.toString());
const commonSnapComp = await this.scope.legacyScope.getConsumerComponent(commonSnapId);
return {
...componentStatus,
currentComponent: commonSnapComp,
componentFromModel: componentOnOther,
divergeData,
};
}
if (!divergeData.isDiverged()) {
if (divergeData.isSourceAhead()) {
// component is ahead nothing to merge.
Expand Down
21 changes: 16 additions & 5 deletions scopes/component/merging/merging.main.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export class MergingMain {
snapMessage,
build,
skipDependencyInstallation,
detachHead,
}: {
mergeStrategy: MergeStrategy;
allComponentsStatus: ComponentMergeStatus[];
Expand All @@ -228,6 +229,7 @@ export class MergingMain {
snapMessage?: string;
build?: boolean;
skipDependencyInstallation?: boolean;
detachHead?: boolean;
}): Promise<ApplyVersionResults> {
const consumer = this.workspace?.consumer;
const legacyScope = this.scope.legacyScope;
Expand Down Expand Up @@ -258,7 +260,8 @@ export class MergingMain {
succeededComponents,
otherLaneId,
mergeStrategy,
currentLane
currentLane,
detachHead
);

const allConfigMerge = compact(succeededComponents.map((c) => c.configMergeResult));
Expand Down Expand Up @@ -397,7 +400,8 @@ export class MergingMain {
succeededComponents: ComponentMergeStatus[],
otherLaneId: LaneId,
mergeStrategy: MergeStrategy,
currentLane?: Lane
currentLane?: Lane,
detachHead?: boolean
): Promise<ApplyVersionWithComps[]> {
const componentsResults = await mapSeries(
succeededComponents,
Expand All @@ -414,6 +418,7 @@ export class MergingMain {
currentLane,
resolvedUnrelated,
configMergeResult,
detachHead,
});
}
);
Expand Down Expand Up @@ -443,6 +448,7 @@ export class MergingMain {
currentLane,
resolvedUnrelated,
configMergeResult,
detachHead,
}: {
currentComponent: ConsumerComponent | null | undefined;
id: ComponentID;
Expand All @@ -453,6 +459,7 @@ export class MergingMain {
currentLane?: Lane;
resolvedUnrelated?: ResolveUnrelatedData;
configMergeResult?: ConfigMergeResult;
detachHead?: boolean;
}): Promise<ApplyVersionWithComps> {
const legacyScope = this.scope.legacyScope;
let filesStatus = {};
Expand Down Expand Up @@ -549,9 +556,13 @@ export class MergingMain {
addToCurrentLane(remoteHead);
} else {
// this is main
modelComponent.setHead(remoteHead);
// mark it as local, otherwise, when importing this component from a remote, it'll override it.
modelComponent.markVersionAsLocal(remoteHead.toString());
if (detachHead) {
modelComponent.detachedHeads.setHead(remoteHead);
} else {
modelComponent.setHead(remoteHead);
// mark it as local, otherwise, when importing this component from a remote, it'll override it.
modelComponent.markVersionAsLocal(remoteHead.toString());
}
legacyScope.objects.add(modelComponent);
}

Expand Down
2 changes: 1 addition & 1 deletion scopes/component/snap-distance/get-diverge-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function getDivergeData({
throwForNoCommonSnap?: boolean;
versionParentsFromObjects?: VersionParents[];
}): Promise<SnapsDistance> {
const isOnLane = modelComponent.laneHeadLocal || modelComponent.laneHeadLocal === null;
const isOnLane = modelComponent.isOnLane();
const localHead = sourceHead || (isOnLane ? modelComponent.laneHeadLocal : modelComponent.getHead());
// uncomment the following line to debug diverge-data issues.
// if (modelComponent.name === 'x') console.log('getDivergeData, localHead', localHead, 'targetHead', targetHead);
Expand Down
Loading