Skip to content

Commit

Permalink
feat: track based on sdr events
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Jun 6, 2022
1 parent 0c2135b commit 1238a68
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 25 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"/oclif.manifest.json"
],
"dependencies": {
"@salesforce/core": "^3.19.0",
"@salesforce/core": "^3.19.2",
"@salesforce/kit": "^1.5.17",
"@salesforce/source-deploy-retrieve": "^6.0.0",
"graceful-fs": "^4.2.9",
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export {
ChangeOptions,
LocalUpdateOptions,
ChangeResult,
ConflictError,
StatusOutputRow,
ConflictResponse,
SourceConflictError,
} from './shared/types';
export { getKeyFromObject } from './shared/functions';
33 changes: 18 additions & 15 deletions src/shared/conflicts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,38 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { resolve } from 'path';
import { SfError } from '@salesforce/core';
import { SourceComponent, ComponentSet, ForceIgnore } from '@salesforce/source-deploy-retrieve';
import { ConflictResponse, ChangeResult } from './types';
import { ComponentSet, ForceIgnore } from '@salesforce/source-deploy-retrieve';
import { ConflictResponse, ChangeResult, SourceConflictError } from './types';
import { getMetadataKey } from './functions';
import { populateTypesAndNames } from './populateTypesAndNames';

export const throwIfConflicts = (conflicts: ConflictResponse[]): void => {
if (conflicts.length > 0) {
const conflictError = new SfError('Conflict detected');
const conflictError = new SourceConflictError(`${conflicts.length} conflicts detected`, 'SourceConflictError');
conflictError.setData(conflicts);
throw conflictError;
}
};

export const findConflictsInComponentSet = (
components: SourceComponent[],
conflicts: ChangeResult[]
): ConflictResponse[] => {
/**
*
* @param cs ComponentSet to compare
* @param conflicts ChangeResult[] representing conflicts from SourceTracking.getConflicts
* @returns ConflictResponse[] de-duped and formatted for json or table display
*/
export const findConflictsInComponentSet = (cs: ComponentSet, conflicts: ChangeResult[]): ConflictResponse[] => {
// map do dedupe by name-type-filename
const conflictMap = new Map<string, ConflictResponse>();
const cs = new ComponentSet(components);
conflicts
.filter((cr) => cr.name && cr.type && cs.has({ fullName: cr.name, type: cr.type }))
.forEach((c) => {
c.filenames?.forEach((f) => {
conflictMap.set(`${c.name}#${c.type}#${f}`, {
.forEach((cr) => {
cr.filenames?.forEach((f) => {
conflictMap.set(`${cr.name}#${cr.type}#${f}`, {
state: 'Conflict',
// the following 2 type assertions are valid because of previous filter statement
// they can be removed once TS is smarter about filtering
fullName: c.name as string,
type: c.type as string,
fullName: cr.name as string,
type: cr.type as string,
filePath: resolve(f),
});
});
Expand All @@ -42,7 +45,7 @@ export const findConflictsInComponentSet = (
return reformattedConflicts;
};

export const dedupeConflictChangeResults = ({
export const getDedupedConflictsFromChanges = ({
localChanges = [],
remoteChanges = [],
projectPath,
Expand Down
14 changes: 8 additions & 6 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

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

export interface ChangeOptions {
origin: 'local' | 'remote';
Expand Down Expand Up @@ -56,17 +57,18 @@ export type SourceMember = {
ignored?: boolean;
};

export interface ConflictError {
message: string;
name: 'conflict';
conflicts: ChangeResult[];
}

export interface ConflictResponse {
state: 'Conflict';
fullName: string;
type: string;
filePath: string;
}

export interface SourceConflictError extends SfError {
name: 'SourceConflictError';
data: ConflictResponse[];
}

export class SourceConflictError extends SfError implements SourceConflictError {}

export type ChangeOptionType = ChangeResult | SourceComponent | string;
8 changes: 6 additions & 2 deletions src/sourceTracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from '@salesforce/source-deploy-retrieve';
import { RemoteSourceTrackingService, remoteChangeElementToChangeResult } from './shared/remoteSourceTrackingService';
import { ShadowRepo } from './shared/localShadowRepo';
import { throwIfConflicts, findConflictsInComponentSet, dedupeConflictChangeResults } from './shared/conflicts';
import { throwIfConflicts, findConflictsInComponentSet, getDedupedConflictsFromChanges } from './shared/conflicts';
import {
RemoteSyncInput,
StatusOutputRow,
Expand Down Expand Up @@ -562,7 +562,7 @@ export class SourceTracking extends AsyncCreatable {
}
this.forceIgnore ??= ForceIgnore.findAndCreate(this.project.getDefaultPackage().path);

return dedupeConflictChangeResults({
return getDedupedConflictsFromChanges({
localChanges,
remoteChanges,
projectPath: this.projectPath,
Expand Down Expand Up @@ -642,11 +642,13 @@ export class SourceTracking extends AsyncCreatable {
this.logger.debug('subscribing to predeploy/retrieve events');
// subscribe to SDR `pre` events to handle conflicts before deploy/retrieve
lifecycle.on('scopedPreDeploy', async (e: ScopedPreDeploy) => {
this.logger.debug('received scopedPreDeploy event');
if (e.orgId === this.orgId) {
throwIfConflicts(findConflictsInComponentSet(e.componentSet, await this.getConflicts()));
}
});
lifecycle.on('scopedPreRetrieve', async (e: ScopedPreRetrieve) => {
this.logger.debug('received scopedPreRetrieve event');
if (e.orgId === this.orgId) {
throwIfConflicts(findConflictsInComponentSet(e.componentSet, await this.getConflicts()));
}
Expand All @@ -657,11 +659,13 @@ export class SourceTracking extends AsyncCreatable {

// yes, the post hooks really have different payloads!
lifecycle.on('scopedPostDeploy', async (e: ScopedPostDeploy) => {
this.logger.debug('received scopedPostDeploy event');
if (e.orgId === this.orgId) {
await this.updateTrackingFromDeploy(e.deployResult);
}
});
lifecycle.on('scopedPostRetrieve', async (e: ScopedPostRetrieve) => {
this.logger.debug('received scopedPostRetrieve event');
if (e.orgId === this.orgId) {
await this.updateTrackingFromRetrieve(e.retrieveResult);
}
Expand Down
83 changes: 83 additions & 0 deletions test/unit/conflicts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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 sinon from 'sinon';
import { expect } from 'chai';
import { ForceIgnore } from '@salesforce/source-deploy-retrieve';
import { getDedupedConflictsFromChanges } from '../../src/shared/conflicts';
import { ChangeResult } from '../../src/shared/types';

const class1Local: ChangeResult = {
origin: 'local',
name: 'MyClass',
type: 'ApexClass',
filenames: ['foo/classes/MyClass.cls', 'foo/classes/MyClass.cls-meta.xml'],
};

describe('conflicts functions', () => {
const sandbox = sinon.createSandbox();
const forceIgnoreStub = sandbox.stub(ForceIgnore.prototype);

after(() => {
sandbox.restore();
});

describe('filter component set', () => {
it('matches a conflict in a component set');
it('returns nothing when no matches');
});
describe('dedupe', () => {
it('works on empty changes', () => {
expect(
getDedupedConflictsFromChanges({
localChanges: [],
remoteChanges: [],
projectPath: 'foo',
forceIgnore: forceIgnoreStub,
})
).to.deep.equal([]);
});
it('returns nothing when only 1 side is changed', () => {
expect(
getDedupedConflictsFromChanges({
localChanges: [class1Local],
remoteChanges: [],
projectPath: 'foo',
forceIgnore: forceIgnoreStub,
})
).to.deep.equal([]);
});
it('does not return non-matching changes', () => {
expect(
getDedupedConflictsFromChanges({
localChanges: [class1Local],
remoteChanges: [
{
origin: 'remote',
name: 'OtherClass',
type: 'ApexClass',
filenames: ['foo/classes/OtherClass.cls', 'foo/classes/OtherClass.cls-meta.xml'],
},
],
projectPath: 'foo',
forceIgnore: forceIgnoreStub,
})
).to.deep.equal([]);
});

it('de-dupes local and remote change where names match', () => {
const { filenames, ...simplifiedResult } = class1Local;
expect(
getDedupedConflictsFromChanges({
localChanges: [class1Local],
remoteChanges: [{ origin: 'remote', name: 'MyClass', type: 'ApexClass' }],
projectPath: 'foo',
forceIgnore: forceIgnoreStub,
})
).to.deep.equal([{ ...simplifiedResult, origin: 'remote' }]);
});
});
});
25 changes: 25 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,31 @@
mkdirp "1.0.4"
ts-retry-promise "^0.6.0"

"@salesforce/core@^3.19.2":
version "3.19.2"
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.19.2.tgz#45b55fec4425ec9a447aba682a0add084c8ba73a"
integrity sha512-uz2ehET9zz8WJj9Gicui3zxSN34TsmjgxrP22XCdQS7/GNhye1SlV8a6ayyQLqJbYlANrPFAs1RhF6LV1mPokg==
dependencies:
"@salesforce/bunyan" "^2.0.0"
"@salesforce/kit" "^1.5.41"
"@salesforce/schemas" "^1.1.0"
"@salesforce/ts-types" "^1.5.20"
"@types/graceful-fs" "^4.1.5"
"@types/mkdirp" "^1.0.2"
"@types/semver" "^7.3.9"
ajv "^8.11.0"
archiver "^5.3.0"
change-case "^4.1.2"
debug "^3.2.7"
faye "^1.4.0"
form-data "^4.0.0"
graceful-fs "^4.2.9"
js2xmlparser "^4.0.1"
jsforce "2.0.0-beta.10"
jsonwebtoken "8.5.1"
mkdirp "1.0.4"
ts-retry-promise "^0.6.0"

"@salesforce/dev-config@^3.0.0":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@salesforce/dev-config/-/dev-config-3.0.1.tgz#631a952abfd69e7cdb0fb312ba4b1656ae632b90"
Expand Down

0 comments on commit 1238a68

Please sign in to comment.