Skip to content

Commit 1238a68

Browse files
committed
feat: track based on sdr events
1 parent 0c2135b commit 1238a68

File tree

7 files changed

+143
-25
lines changed

7 files changed

+143
-25
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"/oclif.manifest.json"
4444
],
4545
"dependencies": {
46-
"@salesforce/core": "^3.19.0",
46+
"@salesforce/core": "^3.19.2",
4747
"@salesforce/kit": "^1.5.17",
4848
"@salesforce/source-deploy-retrieve": "^6.0.0",
4949
"graceful-fs": "^4.2.9",

src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export {
1313
ChangeOptions,
1414
LocalUpdateOptions,
1515
ChangeResult,
16-
ConflictError,
1716
StatusOutputRow,
17+
ConflictResponse,
18+
SourceConflictError,
1819
} from './shared/types';
1920
export { getKeyFromObject } from './shared/functions';

src/shared/conflicts.ts

+18-15
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,38 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import { resolve } from 'path';
8-
import { SfError } from '@salesforce/core';
9-
import { SourceComponent, ComponentSet, ForceIgnore } from '@salesforce/source-deploy-retrieve';
10-
import { ConflictResponse, ChangeResult } from './types';
8+
import { ComponentSet, ForceIgnore } from '@salesforce/source-deploy-retrieve';
9+
import { ConflictResponse, ChangeResult, SourceConflictError } from './types';
1110
import { getMetadataKey } from './functions';
1211
import { populateTypesAndNames } from './populateTypesAndNames';
12+
1313
export const throwIfConflicts = (conflicts: ConflictResponse[]): void => {
1414
if (conflicts.length > 0) {
15-
const conflictError = new SfError('Conflict detected');
15+
const conflictError = new SourceConflictError(`${conflicts.length} conflicts detected`, 'SourceConflictError');
1616
conflictError.setData(conflicts);
17+
throw conflictError;
1718
}
1819
};
1920

20-
export const findConflictsInComponentSet = (
21-
components: SourceComponent[],
22-
conflicts: ChangeResult[]
23-
): ConflictResponse[] => {
21+
/**
22+
*
23+
* @param cs ComponentSet to compare
24+
* @param conflicts ChangeResult[] representing conflicts from SourceTracking.getConflicts
25+
* @returns ConflictResponse[] de-duped and formatted for json or table display
26+
*/
27+
export const findConflictsInComponentSet = (cs: ComponentSet, conflicts: ChangeResult[]): ConflictResponse[] => {
2428
// map do dedupe by name-type-filename
2529
const conflictMap = new Map<string, ConflictResponse>();
26-
const cs = new ComponentSet(components);
2730
conflicts
2831
.filter((cr) => cr.name && cr.type && cs.has({ fullName: cr.name, type: cr.type }))
29-
.forEach((c) => {
30-
c.filenames?.forEach((f) => {
31-
conflictMap.set(`${c.name}#${c.type}#${f}`, {
32+
.forEach((cr) => {
33+
cr.filenames?.forEach((f) => {
34+
conflictMap.set(`${cr.name}#${cr.type}#${f}`, {
3235
state: 'Conflict',
3336
// the following 2 type assertions are valid because of previous filter statement
3437
// they can be removed once TS is smarter about filtering
35-
fullName: c.name as string,
36-
type: c.type as string,
38+
fullName: cr.name as string,
39+
type: cr.type as string,
3740
filePath: resolve(f),
3841
});
3942
});
@@ -42,7 +45,7 @@ export const findConflictsInComponentSet = (
4245
return reformattedConflicts;
4346
};
4447

45-
export const dedupeConflictChangeResults = ({
48+
export const getDedupedConflictsFromChanges = ({
4649
localChanges = [],
4750
remoteChanges = [],
4851
projectPath,

src/shared/types.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { FileResponse, SourceComponent } from '@salesforce/source-deploy-retrieve';
9+
import { SfError } from '@salesforce/core';
910

1011
export interface ChangeOptions {
1112
origin: 'local' | 'remote';
@@ -56,17 +57,18 @@ export type SourceMember = {
5657
ignored?: boolean;
5758
};
5859

59-
export interface ConflictError {
60-
message: string;
61-
name: 'conflict';
62-
conflicts: ChangeResult[];
63-
}
64-
6560
export interface ConflictResponse {
6661
state: 'Conflict';
6762
fullName: string;
6863
type: string;
6964
filePath: string;
7065
}
7166

67+
export interface SourceConflictError extends SfError {
68+
name: 'SourceConflictError';
69+
data: ConflictResponse[];
70+
}
71+
72+
export class SourceConflictError extends SfError implements SourceConflictError {}
73+
7274
export type ChangeOptionType = ChangeResult | SourceComponent | string;

src/sourceTracking.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from '@salesforce/source-deploy-retrieve';
2828
import { RemoteSourceTrackingService, remoteChangeElementToChangeResult } from './shared/remoteSourceTrackingService';
2929
import { ShadowRepo } from './shared/localShadowRepo';
30-
import { throwIfConflicts, findConflictsInComponentSet, dedupeConflictChangeResults } from './shared/conflicts';
30+
import { throwIfConflicts, findConflictsInComponentSet, getDedupedConflictsFromChanges } from './shared/conflicts';
3131
import {
3232
RemoteSyncInput,
3333
StatusOutputRow,
@@ -562,7 +562,7 @@ export class SourceTracking extends AsyncCreatable {
562562
}
563563
this.forceIgnore ??= ForceIgnore.findAndCreate(this.project.getDefaultPackage().path);
564564

565-
return dedupeConflictChangeResults({
565+
return getDedupedConflictsFromChanges({
566566
localChanges,
567567
remoteChanges,
568568
projectPath: this.projectPath,
@@ -642,11 +642,13 @@ export class SourceTracking extends AsyncCreatable {
642642
this.logger.debug('subscribing to predeploy/retrieve events');
643643
// subscribe to SDR `pre` events to handle conflicts before deploy/retrieve
644644
lifecycle.on('scopedPreDeploy', async (e: ScopedPreDeploy) => {
645+
this.logger.debug('received scopedPreDeploy event');
645646
if (e.orgId === this.orgId) {
646647
throwIfConflicts(findConflictsInComponentSet(e.componentSet, await this.getConflicts()));
647648
}
648649
});
649650
lifecycle.on('scopedPreRetrieve', async (e: ScopedPreRetrieve) => {
651+
this.logger.debug('received scopedPreRetrieve event');
650652
if (e.orgId === this.orgId) {
651653
throwIfConflicts(findConflictsInComponentSet(e.componentSet, await this.getConflicts()));
652654
}
@@ -657,11 +659,13 @@ export class SourceTracking extends AsyncCreatable {
657659

658660
// yes, the post hooks really have different payloads!
659661
lifecycle.on('scopedPostDeploy', async (e: ScopedPostDeploy) => {
662+
this.logger.debug('received scopedPostDeploy event');
660663
if (e.orgId === this.orgId) {
661664
await this.updateTrackingFromDeploy(e.deployResult);
662665
}
663666
});
664667
lifecycle.on('scopedPostRetrieve', async (e: ScopedPostRetrieve) => {
668+
this.logger.debug('received scopedPostRetrieve event');
665669
if (e.orgId === this.orgId) {
666670
await this.updateTrackingFromRetrieve(e.retrieveResult);
667671
}

test/unit/conflicts.test.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 sinon from 'sinon';
8+
import { expect } from 'chai';
9+
import { ForceIgnore } from '@salesforce/source-deploy-retrieve';
10+
import { getDedupedConflictsFromChanges } from '../../src/shared/conflicts';
11+
import { ChangeResult } from '../../src/shared/types';
12+
13+
const class1Local: ChangeResult = {
14+
origin: 'local',
15+
name: 'MyClass',
16+
type: 'ApexClass',
17+
filenames: ['foo/classes/MyClass.cls', 'foo/classes/MyClass.cls-meta.xml'],
18+
};
19+
20+
describe('conflicts functions', () => {
21+
const sandbox = sinon.createSandbox();
22+
const forceIgnoreStub = sandbox.stub(ForceIgnore.prototype);
23+
24+
after(() => {
25+
sandbox.restore();
26+
});
27+
28+
describe('filter component set', () => {
29+
it('matches a conflict in a component set');
30+
it('returns nothing when no matches');
31+
});
32+
describe('dedupe', () => {
33+
it('works on empty changes', () => {
34+
expect(
35+
getDedupedConflictsFromChanges({
36+
localChanges: [],
37+
remoteChanges: [],
38+
projectPath: 'foo',
39+
forceIgnore: forceIgnoreStub,
40+
})
41+
).to.deep.equal([]);
42+
});
43+
it('returns nothing when only 1 side is changed', () => {
44+
expect(
45+
getDedupedConflictsFromChanges({
46+
localChanges: [class1Local],
47+
remoteChanges: [],
48+
projectPath: 'foo',
49+
forceIgnore: forceIgnoreStub,
50+
})
51+
).to.deep.equal([]);
52+
});
53+
it('does not return non-matching changes', () => {
54+
expect(
55+
getDedupedConflictsFromChanges({
56+
localChanges: [class1Local],
57+
remoteChanges: [
58+
{
59+
origin: 'remote',
60+
name: 'OtherClass',
61+
type: 'ApexClass',
62+
filenames: ['foo/classes/OtherClass.cls', 'foo/classes/OtherClass.cls-meta.xml'],
63+
},
64+
],
65+
projectPath: 'foo',
66+
forceIgnore: forceIgnoreStub,
67+
})
68+
).to.deep.equal([]);
69+
});
70+
71+
it('de-dupes local and remote change where names match', () => {
72+
const { filenames, ...simplifiedResult } = class1Local;
73+
expect(
74+
getDedupedConflictsFromChanges({
75+
localChanges: [class1Local],
76+
remoteChanges: [{ origin: 'remote', name: 'MyClass', type: 'ApexClass' }],
77+
projectPath: 'foo',
78+
forceIgnore: forceIgnoreStub,
79+
})
80+
).to.deep.equal([{ ...simplifiedResult, origin: 'remote' }]);
81+
});
82+
});
83+
});

yarn.lock

+25
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,31 @@
669669
mkdirp "1.0.4"
670670
ts-retry-promise "^0.6.0"
671671

672+
"@salesforce/core@^3.19.2":
673+
version "3.19.2"
674+
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.19.2.tgz#45b55fec4425ec9a447aba682a0add084c8ba73a"
675+
integrity sha512-uz2ehET9zz8WJj9Gicui3zxSN34TsmjgxrP22XCdQS7/GNhye1SlV8a6ayyQLqJbYlANrPFAs1RhF6LV1mPokg==
676+
dependencies:
677+
"@salesforce/bunyan" "^2.0.0"
678+
"@salesforce/kit" "^1.5.41"
679+
"@salesforce/schemas" "^1.1.0"
680+
"@salesforce/ts-types" "^1.5.20"
681+
"@types/graceful-fs" "^4.1.5"
682+
"@types/mkdirp" "^1.0.2"
683+
"@types/semver" "^7.3.9"
684+
ajv "^8.11.0"
685+
archiver "^5.3.0"
686+
change-case "^4.1.2"
687+
debug "^3.2.7"
688+
faye "^1.4.0"
689+
form-data "^4.0.0"
690+
graceful-fs "^4.2.9"
691+
js2xmlparser "^4.0.1"
692+
jsforce "2.0.0-beta.10"
693+
jsonwebtoken "8.5.1"
694+
mkdirp "1.0.4"
695+
ts-retry-promise "^0.6.0"
696+
672697
"@salesforce/dev-config@^3.0.0":
673698
version "3.0.1"
674699
resolved "https://registry.yarnpkg.com/@salesforce/dev-config/-/dev-config-3.0.1.tgz#631a952abfd69e7cdb0fb312ba4b1656ae632b90"

0 commit comments

Comments
 (0)