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

improvement(bit-status): add a new section showing updates from forked lane #6575

Merged
merged 7 commits into from
Oct 21, 2022
32 changes: 32 additions & 0 deletions e2e/harmony/lanes/diverged-from-forked.e2e.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
79 changes: 56 additions & 23 deletions scopes/component/status/status-cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand All @@ -41,7 +45,7 @@ export class StatusCmd implements Command {
async json() {
const {
newComponents,
modifiedComponent,
modifiedComponents,
stagedComponents,
componentsWithIssues,
importPendingComponents,
Expand All @@ -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(),
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
23 changes: 15 additions & 8 deletions scopes/component/status/status.main.runtime.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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[];
Expand All @@ -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 {
Expand All @@ -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) => {
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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(
Expand All @@ -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,
};
}

Expand Down
48 changes: 48 additions & 0 deletions src/consumer/component/components-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DivergeDataPerId[]> {
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<DivergedComponent[]> {
if (!this._mergePendingComponents) {
const componentsFromFs = await this.getComponentsFromFS(loadOpts);
Expand Down
1 change: 1 addition & 0 deletions src/e2e-helper/e2e-command-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand Down