Skip to content

Commit 14d1ad2

Browse files
authored
improvement(lanes): fetch updates from main when on a lane (teambit#6158)
- when running `bit import`, fetch lane-components not only from lane-scope but also from original scope. - when running `bit status`, show how many snaps the main is ahead. - when running `bit status` and there are issues getting the diverge-data, show them to the user. - improve `bit status --json` to show the staged versions. - fix components with multiple parents in the history to show the staged versions correctly.
1 parent f0f8ff8 commit 14d1ad2

21 files changed

Lines changed: 267 additions & 68 deletions

File tree

e2e/flows/id-with-wildcard.e2e.2.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,12 @@ describe('component id with wildcard', function () {
174174
expect(ls).to.have.lengthOf(2);
175175
});
176176
it('should not export the non matched components', () => {
177-
const status = helper.command.statusJson();
177+
const staged = helper.command.getStagedIdsFromStatus();
178178
// (staged components were not exported)
179-
expect(status.stagedComponents).to.have.lengthOf(3);
180-
expect(status.stagedComponents).to.include('bar/foo');
181-
expect(status.stagedComponents).to.include('utils/is/string');
182-
expect(status.stagedComponents).to.include('utils/is/type');
179+
expect(staged).to.have.lengthOf(3);
180+
expect(staged).to.include('bar/foo');
181+
expect(staged).to.include('utils/is/string');
182+
expect(staged).to.include('utils/is/type');
183183
});
184184
});
185185
});

