Skip to content

Commit b425d77

Browse files
committed
feat: virtualTree for deletes
1 parent 828c1cb commit b425d77

File tree

5 files changed

+131
-27
lines changed

5 files changed

+131
-27
lines changed

src/commands/source/push.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,22 @@ export default class SourcePush extends SfdxCommand {
4141
await tracking.ensureLocalTracking();
4242
const nonDeletes = tracking
4343
// populateTypesAndNames is used to make sure the filenames could be deployed (that they are resolvable in SDR)
44-
.populateTypesAndNames(
45-
(
44+
.populateTypesAndNames({
45+
elements: (
4646
await Promise.all([
4747
tracking.getChanges({ origin: 'local', state: 'changed' }),
4848
tracking.getChanges({ origin: 'local', state: 'add' }),
4949
])
5050
).flat(),
51-
true
52-
)
51+
excludeUnresolvable: true,
52+
})
5353
.map((change) => change.filenames)
5454
.flat();
5555
const deletes = tracking
56-
.populateTypesAndNames(await tracking.getChanges({ origin: 'local', state: 'delete' }), true)
56+
.populateTypesAndNames({
57+
elements: await tracking.getChanges({ origin: 'local', state: 'delete' }),
58+
excludeUnresolvable: true,
59+
})
5760
.map((change) => change.filenames)
5861
.flat();
5962

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

66-
if (deletes.length > 0) {
67-
this.ux.warn(
68-
`Delete not yet implemented. Would have deleted ${deletes.length > 0 ? deletes.join(',') : 'nothing'}`
69-
);
70-
}
71-
72-
const componentSet = ComponentSet.fromSource({ fsPaths: nonDeletes.filter(stringGuard) });
69+
const componentSet = ComponentSet.fromSource({
70+
fsPaths: nonDeletes.filter(stringGuard),
71+
fsDeletePaths: deletes.filter(stringGuard),
72+
});
7373
const deploy = await componentSet.deploy({ usernameOrConnection: this.org.getUsername() as string });
7474
const result = await deploy.pollStatus();
7575

src/commands/source/status.ts

+25-9
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,31 @@ export default class SourceStatus extends SfdxCommand {
4646

4747
if (this.flags.local || this.flags.all || (!this.flags.remote && !this.flags.all)) {
4848
await tracking.ensureLocalTracking();
49-
const [localDeletes, localModifies, localAdds] = (
50-
await Promise.all([
51-
tracking.getChanges({ origin: 'local', state: 'delete' }),
52-
tracking.getChanges({ origin: 'local', state: 'changed' }),
53-
tracking.getChanges({ origin: 'local', state: 'add' }),
54-
])
55-
)
56-
// we don't get type/name on local changes unless we request them
57-
.map((changes) => tracking.populateTypesAndNames(changes, true));
49+
const localDeletes = tracking.populateTypesAndNames({
50+
elements: await tracking.getChanges({ origin: 'local', state: 'delete' }),
51+
excludeUnresolvable: true,
52+
resolveDeleted: true,
53+
});
54+
55+
const localAdds = tracking.populateTypesAndNames({
56+
elements: await tracking.getChanges({ origin: 'local', state: 'add' }),
57+
excludeUnresolvable: true,
58+
});
59+
60+
const localModifies = tracking.populateTypesAndNames({
61+
elements: await tracking.getChanges({ origin: 'local', state: 'changed' }),
62+
excludeUnresolvable: true,
63+
});
64+
65+
// const [localDeletes, localModifies, localAdds] = (
66+
// await Promise.all([
67+
// tracking.getChanges({ origin: 'local', state: 'delete' }),
68+
// tracking.getChanges({ origin: 'local', state: 'changed' }),
69+
// tracking.getChanges({ origin: 'local', state: 'add' }),
70+
// ])
71+
// )
72+
// // we don't get type/name on local changes unless we request them
73+
// .map((changes) => tracking.populateTypesAndNames(changes, true));
5874
outputRows = outputRows.concat(localAdds.map((item) => this.statusResultToOutputRows(item, 'add')).flat());
5975
outputRows = outputRows.concat(
6076
localModifies.map((item) => this.statusResultToOutputRows(item, 'changed')).flat()

src/shared/filenamesToVirtualTree.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import * as path from 'path';
8+
import { VirtualTreeContainer, VirtualDirectory } from '@salesforce/source-deploy-retrieve';
9+
10+
/**
11+
* Designed for recreating virtual files from deleted files where the only information we have is the file's former location
12+
* Any use of MetadataResolver was trying to access the non-existent files and throwing
13+
*
14+
* @param filenames full paths to files
15+
* @returns VirtualTreeContainer to use with MetadataResolver
16+
*/
17+
export const filenamesToVirtualTree = (filenames: string[]): VirtualTreeContainer => {
18+
const virtualDirectoryByFullPath = new Map<string, VirtualDirectory>();
19+
filenames.map((filename) => {
20+
const splits = filename.split(path.sep);
21+
for (let i = 0; i < splits.length - 1; i++) {
22+
const fullPathSoFar = splits.slice(0, i + 1).join(path.sep);
23+
if (virtualDirectoryByFullPath.has(fullPathSoFar)) {
24+
const existing = virtualDirectoryByFullPath.get(fullPathSoFar) as VirtualDirectory;
25+
// only add to children if we don't already have it
26+
if (!existing.children.includes(splits[i + 1])) {
27+
virtualDirectoryByFullPath.set(fullPathSoFar, {
28+
dirPath: existing.dirPath,
29+
children: [...existing.children, splits[i + 1]],
30+
});
31+
}
32+
} else {
33+
virtualDirectoryByFullPath.set(fullPathSoFar, {
34+
dirPath: fullPathSoFar,
35+
children: [splits[i + 1]],
36+
});
37+
}
38+
}
39+
});
40+
return new VirtualTreeContainer(Array.from(virtualDirectoryByFullPath.values()));
41+
};

src/sourceTracking.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ComponentSet, MetadataResolver, SourceComponent } from '@salesforce/sou
1111
import { RemoteSourceTrackingService, RemoteChangeElement, getMetadataKey } from './shared/remoteSourceTrackingService';
1212
import { ShadowRepo } from './shared/localShadowRepo';
1313
import { RemoteSyncInput } from './shared/types';
14+
import { filenamesToVirtualTree } from './shared/filenamesToVirtualTree';
1415

1516
export const getKeyFromObject = (element: RemoteChangeElement | ChangeResult): string => {
1617
if (element.type && element.name) {
@@ -291,23 +292,32 @@ export class SourceTracking {
291292
* @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)
292293
*/
293294
// public async populateFilePaths(elements: ChangeResult[]): Promise<ChangeResult[]> {
294-
public populateTypesAndNames(elements: ChangeResult[], excludeUnresolvable = false): ChangeResult[] {
295+
public populateTypesAndNames({
296+
elements,
297+
excludeUnresolvable = false,
298+
resolveDeleted = false,
299+
}: {
300+
elements: ChangeResult[];
301+
excludeUnresolvable?: boolean;
302+
resolveDeleted?: boolean;
303+
}): ChangeResult[] {
295304
if (elements.length === 0) {
296305
return [];
297306
}
298307

299308
this.logger.debug(`populateTypesAndNames for ${elements.length} change elements`);
300-
// component set generated from the filenames on all local changes
301-
const resolver = new MetadataResolver();
302-
const sourceComponents = elements
309+
const filenames = elements
303310
.map((element) => element.filenames)
304311
.flat()
305-
.filter(stringGuard)
312+
.filter(stringGuard);
313+
314+
// component set generated from the filenames on all local changes
315+
const resolver = new MetadataResolver(undefined, resolveDeleted ? filenamesToVirtualTree(filenames) : undefined);
316+
const sourceComponents = filenames
306317
.map((filename) => {
307318
try {
308319
return resolver.getComponentsFromPath(filename);
309320
} catch (e) {
310-
// there will be some unresolvable files
311321
this.logger.warn(`unable to resolve ${filename}`);
312322
return undefined;
313323
}
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
/* eslint-disable no-console */
9+
10+
import { expect } from 'chai';
11+
import { MetadataResolver } from '@salesforce/source-deploy-retrieve';
12+
import { filenamesToVirtualTree } from '../../src/shared/filenamesToVirtualTree';
13+
14+
describe('two deleted files from an apex class', () => {
15+
const tree = filenamesToVirtualTree([
16+
'force-app/main/default/classes/TestOrderController.cls',
17+
'force-app/main/default/classes/TestOrderController.cls-meta.xml',
18+
]);
19+
20+
it('tree has expected structure', () => {
21+
expect(tree.isDirectory('force-app'), 'force-app').to.equal(true);
22+
expect(tree.isDirectory('force-app/main'), 'force-app/main').to.equal(true);
23+
expect(tree.isDirectory('force-app/main/default'), 'force-app/main/default').to.equal(true);
24+
expect(tree.isDirectory('force-app/main/default/classes'), 'force-app/main/default/classes').to.equal(true);
25+
expect(tree.readDirectory('force-app/main/default/classes')).to.deep.equal([
26+
'TestOrderController.cls',
27+
'TestOrderController.cls-meta.xml',
28+
]);
29+
});
30+
31+
it('tree resolves to a class', () => {
32+
const resolver = new MetadataResolver(undefined, tree);
33+
const resolved = resolver.getComponentsFromPath('force-app');
34+
expect(resolved.length).to.equal(1);
35+
expect(resolved[0].type.name).to.equal('ApexClass');
36+
});
37+
});

0 commit comments

Comments
 (0)