From 6bec27c6b39a961c968b1dbd8cf3b63cb8d6a5fb Mon Sep 17 00:00:00 2001 From: David First Date: Fri, 20 Aug 2021 13:16:22 -0400 Subject: [PATCH] enable tagging components that their head is a snap rather than a tag (#4778) Also, add a new section "snapped components" in `bit status` to display these components. --- e2e/commands/snap.e2e.2.ts | 23 +++++++++++++++++++--- scopes/workspace/workspace/tag-cmd.ts | 7 ++++++- src/api/consumer/lib/status.ts | 3 +++ src/api/consumer/lib/tag.ts | 19 +++++++++++++++--- src/cli/commands/public-cmds/status-cmd.ts | 10 ++++++++++ src/consumer/component/components-list.ts | 19 ++++++++++++++++++ src/e2e-helper/e2e-command-helper.ts | 5 +++-- src/scope/lanes/lanes.ts | 5 +++++ src/scope/models/model-component.ts | 8 ++++++++ 9 files changed, 90 insertions(+), 9 deletions(-) diff --git a/e2e/commands/snap.e2e.2.ts b/e2e/commands/snap.e2e.2.ts index 66fdf6324b64..85a9298f43a7 100644 --- a/e2e/commands/snap.e2e.2.ts +++ b/e2e/commands/snap.e2e.2.ts @@ -2,7 +2,6 @@ import chai, { expect } from 'chai'; import fs from 'fs-extra'; import path from 'path'; import { AUTO_SNAPPED_MSG } from '../../src/cli/commands/public-cmds/snap-cmd'; -import { statusWorkspaceIsCleanMsg } from '../../src/cli/commands/public-cmds/status-cmd'; import { HASH_SIZE } from '../../src/constants'; import ComponentsPendingMerge from '../../src/consumer/component-ops/exceptions/components-pending-merge'; import Helper from '../../src/e2e-helper/e2e-helper'; @@ -89,6 +88,25 @@ describe('bit snap command', function () { expect(barFoo.dependencies).to.have.lengthOf(1); expect(barFoo.dependencies[0].id.version).to.be.a('string').and.have.lengthOf(HASH_SIZE); }); + it('bit status should show them in the "snapped" section', () => { + const status = helper.command.statusJson(); + expect(status.snappedComponents).to.have.lengthOf(3); + }); + describe('tagging the components', () => { + let scopeBeforeTag: string; + before(() => { + scopeBeforeTag = helper.scopeHelper.cloneLocalScope(); + }); + it('--all flag should include the snapped components', () => { + const output = helper.command.tagAllWithoutBuild(); + expect(output).to.include('3 component(s) tagged'); + }); + it('--snapped flag should include the snapped components', () => { + helper.scopeHelper.getClonedLocalScope(scopeBeforeTag); + const output = helper.command.tagWithoutBuild(undefined, '--snapped'); + expect(output).to.include('3 component(s) tagged'); + }); + }); }); describe('untag a snap', () => { let firstSnap: string; @@ -206,8 +224,7 @@ describe('bit snap command', function () { helper.bitMap.expectToHaveIdHarmony('bar/foo', secondSnap, helper.scopes.remote); }); it('bit status should be clean', () => { - const status = helper.command.status(); - expect(status).to.have.string(statusWorkspaceIsCleanMsg); + helper.command.expectStatusToBeClean(['snappedComponents']); }); }); }); diff --git a/scopes/workspace/workspace/tag-cmd.ts b/scopes/workspace/workspace/tag-cmd.ts index b3433de25c9c..7ee3947a7dcd 100644 --- a/scopes/workspace/workspace/tag-cmd.ts +++ b/scopes/workspace/workspace/tag-cmd.ts @@ -27,6 +27,7 @@ ${WILDCARD_HELP('tag')}`; ['m', 'message ', 'log message describing the user changes'], ['a', 'all [version]', 'tag all new and modified components'], ['s', 'scope [version]', 'tag all components of the current scope'], + ['', 'snapped [version]', 'tag components that their head is a snap (not a tag)'], ['', 'ver ', 'tag specified components with the given version'], ['p', 'patch', 'increment the patch version number'], ['', 'minor', 'increment the minor version number'], @@ -61,6 +62,7 @@ ${WILDCARD_HELP('tag')}`; message = '', ver, all = false, + snapped = false, patch, minor, major, @@ -82,6 +84,7 @@ ${WILDCARD_HELP('tag')}`; incrementBy = 1, }: { all?: boolean | string; + snapped?: boolean | string; ver?: string; patch?: boolean; minor?: boolean; @@ -99,10 +102,11 @@ ${WILDCARD_HELP('tag')}`; function getVersion(): string | undefined { if (scope && isString(scope)) return scope; if (all && isString(all)) return all; + if (snapped && isString(snapped)) return snapped; return ver; } - if (!id.length && !all && !scope && !persist) { + if (!id.length && !all && !snapped && !scope && !persist) { throw new GeneralError('missing [id]. to tag all components, please use --all flag'); } if (id.length && all) { @@ -147,6 +151,7 @@ ${WILDCARD_HELP('tag')}`; const params = { ids: id, all: Boolean(all), + snapped: Boolean(snapped), message, exactVersion: getVersion(), releaseType, diff --git a/src/api/consumer/lib/status.ts b/src/api/consumer/lib/status.ts index 79fbba94dc6f..ba1821c3a4ea 100644 --- a/src/api/consumer/lib/status.ts +++ b/src/api/consumer/lib/status.ts @@ -24,6 +24,7 @@ export type StatusResult = { componentsWithIndividualFiles: Component[]; componentsWithTrackDirs: Component[]; softTaggedComponents: BitId[]; + snappedComponents: BitId[]; }; export default async function status(): Promise { @@ -55,6 +56,7 @@ export default async function status(): Promise { }); const componentsDuringMergeState = componentsList.listDuringMergeStateComponents(); const softTaggedComponents = componentsList.listSoftTaggedComponents(); + const snappedComponents = (await componentsList.listSnappedComponentsOnMain()).map((c) => c.toBitId()); Analytics.setExtraData('new_components', newComponents.length); Analytics.setExtraData('staged_components', stagedComponents.length); Analytics.setExtraData('num_components_with_missing_dependencies', componentsWithIssues.length); @@ -77,5 +79,6 @@ export default async function status(): Promise { componentsWithIndividualFiles: await componentsList.listComponentsWithIndividualFiles(), componentsWithTrackDirs: await componentsList.listComponentsWithTrackDir(), softTaggedComponents, + snappedComponents, }; } diff --git a/src/api/consumer/lib/tag.ts b/src/api/consumer/lib/tag.ts index afe65fc2c36e..dfb7e3e7ae43 100644 --- a/src/api/consumer/lib/tag.ts +++ b/src/api/consumer/lib/tag.ts @@ -49,13 +49,15 @@ type TagParams = { ignoreNewestVersion: boolean; ids: string[]; all: boolean; + snapped: boolean; scope?: string | boolean; includeImported: boolean; incrementBy: number; } & BasicTagParams; export async function tagAction(tagParams: TagParams): Promise { - const { ids, all, exactVersion, releaseType, force, ignoreIssues, scope, includeImported, persist } = tagParams; + const { ids, all, exactVersion, releaseType, force, ignoreIssues, scope, includeImported, persist, snapped } = + tagParams; const idsHasWildcard = hasWildcard(ids); const isAll = Boolean(all || scope || idsHasWildcard); const validExactVersion = validateVersion(exactVersion); @@ -72,7 +74,8 @@ export async function tagAction(tagParams: TagParams): Promise { const warnings: string[] = []; const componentsList = new ComponentsList(consumer); @@ -121,6 +125,9 @@ async function getComponentsToTag( ? await componentsList.listTagPendingOfAllScope(includeImported) : await componentsList.listTagPendingComponents(); + const snappedComponents = await componentsList.listSnappedComponentsOnMain(); + const snappedComponentsIds = snappedComponents.map((c) => c.toBitId()); + if (ids.length) { const bitIds = await Promise.all( ids.map(async (id) => { @@ -142,6 +149,12 @@ async function getComponentsToTag( return { bitIds: compact(bitIds.flat()), warnings }; } + if (snapped) { + return { bitIds: snappedComponentsIds, warnings }; + } + + tagPendingComponents.push(...snappedComponentsIds); + if (isAllScope && exactVersion) { const tagPendingComponentsLatest = await consumer.scope.latestVersions(tagPendingComponents, false); tagPendingComponentsLatest.forEach((componentId) => { diff --git a/src/cli/commands/public-cmds/status-cmd.ts b/src/cli/commands/public-cmds/status-cmd.ts index dc149f511214..d3bac7d72d51 100644 --- a/src/cli/commands/public-cmds/status-cmd.ts +++ b/src/cli/commands/public-cmds/status-cmd.ts @@ -58,6 +58,7 @@ export default class Status implements LegacyCommand { componentsWithIndividualFiles, componentsWithTrackDirs, softTaggedComponents, + snappedComponents, }: StatusResult): string { if (this.json) { return JSON.stringify( @@ -78,6 +79,7 @@ export default class Status implements LegacyCommand { componentsWithIndividualFiles: componentsWithIndividualFiles.map((c) => c.id.toString()), componentsWithTrackDirs: componentsWithTrackDirs.map((c) => c.id.toString()), softTaggedComponents: softTaggedComponents.map((s) => s.toString()), + snappedComponents: snappedComponents.map((s) => s.toString()), }, null, 2 @@ -217,6 +219,13 @@ or use "bit merge [component-id] --abort" to cancel the merge operation)\n`; stagedComponents.length ? chalk.underline.white('staged components') + stagedDesc : '' ).join('\n'); + const snappedDesc = '\n(use "bit tag --all [version]" or "bit tag --snapped [version]" to lock a version)\n'; + const snappedComponentsOutput = immutableUnshift( + // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! + snappedComponents.map((c) => format(c, true)), + snappedComponents.length ? chalk.underline.white('snapped components') + snappedDesc : '' + ).join('\n'); + const troubleshootingStr = showTroubleshootingLink ? `\n${TROUBLESHOOTING_MESSAGE}` : ''; return ( @@ -227,6 +236,7 @@ or use "bit merge [component-id] --abort" to cancel the merge operation)\n`; compWithConflictsStr, newComponentsOutput, modifiedComponentOutput, + snappedComponentsOutput, stagedComponentsOutput, autoTagPendingOutput, invalidComponentOutput, diff --git a/src/consumer/component/components-list.ts b/src/consumer/component/components-list.ts index b50af5bdf5e5..fd5f98ff4385 100644 --- a/src/consumer/component/components-list.ts +++ b/src/consumer/component/components-list.ts @@ -144,6 +144,25 @@ export default class ComponentsList { return fileSystemComponents.filter((f) => f.latestVersion); } + /** + * list components that their head is a snap, not a tag. + * this is relevant only when the lane is the default (main), otherwise, the head is always a snap. + * components that are during-merge are filtered out, we don't want them during tag and don't want + * to show them in the "snapped" section in bit-status. + */ + async listSnappedComponentsOnMain() { + if (!this.scope.lanes.isOnDefaultLane()) { + return []; + } + const componentsFromModel = await this.getModelComponents(); + const authoredAndImportedIds = this.bitMap.getAuthoredAndImportedBitIds(); + const compsDuringMerge = this.listDuringMergeStateComponents(); + return componentsFromModel + .filter((c) => authoredAndImportedIds.hasWithoutVersion(c.toBitId())) + .filter((c) => !compsDuringMerge.hasWithoutVersion(c.toBitId())) + .filter((c) => c.isHeadSnap()); + } + async listMergePendingComponents(): Promise { if (!this._mergePendingComponents) { const componentsFromFs = await this.getAuthoredAndImportedFromFS(); diff --git a/src/e2e-helper/e2e-command-helper.ts b/src/e2e-helper/e2e-command-helper.ts index d990d4cdffd0..3bb272ac62d1 100644 --- a/src/e2e-helper/e2e-command-helper.ts +++ b/src/e2e-helper/e2e-command-helper.ts @@ -168,7 +168,7 @@ export default class CommandHelper { expect(result).to.not.have.string(NOTHING_TO_TAG_MSG); return result; } - tagWithoutBuild(id: string, options = '') { + tagWithoutBuild(id = '', options = '') { const result = this.runCmd(`bit tag ${id} ${options}`, undefined, undefined, BUILD_ON_CI); expect(result).to.not.have.string(NOTHING_TO_TAG_MSG); return result; @@ -409,9 +409,10 @@ export default class CommandHelper { return JSON.parse(status); } - expectStatusToBeClean() { + expectStatusToBeClean(exclude: string[] = []) { const statusJson = this.statusJson(); Object.keys(statusJson).forEach((key) => { + if (exclude.includes(key)) return; expect(statusJson[key], `status.${key} should be empty`).to.have.lengthOf(0); }); } diff --git a/src/scope/lanes/lanes.ts b/src/scope/lanes/lanes.ts index 3c4a5abf2b7e..7160c6519066 100644 --- a/src/scope/lanes/lanes.ts +++ b/src/scope/lanes/lanes.ts @@ -39,6 +39,11 @@ export default class Lanes { return laneName; } + isOnDefaultLane(): boolean { + const currentLane = this.getCurrentLaneName(); + return currentLane === DEFAULT_LANE; + } + getCurrentLaneId(): LocalLaneId { return LocalLaneId.from(this.getCurrentLaneName() || DEFAULT_LANE); } diff --git a/src/scope/models/model-component.ts b/src/scope/models/model-component.ts index b32794b99f4c..208fb16f3bb7 100644 --- a/src/scope/models/model-component.ts +++ b/src/scope/models/model-component.ts @@ -208,6 +208,14 @@ export default class Component extends BitObject { return Boolean(this.versions[version] || this.orphanedVersions[version]); } + /** + * whether the head is a snap (not a tag) + */ + isHeadSnap() { + const tagsHashes = this.versionArray.map((ref) => ref.toString()); + return this.head && !tagsHashes.includes(this.head.toString()); + } + /** * add a new remote if it is not there already */