diff --git a/e2e/harmony/lanes/diverged-from-forked.e2e.ts b/e2e/harmony/lanes/diverged-from-forked.e2e.ts new file mode 100644 index 000000000000..72a13be1a069 --- /dev/null +++ b/e2e/harmony/lanes/diverged-from-forked.e2e.ts @@ -0,0 +1,32 @@ +import chai, { expect } from 'chai'; +import Helper from '../../../src/e2e-helper/e2e-helper'; + +chai.use(require('chai-fs')); + +describe('lane-b was forked from lane-a and they are now diverged', function () { + this.timeout(0); + let helper: Helper; + before(() => { + helper = new Helper(); + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.bitJsonc.setupDefault(); + helper.command.createLane('lane-a'); + helper.fixtures.populateComponents(1, false); + helper.command.snapAllComponentsWithoutBuild(); + helper.command.export(); + helper.command.createLane('lane-b'); + helper.command.snapAllComponentsWithoutBuild('--unmodified'); + helper.command.export(); + helper.command.switchLocalLane('lane-a'); + helper.command.snapAllComponentsWithoutBuild('--unmodified'); + helper.command.export(); + helper.command.switchLocalLane('lane-b'); + }); + it('bit status should have the diverged component in the updatesFromForked section', () => { + const status = helper.command.statusJson(); + expect(status.updatesFromForked).to.have.lengthOf(1); + }); + after(() => { + helper.scopeHelper.destroy(); + }); +}); diff --git a/package.json b/package.json index bb9c598cd806..316133ec0f54 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,6 @@ }, "dependencies": { "@teambit/defender.fs.global-bit-temp-dir": "0.0.1", - "@teambit/component-issues": "0.0.72", "@teambit/toolbox.network.agent": "0.0.116", "@babel/core": "7.12.17", "@babel/runtime": "7.12.18", diff --git a/scopes/component/status/status-cmd.ts b/scopes/component/status/status-cmd.ts index d2e1a076104e..551ac10bb807 100644 --- a/scopes/component/status/status-cmd.ts +++ b/scopes/component/status/status-cmd.ts @@ -14,7 +14,7 @@ import { statusInvalidComponentsMsg, statusWorkspaceIsCleanMsg, } from '@teambit/legacy/dist/constants'; -import { partition } from 'lodash'; +import { compact, partition } from 'lodash'; import { isHash } from '@teambit/component-version'; import { StatusMain, StatusResult } from './status.main.runtime'; @@ -30,7 +30,11 @@ export class StatusCmd implements Command { alias = 's'; options = [ ['j', 'json', 'return a json version of the component'], - ['', 'verbose', 'show full snap hashes'], + [ + '', + 'verbose', + 'show extra data: full snap hashes for staged, divergence point for lanes and updates-from-main for forked lanes', + ], ['', 'strict', 'in case issues found, exit with code 1'], ] as CommandOptions; loader = true; @@ -41,7 +45,7 @@ export class StatusCmd implements Command { async json() { const { newComponents, - modifiedComponent, + modifiedComponents, stagedComponents, componentsWithIssues, importPendingComponents, @@ -55,10 +59,13 @@ export class StatusCmd implements Command { softTaggedComponents, snappedComponents, pendingUpdatesFromMain, + updatesFromForked, + currentLaneId, + forkedLaneId, }: StatusResult = await this.status.status(); return { newComponents: newComponents.map((c) => c.toStringWithoutVersion()), - modifiedComponents: modifiedComponent.map((c) => c.toString()), + modifiedComponents: modifiedComponents.map((c) => c.toString()), stagedComponents: stagedComponents.map((c) => ({ id: c.id.toStringWithoutVersion(), versions: c.versions })), componentsWithIssues: componentsWithIssues.map((c) => ({ id: c.id.toString(), @@ -75,13 +82,19 @@ export class StatusCmd implements Command { softTaggedComponents: softTaggedComponents.map((s) => s.toString()), snappedComponents: snappedComponents.map((s) => s.toString()), pendingUpdatesFromMain: pendingUpdatesFromMain.map((p) => ({ id: p.id.toString(), divergeData: p.divergeData })), + updatesFromForked: updatesFromForked.map((p) => ({ + id: p.id.toString(), + divergeData: p.divergeData, + })), + currentLaneId, + forkedLaneId, }; } async report(_args, { strict, verbose }: { strict?: boolean; verbose?: boolean }) { const { newComponents, - modifiedComponent, + modifiedComponents, stagedComponents, componentsWithIssues, importPendingComponents, @@ -95,7 +108,9 @@ export class StatusCmd implements Command { softTaggedComponents, snappedComponents, pendingUpdatesFromMain, - laneName, + updatesFromForked, + currentLaneId, + forkedLaneId, }: StatusResult = await this.status.status(); // If there is problem with at least one component we want to show a link to the // troubleshooting doc @@ -189,8 +204,8 @@ or use "bit merge [component-id] --abort" to cancel the merge operation)\n`; const modifiedDesc = '(use "bit diff" to compare changes)\n'; const modifiedComponentOutput = immutableUnshift( - modifiedComponent.map((c) => format(c, true)), - modifiedComponent.length + modifiedComponents.map((c) => format(c, true)), + modifiedComponents.length ? chalk.underline.white('modified components') + newComponentDescription + modifiedDesc : '' ).join('\n'); @@ -234,33 +249,53 @@ or use "bit merge [component-id] --abort" to cancel the merge operation)\n`; snappedComponents.length ? chalk.underline.white('snapped components') + snappedDesc : '' ).join('\n'); - const getUpdateFromMainMsg = (divergeData: DivergeData): string => { + const getUpdateFromMsg = (divergeData: DivergeData, from = 'main'): string => { if (divergeData.err) return divergeData.err.message; - let msg = `main is ahead by ${divergeData.snapsOnRemoteOnly.length || 0} snaps`; + let msg = `${from} is ahead by ${divergeData.snapsOnRemoteOnly.length || 0} snaps`; if (divergeData.snapsOnLocalOnly && verbose) { msg += ` (diverged since ${divergeData.commonSnapBeforeDiverge?.toShortString()})`; } return msg; }; - const updatesFromMainDesc = '\n(EXPERIMENTAL. use "bit lane merge main" to merge the changes)\n'; - const pendingUpdatesFromMainIds = pendingUpdatesFromMain.map((c) => - format(c.id, false, getUpdateFromMainMsg(c.divergeData)) - ); - const updatesFromMainOutput = immutableUnshift( - pendingUpdatesFromMainIds, - pendingUpdatesFromMain.length ? chalk.underline.white('pending updates from main') + updatesFromMainDesc : '' - ).join('\n'); - const laneStr = laneName ? `\non ${chalk.bold(laneName)} lane` : ''; + let updatesFromMainOutput = ''; + if (!forkedLaneId || verbose) { + const updatesFromMainDesc = '\n(use "bit lane merge main" to merge the changes)\n'; + const pendingUpdatesFromMainIds = pendingUpdatesFromMain.map((c) => + format(c.id, false, getUpdateFromMsg(c.divergeData)) + ); + updatesFromMainOutput = [ + pendingUpdatesFromMain.length ? chalk.underline.white('pending updates from main') + updatesFromMainDesc : '', + ...pendingUpdatesFromMainIds, + ].join('\n'); + } + + let updatesFromForkedOutput = ''; + if (forkedLaneId) { + const updatesFromForkedDesc = `\n(use "bit lane merge ${forkedLaneId.toString()}" to merge the changes +use "bit fetch ${forkedLaneId.toString()} --lanes" to update ${forkedLaneId.name} locally)\n`; + const pendingUpdatesFromForkedIds = updatesFromForked.map((c) => + format(c.id, false, getUpdateFromMsg(c.divergeData, forkedLaneId.name)) + ); + updatesFromForkedOutput = [ + updatesFromForked.length + ? chalk.underline.white(`updates from ${forkedLaneId.name}`) + updatesFromForkedDesc + : '', + ...pendingUpdatesFromForkedIds, + ].join('\n'); + } + + const laneStr = currentLaneId.isDefault() ? '' : `\non ${chalk.bold(currentLaneId.toString())} lane`; const troubleshootingStr = showTroubleshootingLink ? `\n${TROUBLESHOOTING_MESSAGE}` : ''; const statusMsg = importPendingWarning + - [ + compact([ outdatedStr, pendingMergeStr, updatesFromMainOutput, + updatesFromForkedOutput, compDuringMergeStr, newComponentsOutput, modifiedComponentOutput, @@ -270,9 +305,7 @@ or use "bit merge [component-id] --abort" to cancel the merge operation)\n`; invalidComponentOutput, locallySoftRemovedOutput, remotelySoftRemovedOutput, - ] - .filter((x) => x) - .join(chalk.underline('\n \n') + chalk.white('\n')) + + ]).join(chalk.underline('\n \n') + chalk.white('\n')) + troubleshootingStr; const results = (statusMsg || chalk.yellow(statusWorkspaceIsCleanMsg)) + laneStr; diff --git a/scopes/component/status/status.main.runtime.ts b/scopes/component/status/status.main.runtime.ts index d35b6f6fad59..04020adf1278 100644 --- a/scopes/component/status/status.main.runtime.ts +++ b/scopes/component/status/status.main.runtime.ts @@ -1,5 +1,6 @@ import { CLIAspect, CLIMain, MainRuntime } from '@teambit/cli'; import pMapSeries from 'p-map-series'; +import { LaneId } from '@teambit/lane-id'; import { IssuesList } from '@teambit/component-issues'; import WorkspaceAspect, { Workspace } from '@teambit/workspace'; import { ComponentID } from '@teambit/component-id'; @@ -23,7 +24,7 @@ type DivergeDataPerId = { id: ComponentID; divergeData: DivergeData }; export type StatusResult = { newComponents: ComponentID[]; - modifiedComponent: ComponentID[]; + modifiedComponents: ComponentID[]; stagedComponents: { id: ComponentID; versions: string[] }[]; componentsWithIssues: { id: ComponentID; issues: IssuesList }[]; importPendingComponents: ComponentID[]; @@ -37,7 +38,9 @@ export type StatusResult = { softTaggedComponents: ComponentID[]; snappedComponents: ComponentID[]; pendingUpdatesFromMain: DivergeDataPerId[]; - laneName: string | null; // null if default + updatesFromForked: DivergeDataPerId[]; + currentLaneId: LaneId; + forkedLaneId?: LaneId; }; export class StatusMain { @@ -62,7 +65,7 @@ export class StatusMain { true, loadOpts )) as ConsumerComponent[]; - const modifiedComponent = (await componentsList.listModifiedComponents(true, loadOpts)) as ConsumerComponent[]; + const modifiedComponents = (await componentsList.listModifiedComponents(true, loadOpts)) as ConsumerComponent[]; const stagedComponents: ModelComponent[] = await componentsList.listExportPendingComponents(laneObj); await this.addRemovedStagedIfNeeded(stagedComponents); const stagedComponentsWithVersions = await pMapSeries(stagedComponents, async (stagedComp) => { @@ -87,7 +90,7 @@ export class StatusMain { const invalidComponents = allInvalidComponents.filter((c) => !(c.error instanceof ComponentsPendingImport)); const outdatedComponents = await componentsList.listOutdatedComponents(); const mergePendingComponents = await componentsList.listMergePendingComponents(); - const newAndModifiedLegacy: ConsumerComponent[] = newComponents.concat(modifiedComponent); + const newAndModifiedLegacy: ConsumerComponent[] = newComponents.concat(modifiedComponents); const issuesToIgnore = this.issues.getIssuesToIgnoreGlobally(); if (newAndModifiedLegacy.length) { const newAndModified = await this.workspace.getManyByLegacy(newAndModifiedLegacy, loadOpts); @@ -101,8 +104,10 @@ export class StatusMain { const softTaggedComponents = componentsList.listSoftTaggedComponents(); const snappedComponents = (await componentsList.listSnappedComponentsOnMain()).map((c) => c.toBitId()); const pendingUpdatesFromMain = await componentsList.listUpdatesFromMainPending(); - const currentLane = consumer.getCurrentLaneId(); - const laneName = currentLane.isDefault() ? null : currentLane.name; + const updatesFromForked = await componentsList.listUpdatesFromForked(); + const currentLaneId = consumer.getCurrentLaneId(); + const currentLane = await consumer.getCurrentLaneObject(); + const forkedLaneId = currentLane?.forkedFrom; Analytics.setExtraData('new_components', newComponents.length); Analytics.setExtraData('staged_components', stagedComponents.length); Analytics.setExtraData('num_components_with_missing_dependencies', componentsWithIssues.length); @@ -129,7 +134,7 @@ export class StatusMain { await consumer.onDestroy(); return { newComponents: await convertBitIdToComponentIdsAndSort(newComponents.map((c) => c.id)), - modifiedComponent: await convertBitIdToComponentIdsAndSort(modifiedComponent.map((c) => c.id)), + modifiedComponents: await convertBitIdToComponentIdsAndSort(modifiedComponents.map((c) => c.id)), stagedComponents: await convertObjToComponentIdsAndSort(stagedComponentsWithVersions), // @ts-ignore - not clear why, it fails the "bit build" without it componentsWithIssues: await convertObjToComponentIdsAndSort( @@ -155,7 +160,9 @@ export class StatusMain { softTaggedComponents: await convertBitIdToComponentIdsAndSort(softTaggedComponents), snappedComponents: await convertBitIdToComponentIdsAndSort(snappedComponents), pendingUpdatesFromMain: await convertObjToComponentIdsAndSort(pendingUpdatesFromMain), - laneName, + updatesFromForked: await convertObjToComponentIdsAndSort(updatesFromForked), + currentLaneId, + forkedLaneId, }; } diff --git a/src/consumer/component/components-list.ts b/src/consumer/component/components-list.ts index 02e54d54b2f4..56f39168effb 100644 --- a/src/consumer/component/components-list.ts +++ b/src/consumer/component/components-list.ts @@ -187,6 +187,54 @@ export default class ComponentsList { return compact(results); } + /** + * if the local lane was forked from another lane, this gets the differences between the two + */ + async listUpdatesFromForked(): Promise { + if (this.consumer.isOnMain()) { + return []; + } + const lane = await this.consumer.getCurrentLaneObject(); + const forkedFromLaneId = lane?.forkedFrom; + if (!forkedFromLaneId) { + return []; + } + const forkedFromLane = await this.scope.loadLane(forkedFromLaneId); + if (!forkedFromLane) return []; // should we fetch it here? + + const authoredAndImportedIds = this.bitMap.getAllBitIds(); + + const duringMergeIds = this.listDuringMergeStateComponents(); + + const componentsFromModel = await this.getModelComponents(); + const compFromModelOnWorkspace = componentsFromModel + .filter((c) => authoredAndImportedIds.hasWithoutVersion(c.toBitId())) + // if a component is merge-pending, it needs to be resolved first before getting more updates from main + .filter((c) => !duringMergeIds.hasWithoutVersion(c.toBitId())); + + const remoteForkedLane = await this.scope.objects.remoteLanes.getRemoteLane(forkedFromLaneId); + if (!remoteForkedLane.length) return []; + + const results = await Promise.all( + compFromModelOnWorkspace.map(async (modelComponent) => { + const headOnForked = remoteForkedLane.find((c) => c.id.isEqualWithoutVersion(modelComponent.toBitId())); + const headOnLane = modelComponent.laneHeadLocal; + if (!headOnForked || !headOnLane) return undefined; + const divergeData = await getDivergeData({ + repo: this.scope.objects, + modelComponent, + remoteHead: headOnForked.head, + checkedOutLocalHead: headOnLane, + throws: false, + }); + if (!divergeData.snapsOnRemoteOnly.length && !divergeData.err) return undefined; + return { id: modelComponent.toBitId(), divergeData }; + }) + ); + + return compact(results); + } + async listMergePendingComponents(loadOpts?: ComponentLoadOptions): Promise { if (!this._mergePendingComponents) { const componentsFromFs = await this.getComponentsFromFS(loadOpts); diff --git a/src/e2e-helper/e2e-command-helper.ts b/src/e2e-helper/e2e-command-helper.ts index 3c93281dfa4c..2a9f15322ab5 100644 --- a/src/e2e-helper/e2e-command-helper.ts +++ b/src/e2e-helper/e2e-command-helper.ts @@ -498,6 +498,7 @@ export default class CommandHelper { const statusJson = this.statusJson(); Object.keys(statusJson).forEach((key) => { if (exclude.includes(key)) return; + if (key === 'currentLaneId' || key === 'forkedLaneId') return; expect(statusJson[key], `status.${key} should be empty`).to.have.lengthOf(0); }); }