diff --git a/e2e/harmony/checkout-harmony.e2e.ts b/e2e/harmony/checkout-harmony.e2e.ts index 78b701c0d087..7a8e6a2f0c3a 100644 --- a/e2e/harmony/checkout-harmony.e2e.ts +++ b/e2e/harmony/checkout-harmony.e2e.ts @@ -6,6 +6,7 @@ import * as path from 'path'; import { MissingBitMapComponent } from '../../src/consumer/bit-map/exceptions'; import { NewerVersionFound } from '../../src/consumer/exceptions'; import Helper, { FileStatusWithoutChalk } from '../../src/e2e-helper/e2e-helper'; +import { FILE_CHANGES_CHECKOUT_MSG } from '../../src/constants'; chai.use(require('chai-fs')); @@ -362,7 +363,7 @@ describe('bit checkout command', function () { expect(output).to.have.string('bar/foo'); }); it('should indicate that the file was not changed', () => { - expect(output).to.have.string(FileStatusWithoutChalk.unchanged); + expect(output).to.not.have.string(FILE_CHANGES_CHECKOUT_MSG); }); it('should leave the file intact', () => { const fileContent = fs.readFileSync(path.join(helper.scopes.localPath, 'bar/foo.js')).toString(); @@ -419,8 +420,7 @@ describe('bit checkout command', function () { output = helper.command.checkoutVersion('0.0.1', 'bar/foo', '--auto-merge-resolve ours'); }); it('should indicate that the new file was not changed', () => { - expect(output).to.have.string(FileStatusWithoutChalk.unchanged); - expect(output).to.have.string('foo2.js'); + expect(output).to.not.have.string(FILE_CHANGES_CHECKOUT_MSG); }); it('should not delete the file', () => { expect(path.join(helper.scopes.localPath, 'bar/foo2.js')).to.be.a.file(); diff --git a/e2e/harmony/lanes/merge-lanes.e2e.ts b/e2e/harmony/lanes/merge-lanes.e2e.ts index 7609da488b1b..05aa5ebf3ccb 100644 --- a/e2e/harmony/lanes/merge-lanes.e2e.ts +++ b/e2e/harmony/lanes/merge-lanes.e2e.ts @@ -567,7 +567,7 @@ describe('merge lanes', function () { mergeOutput = helper.command.mergeLane('main', '--resolve-unrelated'); }); it('should merge successfully', () => { - expect(mergeOutput).to.have.string('successfully merged components'); + expect(mergeOutput).to.have.string('successfully merged'); }); it('bit status should show the component as staged and not everywhere else', () => { helper.command.expectStatusToBeClean(['stagedComponents']); @@ -1524,4 +1524,32 @@ describe('merge lanes', function () { }); }); }); + describe('multiple files, some are not changes', () => { + let switchOutput: string; + before(() => { + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.command.createLane('lane-a'); + helper.fixtures.populateComponents(1, false); + helper.fs.outputFile('comp1/foo.js'); + helper.command.snapAllComponentsWithoutBuild(); + helper.command.export(); + helper.command.createLane('lane-b'); + helper.command.snapAllComponentsWithoutBuild('--unmodified'); + helper.command.export(); + + switchOutput = helper.command.switchLocalLane('lane-a', '-x'); + }); + it('expect to have all files as unchanged, not updated', () => { + expect(switchOutput).to.not.have.string('updated'); + }); + describe('merge the lane', () => { + let mergeOutput: string; + before(() => { + mergeOutput = helper.command.mergeLane('lane-b', '-x'); + }); + it('expect to have all files as unchanged, not updated', () => { + expect(mergeOutput).to.not.have.string('updated'); + }); + }); + }); }); diff --git a/e2e/harmony/snap.e2e.2.ts b/e2e/harmony/snap.e2e.2.ts index 2a9493725df5..2afb132ba1e1 100644 --- a/e2e/harmony/snap.e2e.2.ts +++ b/e2e/harmony/snap.e2e.2.ts @@ -1,7 +1,7 @@ import chai, { expect } from 'chai'; import fs from 'fs-extra'; import path from 'path'; -import { HASH_SIZE, AUTO_SNAPPED_MSG } from '../../src/constants'; +import { HASH_SIZE, AUTO_SNAPPED_MSG, FILE_CHANGES_CHECKOUT_MSG } from '../../src/constants'; import ComponentsPendingMerge from '../../src/consumer/component-ops/exceptions/components-pending-merge'; import Helper from '../../src/e2e-helper/e2e-helper'; import * as fixtures from '../../src/fixtures/fixtures'; @@ -266,7 +266,7 @@ describe('bit snap command', function () { mergeOutput = helper.command.merge('bar/foo --auto-merge-resolve ours'); }); it('should succeed and indicate that the files were not changed', () => { - expect(mergeOutput).to.have.string('unchanged'); + expect(mergeOutput).to.not.have.string(FILE_CHANGES_CHECKOUT_MSG); }); it('should indicate that a component was snapped', () => { expect(mergeOutput).to.have.string('merge-snapped components'); @@ -298,7 +298,7 @@ describe('bit snap command', function () { mergeOutput = helper.command.merge('bar/foo --auto-merge-resolve ours --no-snap'); }); it('should succeed and indicate that the files were not changed', () => { - expect(mergeOutput).to.have.string('unchanged'); + expect(mergeOutput).to.not.have.string(FILE_CHANGES_CHECKOUT_MSG); }); it('should not show a message about merge-snapped components', () => { expect(mergeOutput).to.not.have.string('merge-snapped components'); diff --git a/e2e/harmony/updates-from-main-and-lane.e2e.ts b/e2e/harmony/updates-from-main-and-lane.e2e.ts index 4d093c67e9d6..4cc477babbc8 100644 --- a/e2e/harmony/updates-from-main-and-lane.e2e.ts +++ b/e2e/harmony/updates-from-main-and-lane.e2e.ts @@ -72,8 +72,7 @@ describe('updates from main and lane', function () { ); }); it('should update not only components belong to the main but also components that are available on the workspace and have updates from main', () => { - expect(mergeOutput).to.have.string('comp1'); - expect(mergeOutput).to.have.string('comp2'); + expect(mergeOutput).to.have.string('Total Merged: 2'); const status = helper.command.statusJson(undefined, '--lanes'); expect(status.pendingUpdatesFromMain).to.have.lengthOf(0); diff --git a/scopes/component/checkout/checkout-cmd.ts b/scopes/component/checkout/checkout-cmd.ts index e38246416e32..7919e3085d7d 100644 --- a/scopes/component/checkout/checkout-cmd.ts +++ b/scopes/component/checkout/checkout-cmd.ts @@ -200,7 +200,7 @@ once ready, snap/tag the components to persist the changes`; // @ts-ignore version is defined when !reset head || latest ? component.id.version : version )}\n`; - return `${chalk.underline(title)} ${applyVersionReport(components, false)}`; + return chalk.bold(title) + applyVersionReport(components, false); } if (reset) { const title = 'successfully reset the following components\n\n'; @@ -215,10 +215,9 @@ once ready, snap/tag the components to persist the changes`; return `version ${chalk.bold(version)}`; }; const versionOutput = getVerOutput(); - const title = `successfully ${switchedOrReverted} the following components to ${versionOutput}\n\n`; + const title = `successfully ${switchedOrReverted} ${components.length} components to ${versionOutput}\n`; const showVersion = head || reset; - const componentsStr = applyVersionReport(components, true, showVersion); - return chalk.underline(title) + componentsStr; + return chalk.bold(title) + applyVersionReport(components, true, showVersion); }; const getNewOnLaneOutput = () => { if (!newFromLane?.length) return ''; diff --git a/scopes/component/checkout/checkout-version.ts b/scopes/component/checkout/checkout-version.ts index b3064080b25c..cd6d4b11934a 100644 --- a/scopes/component/checkout/checkout-version.ts +++ b/scopes/component/checkout/checkout-version.ts @@ -91,9 +91,7 @@ export async function applyVersion( if (componentFromFS && !componentMap) throw new GeneralError('applyVersion: componentMap was not found'); const files = component.files; - files.forEach((file) => { - filesStatus[pathNormalizeToLinux(file.relative)] = FileStatus.updated; - }); + updateFileStatus(files, filesStatus, componentFromFS || undefined); await removeFilesIfNeeded(filesStatus, componentFromFS || undefined); @@ -110,6 +108,15 @@ export async function applyVersion( }; } +export function updateFileStatus(files: SourceFile[], filesStatus: FilesStatus, componentFromFS?: ConsumerComponent) { + files.forEach((file) => { + const fileFromFs = componentFromFS?.files.find((f) => f.relative === file.relative); + const areFilesEqual = fileFromFs && Buffer.compare(fileFromFs.contents, file.contents) === 0; + // @ts-ignore + filesStatus[pathNormalizeToLinux(file.relative)] = areFilesEqual ? FileStatus.unchanged : FileStatus.updated; + }); +} + /** * when files exist on the filesystem but not on the checked out versions, they need to be deleted. * without this function, these files would be left on the filesystem. (we don't delete the comp-dir before writing). diff --git a/scopes/component/checkout/index.ts b/scopes/component/checkout/index.ts index b3a2b449f69f..fa6870ad1d37 100644 --- a/scopes/component/checkout/index.ts +++ b/scopes/component/checkout/index.ts @@ -12,5 +12,6 @@ export { ComponentStatusBase, ApplyVersionWithComps, removeFilesIfNeeded, + updateFileStatus, } from './checkout-version'; export { checkoutOutput } from './checkout-cmd'; diff --git a/scopes/component/merging/merge-cmd.ts b/scopes/component/merging/merge-cmd.ts index 045c53b16dac..2a7b3c160448 100644 --- a/scopes/component/merging/merge-cmd.ts +++ b/scopes/component/merging/merge-cmd.ts @@ -3,7 +3,12 @@ import { Command, CommandOptions } from '@teambit/cli'; import { BitId } from '@teambit/legacy-bit-id'; import { ComponentID } from '@teambit/component-id'; import { compact } from 'lodash'; -import { WILDCARD_HELP, AUTO_SNAPPED_MSG, MergeConfigFilename } from '@teambit/legacy/dist/constants'; +import { + WILDCARD_HELP, + AUTO_SNAPPED_MSG, + MergeConfigFilename, + FILE_CHANGES_CHECKOUT_MSG, +} from '@teambit/legacy/dist/constants'; import { FileStatus, ApplyVersionResult, @@ -148,10 +153,12 @@ export function mergeReport({ }: ApplyVersionResults & { configMergeResults?: ConfigMergeResult[] }): string { const getSuccessOutput = () => { if (!components || !components.length) return ''; - // @ts-ignore version is set in case of merge command - const title = `successfully merged components${version ? `from version ${chalk.bold(version)}` : ''}\n`; - // @ts-ignore components is set in case of merge command - return chalk.underline(title) + chalk.green(applyVersionReport(components)); + const title = `successfully merged ${components.length} components${ + version ? `from version ${chalk.bold(version)}` : '' + }\n`; + const fileChangesReport = applyVersionReport(components); + + return chalk.bold(title) + fileChangesReport; }; const getConflictSummary = () => { @@ -263,13 +270,18 @@ ${mergeSnapError.message} ); } +/** + * shows only the file-changes section. + * if all files are "unchanged", it returns an empty string + */ export function applyVersionReport(components: ApplyVersionResult[], addName = true, showVersion = false): string { const tab = addName ? '\t' : ''; - return components - .map((component: ApplyVersionResult) => { + const fileChanges = compact( + components.map((component: ApplyVersionResult) => { const name = showVersion ? component.id.toString() : component.id.toStringWithoutVersion(); - const files = Object.keys(component.filesStatus) - .map((file) => { + const files = compact( + Object.keys(component.filesStatus).map((file) => { + if (component.filesStatus[file] === FileStatus.unchanged) return null; const note = component.filesStatus[file] === FileStatus.manual ? chalk.white( @@ -278,10 +290,16 @@ export function applyVersionReport(components: ApplyVersionResult[], addName = t : ''; return `${tab}${component.filesStatus[file]} ${chalk.bold(file)} ${note}`; }) - .join('\n'); + ).join('\n'); + if (!files) return null; return `${addName ? name : ''}\n${chalk.cyan(files)}`; }) - .join('\n\n'); + ).join('\n\n'); + if (!fileChanges) { + return ''; + } + const title = `\n${FILE_CHANGES_CHECKOUT_MSG}\n`; + return chalk.underline(title) + fileChanges; } export function conflictSummaryReport(components: ApplyVersionResult[]): string { diff --git a/scopes/component/merging/merging.main.runtime.ts b/scopes/component/merging/merging.main.runtime.ts index 4453dabefd76..b0e21be27d5d 100644 --- a/scopes/component/merging/merging.main.runtime.ts +++ b/scopes/component/merging/merging.main.runtime.ts @@ -40,6 +40,7 @@ import { ComponentStatusBase, applyModifiedVersion, removeFilesIfNeeded, + updateFileStatus, } from '@teambit/checkout'; import { ComponentID } from '@teambit/component-id'; import { DEPENDENCIES_FIELDS } from '@teambit/legacy/dist/constants'; @@ -597,10 +598,7 @@ export class MergingMain { legacyComponent.version = id.version; } const files = legacyComponent.files; - files.forEach((file) => { - // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! - filesStatus[pathNormalizeToLinux(file.relative)] = FileStatus.updated; - }); + updateFileStatus(files, filesStatus, currentComponent || undefined); if (mergeResults) { // update files according to the merge results diff --git a/scopes/lanes/lanes/switch.cmd.ts b/scopes/lanes/lanes/switch.cmd.ts index 5007a1d2ccd5..5b3cb448714b 100644 --- a/scopes/lanes/lanes/switch.cmd.ts +++ b/scopes/lanes/lanes/switch.cmd.ts @@ -85,17 +85,8 @@ ${COMPONENT_PATTERN_HELP}`, const getSuccessfulOutput = () => { const laneSwitched = chalk.green(`\nsuccessfully set "${chalk.bold(lane)}" as the active lane`); if (!components || !components.length) return `No components have been changed.${laneSwitched}`; - if (components.length === 1) { - const component = components[0]; - const componentName = component.id.toStringWithoutVersion(); - const title = `successfully switched ${chalk.bold(componentName)} to version ${chalk.bold( - component.id.version as string - )}\n`; - return `${title} ${applyVersionReport(components, false)}${laneSwitched}`; - } - const title = `successfully switched the following components to the head of lane ${lane}\n\n`; - const componentsStr = applyVersionReport(components, true, false); - return title + componentsStr + laneSwitched; + const title = `successfully switched ${components.length} components to the head of lane ${lane}\n`; + return chalk.bold(title) + applyVersionReport(components, true, false) + laneSwitched; }; const failedOutput = getFailureOutput(); const successOutput = getSuccessfulOutput(); diff --git a/scopes/lanes/merge-lanes/merge-lanes.main.runtime.ts b/scopes/lanes/merge-lanes/merge-lanes.main.runtime.ts index f9c13bab3d70..d21aac183b37 100644 --- a/scopes/lanes/merge-lanes/merge-lanes.main.runtime.ts +++ b/scopes/lanes/merge-lanes/merge-lanes.main.runtime.ts @@ -250,7 +250,7 @@ export class MergeLanesMain { const readmeComponentId = otherLane.readmeComponent.id.changeVersion(otherLane.readmeComponent?.head?.hash); deleteResults = await this.remove.removeLocallyByIds([readmeComponentId]); } else if (otherLane && !otherLane.readmeComponent) { - deleteResults = { readmeResult: `\nlane ${otherLane.name} doesn't have a readme component` }; + deleteResults = { readmeResult: '' }; } const configMergeResults = allComponentsStatus.map((c) => c.configMergeResult); diff --git a/src/constants.ts b/src/constants.ts index 027a7d42a856..fefad085e7dd 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -609,3 +609,5 @@ export enum BuildStatus { } export const SOURCE_DIR_SYMLINK_TO_NM = '_src'; // symlink from node_modules to the workspace sources files + +export const FILE_CHANGES_CHECKOUT_MSG = 'components with file changes';