Skip to content

Commit

Permalink
feat: virtualTree for deletes
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Sep 1, 2021
1 parent 828c1cb commit b425d77
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 27 deletions.
24 changes: 12 additions & 12 deletions src/commands/source/push.ts
Expand Up @@ -41,19 +41,22 @@ export default class SourcePush extends SfdxCommand {
await tracking.ensureLocalTracking();
const nonDeletes = tracking
// populateTypesAndNames is used to make sure the filenames could be deployed (that they are resolvable in SDR)
.populateTypesAndNames(
(
.populateTypesAndNames({
elements: (
await Promise.all([
tracking.getChanges({ origin: 'local', state: 'changed' }),
tracking.getChanges({ origin: 'local', state: 'add' }),
])
).flat(),
true
)
excludeUnresolvable: true,
})
.map((change) => change.filenames)
.flat();
const deletes = tracking
.populateTypesAndNames(await tracking.getChanges({ origin: 'local', state: 'delete' }), true)
.populateTypesAndNames({
elements: await tracking.getChanges({ origin: 'local', state: 'delete' }),
excludeUnresolvable: true,
})
.map((change) => change.filenames)
.flat();

Expand All @@ -63,13 +66,10 @@ export default class SourcePush extends SfdxCommand {
return [];
}

if (deletes.length > 0) {
this.ux.warn(
`Delete not yet implemented. Would have deleted ${deletes.length > 0 ? deletes.join(',') : 'nothing'}`
);
}

const componentSet = ComponentSet.fromSource({ fsPaths: nonDeletes.filter(stringGuard) });
const componentSet = ComponentSet.fromSource({
fsPaths: nonDeletes.filter(stringGuard),
fsDeletePaths: deletes.filter(stringGuard),
});
const deploy = await componentSet.deploy({ usernameOrConnection: this.org.getUsername() as string });
const result = await deploy.pollStatus();

Expand Down
34 changes: 25 additions & 9 deletions src/commands/source/status.ts
Expand Up @@ -46,15 +46,31 @@ export default class SourceStatus extends SfdxCommand {

if (this.flags.local || this.flags.all || (!this.flags.remote && !this.flags.all)) {
await tracking.ensureLocalTracking();
const [localDeletes, localModifies, localAdds] = (
await Promise.all([
tracking.getChanges({ origin: 'local', state: 'delete' }),
tracking.getChanges({ origin: 'local', state: 'changed' }),
tracking.getChanges({ origin: 'local', state: 'add' }),
])
)
// we don't get type/name on local changes unless we request them
.map((changes) => tracking.populateTypesAndNames(changes, true));
const localDeletes = tracking.populateTypesAndNames({
elements: await tracking.getChanges({ origin: 'local', state: 'delete' }),
excludeUnresolvable: true,
resolveDeleted: true,
});

const localAdds = tracking.populateTypesAndNames({
elements: await tracking.getChanges({ origin: 'local', state: 'add' }),
excludeUnresolvable: true,
});

const localModifies = tracking.populateTypesAndNames({
elements: await tracking.getChanges({ origin: 'local', state: 'changed' }),
excludeUnresolvable: true,
});

// const [localDeletes, localModifies, localAdds] = (
// await Promise.all([
// tracking.getChanges({ origin: 'local', state: 'delete' }),
// tracking.getChanges({ origin: 'local', state: 'changed' }),
// tracking.getChanges({ origin: 'local', state: 'add' }),
// ])
// )
// // we don't get type/name on local changes unless we request them
// .map((changes) => tracking.populateTypesAndNames(changes, true));
outputRows = outputRows.concat(localAdds.map((item) => this.statusResultToOutputRows(item, 'add')).flat());
outputRows = outputRows.concat(
localModifies.map((item) => this.statusResultToOutputRows(item, 'changed')).flat()
Expand Down
41 changes: 41 additions & 0 deletions src/shared/filenamesToVirtualTree.ts
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as path from 'path';
import { VirtualTreeContainer, VirtualDirectory } from '@salesforce/source-deploy-retrieve';

/**
* Designed for recreating virtual files from deleted files where the only information we have is the file's former location
* Any use of MetadataResolver was trying to access the non-existent files and throwing
*
* @param filenames full paths to files
* @returns VirtualTreeContainer to use with MetadataResolver
*/
export const filenamesToVirtualTree = (filenames: string[]): VirtualTreeContainer => {
const virtualDirectoryByFullPath = new Map<string, VirtualDirectory>();
filenames.map((filename) => {
const splits = filename.split(path.sep);
for (let i = 0; i < splits.length - 1; i++) {
const fullPathSoFar = splits.slice(0, i + 1).join(path.sep);
if (virtualDirectoryByFullPath.has(fullPathSoFar)) {
const existing = virtualDirectoryByFullPath.get(fullPathSoFar) as VirtualDirectory;
// only add to children if we don't already have it
if (!existing.children.includes(splits[i + 1])) {
virtualDirectoryByFullPath.set(fullPathSoFar, {
dirPath: existing.dirPath,
children: [...existing.children, splits[i + 1]],
});
}
} else {
virtualDirectoryByFullPath.set(fullPathSoFar, {
dirPath: fullPathSoFar,
children: [splits[i + 1]],
});
}
}
});
return new VirtualTreeContainer(Array.from(virtualDirectoryByFullPath.values()));
};
22 changes: 16 additions & 6 deletions src/sourceTracking.ts
Expand Up @@ -11,6 +11,7 @@ import { ComponentSet, MetadataResolver, SourceComponent } from '@salesforce/sou
import { RemoteSourceTrackingService, RemoteChangeElement, getMetadataKey } from './shared/remoteSourceTrackingService';
import { ShadowRepo } from './shared/localShadowRepo';
import { RemoteSyncInput } from './shared/types';
import { filenamesToVirtualTree } from './shared/filenamesToVirtualTree';

export const getKeyFromObject = (element: RemoteChangeElement | ChangeResult): string => {
if (element.type && element.name) {
Expand Down Expand Up @@ -291,23 +292,32 @@ export class SourceTracking {
* @input excludeUnresolvables: boolean Filter out components where you can't get the name and type (that is, it's probably not a valid source component)
*/
// public async populateFilePaths(elements: ChangeResult[]): Promise<ChangeResult[]> {
public populateTypesAndNames(elements: ChangeResult[], excludeUnresolvable = false): ChangeResult[] {
public populateTypesAndNames({
elements,
excludeUnresolvable = false,
resolveDeleted = false,
}: {
elements: ChangeResult[];
excludeUnresolvable?: boolean;
resolveDeleted?: boolean;
}): ChangeResult[] {
if (elements.length === 0) {
return [];
}

this.logger.debug(`populateTypesAndNames for ${elements.length} change elements`);
// component set generated from the filenames on all local changes
const resolver = new MetadataResolver();
const sourceComponents = elements
const filenames = elements
.map((element) => element.filenames)
.flat()
.filter(stringGuard)
.filter(stringGuard);

// component set generated from the filenames on all local changes
const resolver = new MetadataResolver(undefined, resolveDeleted ? filenamesToVirtualTree(filenames) : undefined);
const sourceComponents = filenames
.map((filename) => {
try {
return resolver.getComponentsFromPath(filename);
} catch (e) {
// there will be some unresolvable files
this.logger.warn(`unable to resolve ${filename}`);
return undefined;
}
Expand Down
37 changes: 37 additions & 0 deletions test/unit/filenamesToVirtualTree.test.ts
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

/* eslint-disable no-console */

import { expect } from 'chai';
import { MetadataResolver } from '@salesforce/source-deploy-retrieve';
import { filenamesToVirtualTree } from '../../src/shared/filenamesToVirtualTree';

describe('two deleted files from an apex class', () => {
const tree = filenamesToVirtualTree([
'force-app/main/default/classes/TestOrderController.cls',
'force-app/main/default/classes/TestOrderController.cls-meta.xml',
]);

it('tree has expected structure', () => {
expect(tree.isDirectory('force-app'), 'force-app').to.equal(true);
expect(tree.isDirectory('force-app/main'), 'force-app/main').to.equal(true);
expect(tree.isDirectory('force-app/main/default'), 'force-app/main/default').to.equal(true);
expect(tree.isDirectory('force-app/main/default/classes'), 'force-app/main/default/classes').to.equal(true);
expect(tree.readDirectory('force-app/main/default/classes')).to.deep.equal([
'TestOrderController.cls',
'TestOrderController.cls-meta.xml',
]);
});

it('tree resolves to a class', () => {
const resolver = new MetadataResolver(undefined, tree);
const resolved = resolver.getComponentsFromPath('force-app');
expect(resolved.length).to.equal(1);
expect(resolved[0].type.name).to.equal('ApexClass');
});
});

0 comments on commit b425d77

Please sign in to comment.