Skip to content

Commit

Permalink
improvement(bit-status): add a new section showing updates from forke…
Browse files Browse the repository at this point in the history
…d lane (#6575)

currently, it always shows the updates from main. however, for forked lanes, the updates from the original lanes are more helpful.
  • Loading branch information
davidfirst authored Oct 21, 2022
1 parent bca4036 commit 2620b41
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 32 deletions.
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();
});
});
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
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

0 comments on commit 2620b41

Please sign in to comment.