e2e/functionalities/auto-tagging.e2e.2.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ describe('auto tagging functionality', function () {
5959
it('bit-status should show them all as staged and not modified', () => {
6060
const status = helper.command.statusJson();
6161
expect(status.modifiedComponent).to.be.empty;
62-
expect(status.stagedComponents).to.include('comp1');
63-
expect(status.stagedComponents).to.include('comp2');
64-
expect(status.stagedComponents).to.include('comp3');
62+
const staged = helper.command.getStagedIdsFromStatus();
63+
expect(staged).to.include('comp1');
64+
expect(staged).to.include('comp2');
65+
expect(staged).to.include('comp3');
6566
});
6667
describe('with --skip-auto-tag', () => {
6768
before(() => {

e2e/harmony/lanes/lanes.e2e.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,8 @@ describe('bit lane command', function () {
362362
// main components belong to lane-a only if they are snapped on lane-a, so utils/is-type
363363
// doesn't belong to lane-a and should not appear as staged when on lane-a.
364364
it('bit status should not show neither lane-b nor main components as staged', () => {
365-
const statusParsed = helper.command.statusJson();
366-
expect(statusParsed.stagedComponents).to.deep.equal(['utils/is-string']);
365+
const staged = helper.command.getStagedIdsFromStatus();
366+
expect(staged).to.deep.equal(['utils/is-string']);
367367
const status = helper.command.status();
368368
expect(status).to.not.have.string('bar/foo');
369369
});
@@ -383,8 +383,8 @@ describe('bit lane command', function () {
383383
expect(list).to.have.lengthOf(1);
384384
});
385385
it('bit status should show only main components as staged', () => {
386-
const statusParsed = helper.command.statusJson();
387-
expect(statusParsed.stagedComponents).to.deep.equal(['utils/is-type']);
386+
const staged = helper.command.getStagedIdsFromStatus();
387+
expect(staged).to.deep.equal(['utils/is-type']);
388388
const status = helper.command.status();
389389
expect(status).to.not.have.string('bar/foo');
390390
expect(status).to.not.have.string('utils/is-string');
@@ -401,8 +401,8 @@ describe('bit lane command', function () {
401401
expect(list).to.have.lengthOf(1);
402402
});
403403
it('bit status should show only main components as staged', () => {
404-
const statusParsed = helper.command.statusJson();
405-
expect(statusParsed.stagedComponents).to.deep.equal(['utils/is-type']);
404+
const staged = helper.command.getStagedIdsFromStatus();
405+
expect(staged).to.deep.equal(['utils/is-type']);
406406
const status = helper.command.status();
407407
expect(status).to.not.have.string('bar/foo');
408408
expect(status).to.not.have.string('utils/is-string');
@@ -545,9 +545,10 @@ describe('bit lane command', function () {
545545
it('bit-status should show them all as staged and not modified', () => {
546546
const status = helper.command.statusJson();
547547
expect(status.modifiedComponent).to.be.empty;
548-
expect(status.stagedComponents).to.include('comp1');
549-
expect(status.stagedComponents).to.include('comp2');
550-
expect(status.stagedComponents).to.include('comp3');
548+
const staged = helper.command.getStagedIdsFromStatus();
549+
expect(staged).to.include('comp1');
550+
expect(staged).to.include('comp2');
551+
expect(staged).to.include('comp3');
551552
});
552553
// @todo
553554
describe.skip('importing the component to another scope', () => {

e2e/harmony/lanes/merge-lanes.e2e.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,51 @@ describe('merge lanes', function () {
306306
});
307307
});
308308
});
309+
describe('getting updates from main when lane is diverge', () => {
310+
let workspaceOnLane: string;
311+
let comp2HeadOnMain: string;
312+
before(() => {
313+
helper.scopeHelper.setNewLocalAndRemoteScopesHarmony();
314+
helper.bitJsonc.setupDefault();
315+
helper.fixtures.populateComponents(2);
316+
helper.command.tagAllWithoutBuild();
317+
helper.command.export();
318+
helper.command.createLane();
319+
helper.fixtures.populateComponents(2, undefined, 'v2');
320+
helper.command.snapAllComponentsWithoutBuild();
321+
helper.command.export();
322+
workspaceOnLane = helper.scopeHelper.cloneLocalScope();
323+
helper.command.switchLocalLane('main');
324+
helper.fixtures.populateComponents(2, undefined, 'v3');
325+
helper.command.snapAllComponentsWithoutBuild();
326+
comp2HeadOnMain = helper.command.getHead(`${helper.scopes.remote}/comp2`);
327+
helper.command.export();
328+
helper.scopeHelper.getClonedLocalScope(workspaceOnLane);
329+
helper.command.import();
330+
});
331+
it('bit import should bring the latest main objects', () => {
332+
const head = helper.command.getHead(`${helper.scopes.remote}/comp2`);
333+
expect(head).to.equal(comp2HeadOnMain);
334+
});
335+
it('bit status should indicate that the main is ahead', () => {
336+
const status = helper.command.status();
337+
expect(status).to.have.string(`${helper.scopes.remote}/comp1 ... main is ahead by 1 snaps`);
338+
});
339+
describe('merging the lane', () => {
340+
let status;
341+
before(() => {
342+
helper.command.mergeLane('main', '--theirs');
343+
status = helper.command.statusJson();
344+
});
345+
it('bit status should show two staging versions, the main-head and merge-snap', () => {
346+
const stagedVersions = status.stagedComponents.find((c) => c.id === `${helper.scopes.remote}/comp2`);
347+
expect(stagedVersions.versions).to.have.lengthOf(2);
348+
expect(stagedVersions.versions).to.include(comp2HeadOnMain);
349+
expect(stagedVersions.versions).to.include(helper.command.getHeadOfLane('dev', 'comp2'));
350+
});
351+
it('bit status should not show the components in pending-merge', () => {
352+
expect(status.mergePendingComponents).to.have.lengthOf(0);
353+
});
354+
});
355+
});
309356
});

e2e/harmony/snap.e2e.2.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -629,9 +629,10 @@ describe('bit snap command', function () {
629629
it('bit-status should show them all as staged and not modified', () => {
630630
const status = helper.command.statusJson();
631631
expect(status.modifiedComponent).to.be.empty;
632-
expect(status.stagedComponents).to.include('comp1');
633-
expect(status.stagedComponents).to.include('comp2');
634-
expect(status.stagedComponents).to.include('comp3');
632+
const staged = helper.command.getStagedIdsFromStatus();
633+
expect(staged).to.include('comp1');
634+
expect(staged).to.include('comp2');
635+
expect(staged).to.include('comp3');
635636
});
636637
describe('importing the component to another scope', () => {
637638
before(() => {

scopes/component/status/status-cmd.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import R from 'ramda';
33
import { Command, CommandOptions } from '@teambit/cli';
44
import { BitId } from '@teambit/legacy-bit-id';
55
import Component from '@teambit/legacy/dist/consumer/component';
6+
import { DivergeData } from '@teambit/legacy/dist/scope/component-ops/diverge-data';
67
import { immutableUnshift } from '@teambit/legacy/dist/utils';
78
import { formatBitString, formatNewBit } from '@teambit/legacy/dist/cli/chalk-box';
89
import { getInvalidComponentLabel, formatIssues } from '@teambit/legacy/dist/cli/templates/component-issues-template';
@@ -55,11 +56,12 @@ export class StatusCmd implements Command {
5556
componentsWithIndividualFiles,
5657
softTaggedComponents,
5758
snappedComponents,
59+
pendingUpdatesFromMain,
5860
}: StatusResult = await this.status.status();
5961
return {
6062
newComponents,
6163
modifiedComponent: modifiedComponent.map((c) => c.id.toString()),
62-
stagedComponents: stagedComponents.map((c) => c.id()),
64+
stagedComponents: stagedComponents.map((c) => ({ id: c.id(), versions: c.getLocalTagsOrHashes() })),
6365
componentsWithIssues: componentsWithIssues.map((c) => ({
6466
id: c.id.toString(),
6567
issues: c.issues?.toObject(),
@@ -73,6 +75,7 @@ export class StatusCmd implements Command {
7375
componentsWithIndividualFiles: componentsWithIndividualFiles.map((c) => c.id.toString()),
7476
softTaggedComponents: softTaggedComponents.map((s) => s.toString()),
7577
snappedComponents: snappedComponents.map((s) => s.toString()),
78+
pendingUpdatesFromMain: pendingUpdatesFromMain.map((p) => ({ id: p.id.toString(), divergeData: p.divergeData })),
7679
};
7780
}
7881

@@ -91,6 +94,7 @@ export class StatusCmd implements Command {
9194
componentsWithIndividualFiles,
9295
softTaggedComponents,
9396
snappedComponents,
97+
pendingUpdatesFromMain,
9498
laneName,
9599
}: StatusResult = await this.status.status();
96100
// If there is problem with at least one component we want to show a link to the
@@ -229,13 +233,30 @@ or use "bit merge [component-id] --abort" to cancel the merge operation)\n`;
229233
stagedComponents.length ? chalk.underline.white('staged components') + stagedDesc : ''
230234
).join('\n');
231235

232-
const snappedDesc = '\n(use "bit tag --all [version]" or "bit tag --snapped [version]" to lock a version)\n';
236+
const snappedDesc = '\n(use "bit tag [version]" or "bit tag --snapped [version]" to lock a version)\n';
233237
const snappedComponentsOutput = immutableUnshift(
234238
// @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
235239
snappedComponents.map((c) => format(c, true)),
236240
snappedComponents.length ? chalk.underline.white('snapped components') + snappedDesc : ''
237241
).join('\n');
238242

243+
const getUpdateFromMainMsg = (divergeData: DivergeData): string => {
244+
if (divergeData.err) return divergeData.err.message;
245+
let msg = `main is ahead by ${divergeData.snapsOnRemoteOnly.length || 0} snaps`;
246+
if (divergeData.snapsOnLocalOnly && verbose) {
247+
msg += ` (diverged since ${divergeData.commonSnapBeforeDiverge?.toShortString()})`;
248+
}
249+
return msg;
250+
};
251+
const updatesFromMainDesc = '\n(EXPERIMENTAL. use "bit lane merge main" to merge the changes)\n';
252+
const pendingUpdatesFromMainIds = pendingUpdatesFromMain.map((c) =>
253+
format(c.id, true, getUpdateFromMainMsg(c.divergeData))
254+
);
255+
const updatesFromMainOutput = immutableUnshift(
256+
pendingUpdatesFromMainIds,
257+
pendingUpdatesFromMain.length ? chalk.underline.white('pending updates from main') + updatesFromMainDesc : ''
258+
).join('\n');
259+
239260
const laneStr = laneName ? `\non ${chalk.bold(laneName)} lane` : '';
240261

241262
const troubleshootingStr = showTroubleshootingLink ? `\n${TROUBLESHOOTING_MESSAGE}` : '';
@@ -245,6 +266,7 @@ or use "bit merge [component-id] --abort" to cancel the merge operation)\n`;
245266
[
246267
outdatedStr,
247268
pendingMergeStr,
269+
updatesFromMainOutput,
248270
compWithConflictsStr,
249271
newComponentsOutput,
250272
modifiedComponentOutput,

scopes/component/status/status.main.runtime.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import loader from '@teambit/legacy/dist/cli/loader';
77
import { BEFORE_STATUS } from '@teambit/legacy/dist/cli/loader/loader-messages';
88
import ConsumerComponent from '@teambit/legacy/dist/consumer/component';
99
import ComponentsPendingImport from '@teambit/legacy/dist/consumer/component-ops/exceptions/components-pending-import';
10-
import ComponentsList, { DivergedComponent } from '@teambit/legacy/dist/consumer/component/components-list';
10+
import ComponentsList, {
11+
DivergeDataPerId,
12+
DivergedComponent,
13+
} from '@teambit/legacy/dist/consumer/component/components-list';
1114
import { InvalidComponent } from '@teambit/legacy/dist/consumer/component/consumer-component';
1215
import { ModelComponent } from '@teambit/legacy/dist/scope/models';
1316
import { ConsumerNotFound } from '@teambit/legacy/dist/consumer/exceptions';
@@ -30,6 +33,7 @@ export type StatusResult = {
3033
componentsWithIndividualFiles: ConsumerComponent[];
3134
softTaggedComponents: BitId[];
3235
snappedComponents: BitId[];
36+
pendingUpdatesFromMain: DivergeDataPerId[];
3337
laneName: string | null; // null if default
3438
};
3539

@@ -72,6 +76,7 @@ export class StatusMain {
7276
const componentsDuringMergeState = componentsList.listDuringMergeStateComponents();
7377
const softTaggedComponents = componentsList.listSoftTaggedComponents();
7478
const snappedComponents = (await componentsList.listSnappedComponentsOnMain()).map((c) => c.toBitId());
79+
const pendingUpdatesFromMain = await componentsList.listUpdatesFromMainPending();
7580
const currentLane = consumer.getCurrentLaneId();
7681
const laneName = currentLane.isDefault() ? null : currentLane.name;
7782
Analytics.setExtraData('new_components', newComponents.length);
@@ -96,6 +101,7 @@ export class StatusMain {
96101
componentsWithIndividualFiles: await componentsList.listComponentsWithIndividualFiles(),
97102
softTaggedComponents,
98103
snappedComponents,
104+
pendingUpdatesFromMain,
99105
laneName,
100106
};
101107
}

scopes/lanes/lanes/lanes.docs.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ Summary of when/what lanes data is saved per command:
8787
- in case the merge wasn't done and the user is trying to export, the remote blocks is as it finds out that its head doesn't exist on the incoming component.
8888
- in case the snap-merge failed due to a conflict, it saves the conflict and heads data into `.bit/unmerged.json` file.
8989

90+
### Merge components
91+
92+
- to merge an entire lane, use `bit lane merge`.
93+
- to get updates from a remote lane, run `bit import`, and then `bit checkout head`. alternatively, run `bit merge <component-id>` in one command.
94+
- to merge an individual component from a lane to main, run `bit lane merge <lane> --pattern <component-id>` when on main.
95+
9096
### Useful APIs
9197

9298
- bit-map `getAllIdsAvailableOnLane()` filters the currently checked out lane.

scopes/lanes/lanes/merge-lanes.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export async function mergeLanes({
3636
const consumer = workspace.consumer;
3737
const currentLaneId = consumer.getCurrentLaneId();
3838
if (!remoteName && laneName === currentLaneId.name) {
39-
throw new BitError(`unable to switch to lane "${laneName}", you're already checked out to this lane`);
39+
throw new BitError(
40+
`unable to merge lane "${laneName}", you're already at this lane. to get updates, simply run "bit checkout head"`
41+
);
4042
}
4143
const parsedLaneId = await consumer.getParsedLaneId(laneName);
4244
const localLane = currentLaneId.isDefault() ? null : await consumer.scope.loadLane(currentLaneId);
@@ -47,7 +49,8 @@ export async function mergeLanes({
4749
const isDefaultLane = laneName === DEFAULT_LANE;
4850

4951
if (isDefaultLane) {
50-
bitIds = consumer.bitMap.getAuthoredAndImportedBitIdsOfDefaultLane().filter((id) => id.hasVersion());
52+
if (!localLane) throw new Error(`unable to merge ${DEFAULT_LANE}, the current lane was not found`);
53+
bitIds = await consumer.scope.getDefaultLaneIdsFromLane(localLane);
5154
otherLaneName = DEFAULT_LANE;
5255
} else if (remoteName) {
5356
const remoteLaneId = LaneId.from(parsedLaneId.name, remoteName);

src/api/scope/lib/fetch.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type FETCH_OPTIONS = {
2323
allowExternal: boolean; // allow fetching components of other scope from this scope. needed for lanes.
2424
laneId?: string; // relevant for "component-delta" type to know where to find the component latest
2525
onlyIfBuilt?: boolean; // relevant when fetching with deps. if true, and the component wasn't built successfully, don't fetch it.
26+
ignoreMissingHead?: boolean; // if asking for id without version and the component has no head, don't throw, just ignore
2627
};
2728

2829
const HooksManagerInstance = HooksManager.getInstance();
@@ -58,7 +59,11 @@ export default async function fetch(
5859
const scopeComponentsImporter = ScopeComponentsImporter.getInstance(scope);
5960
const getComponentsWithOptions = async (): Promise<ComponentWithCollectOptions[]> => {
6061
if (withoutDependencies) {
61-
const componentsVersions = await scopeComponentsImporter.fetchWithoutDeps(bitIds, allowExternal);
62+
const componentsVersions = await scopeComponentsImporter.fetchWithoutDeps(
63+
bitIds,
64+
allowExternal,
65+
fetchOptions.ignoreMissingHead
66+
);
6267
return componentsVersions.map((compVersion) => ({
6368
component: compVersion.component,
6469
version: compVersion.version,
@@ -97,7 +102,8 @@ export default async function fetch(
97102
const bitIdsLatest = bitIdsToLatest(bitIdsWithHashToStop, lane);
98103
const importedComponents = await scopeComponentsImporter.fetchWithoutDeps(
99104
bitIdsLatest,
100-
fetchOptions.allowExternal
105+
fetchOptions.allowExternal,
106+
fetchOptions.ignoreMissingHead
101107
);
102108
const componentsWithOptions: ComponentWithCollectOptions[] = importedComponents.map((compVersion) => {
103109
const hashToStop = bitIdsWithHashToStop.searchWithoutVersion(compVersion.id)?.version;

0 commit comments

Comments
 (0)