Skip to content

Commit

Permalink
feat: remote and conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Aug 16, 2021
1 parent 3e22774 commit f98ecf1
Show file tree
Hide file tree
Showing 10 changed files with 489 additions and 205 deletions.
86 changes: 84 additions & 2 deletions README.md
Expand Up @@ -4,6 +4,88 @@ JavaScript library for tracking local and remote Salesforce metadata changes.

**_ UNDER DEVELOPMENT _**

RemoteSourceTracking:
You should use the class named sourceTracking.

- in moving the
## TODO

integration testing

Push can have partial successes and needs a proper status code ex:

```json
{
"checkOnly": false,
"completedDate": "2021-08-14T18:03:37.000Z",
"createdBy": "005R0000009HFrL",
"createdByName": "User User",
"createdDate": "2021-08-14T18:03:34.000Z",
"details": {
"componentFailures": {
"changed": "false",
"componentType": "Profile",
"created": "false",
"createdDate": "2021-08-14T18:03:36.000Z",
"deleted": "false",
"fileName": "profiles/Admin.profile",
"fullName": "Admin",
"problem": "In field: field - no CustomField named Account.test__c found",
"problemType": "Error",
"success": "false"
},
"componentSuccesses": [
{
"changed": "true",
"componentType": "ApexClass",
"created": "true",
"createdDate": "2021-08-14T18:03:35.000Z",
"deleted": "false",
"fileName": "classes/test2.cls",
"fullName": "test2",
"id": "01pR000000DVwPqIAL",
"success": "true"
},
{
"changed": "true",
"componentType": "ApexClass",
"created": "true",
"createdDate": "2021-08-14T18:03:35.000Z",
"deleted": "false",
"fileName": "classes/test.cls",
"fullName": "test",
"id": "01pR000000DVwPpIAL",
"success": "true"
},
{
"changed": "true",
"componentType": "",
"created": "false",
"createdDate": "2021-08-14T18:03:36.000Z",
"deleted": "false",
"fileName": "package.xml",
"fullName": "package.xml",
"success": "true"
}
],
"runTestResult": {
"numFailures": "0",
"numTestsRun": "0",
"totalTime": "0.0"
}
},
"done": true,
"id": "0AfR000001SjwjQKAR",
"ignoreWarnings": false,
"lastModifiedDate": "2021-08-14T18:03:37.000Z",
"numberComponentErrors": 1,
"numberComponentsDeployed": 2,
"numberComponentsTotal": 3,
"numberTestErrors": 0,
"numberTestsCompleted": 0,
"numberTestsTotal": 0,
"rollbackOnError": true,
"runTestsEnabled": false,
"startDate": "2021-08-14T18:03:34.000Z",
"status": "Failed",
"success": false
}
```
39 changes: 18 additions & 21 deletions src/commands/source/pull.ts
Expand Up @@ -9,7 +9,7 @@ import { unlink } from 'fs/promises';
import { FlagsConfig, flags, SfdxCommand } from '@salesforce/command';

import { SfdxProject, Org } from '@salesforce/core';
import { ComponentSet } from '@salesforce/source-deploy-retrieve';
import { ComponentSet, ComponentStatus } from '@salesforce/source-deploy-retrieve';
import { writeConflictTable } from '../../writeConflictTable';
import { SourceTracking, ChangeResult } from '../../sourceTracking';

Expand Down Expand Up @@ -71,13 +71,17 @@ export default class SourcePull extends SfdxCommand {
.flat()
.filter(Boolean);
await Promise.all(filenames.map((filename) => unlink(filename)));
await tracking.updateLocal({ deletedFiles: filenames });
await Promise.all([
tracking.updateLocalTracking({ deletedFiles: filenames }),
tracking.updateRemoteTracking(
changesToDeleteWithFilePaths.map((change) => ({ type: change.type as string, name: change.name as string }))
),
]);
}

// we might skip this and do only local deletes!
if (componentSetFromRemoteChanges.size === 0) {
this.ux.stopSpinner('No remote adds/modifications to merge locally');
await tracking.updateRemote();
return;
}

Expand All @@ -93,33 +97,26 @@ export default class SourcePull extends SfdxCommand {
});
this.ux.setSpinnerStatus('waiting for the retrieve results');
const retrieveResult = await mdapiRetrieve.pollStatus(1000);

this.ux.setSpinnerStatus('updating source tracking files');
// TODO: those remote deletes need to delete the local source!
this.ux.warn(
`Delete not yet implemented in. Would have deleted ${
changesToDelete.length > 0 ? changesToDelete.map((change) => `${change.filepath}`).join(',') : 'nothing'
}`
);
this.ux.setSpinnerStatus('updating source tracking files');

const successes = retrieveResult
.getFileResponses()
.filter((fileResponse) => fileResponse.state !== ComponentStatus.Failed);

this.logger.debug(
'files received from the server are',
retrieveResult
.getFileResponses()
.map((fileResponse) => fileResponse.filePath as string)
.filter(Boolean)
successes.map((fileResponse) => fileResponse.filePath as string).filter(Boolean)
);

await Promise.all([
// commit the local file changes that the retrieve modified
tracking.updateLocal({
files: retrieveResult
.getFileResponses()
.map((fileResponse) => fileResponse.filePath as string)
.filter(Boolean),
tracking.updateLocalTracking({
files: successes.map((fileResponse) => fileResponse.filePath as string).filter(Boolean),
}),
// calling with no metadata types gets the latest sourceMembers from the org
tracking.updateRemote(),
tracking.updateRemoteTracking(
successes.map((fileResponse) => ({ name: fileResponse.fullName, type: fileResponse.type }))
),
]);
return retrieveResult.response;
}
Expand Down
31 changes: 18 additions & 13 deletions src/commands/source/push.ts
Expand Up @@ -8,7 +8,7 @@
import { FlagsConfig, flags, SfdxCommand } from '@salesforce/command';
import { SfdxProject, Org } from '@salesforce/core';
import { ComponentSet, FileResponse, ComponentStatus } from '@salesforce/source-deploy-retrieve';
import { SourceTracking } from '../../sourceTracking';
import { MetadataKeyPair, SourceTracking } from '../../sourceTracking';
import { writeConflictTable } from '../../writeConflictTable';

export default class SourcePush extends SfdxCommand {
Expand Down Expand Up @@ -52,35 +52,40 @@ export default class SourcePush extends SfdxCommand {
return [];
}

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

// this.ux.log(`should build component set from ${nonDeletes.join(',')}`);
const componentSet = ComponentSet.fromSource({ fsPaths: nonDeletes });
const deploy = await componentSet.deploy({ usernameOrConnection: this.org.getUsername() as string });
const result = await deploy.pollStatus();

const successes = result.getFileResponses().filter((fileResponse) => fileResponse.state !== ComponentStatus.Failed);
// then commit successes to local tracking;
await tracking.updateLocal({
files: result
.getFileResponses()
.filter((fileResponse) => fileResponse.state !== ComponentStatus.Failed)
.map((fileResponse) => fileResponse.filePath) as string[],
await tracking.updateLocalTracking({
files: successes.map((fileResponse) => fileResponse.filePath) as string[],
});
if (!this.flags.json) {
this.ux.logJson(result.response);
}
// and update the remote tracking
const successComponentNames = (
const successComponentKeys = (
Array.isArray(result.response.details.componentSuccesses)
? result.response.details.componentSuccesses
: [result.response.details.componentSuccesses]
)
.filter((success) => success?.componentType && success.fullName) // we don't want package.xml
.map((success) => success?.fullName as string);
.map((success) =>
success?.fullName && success?.componentType
? { name: success?.fullName, type: success?.componentType }
: undefined
)
.filter(Boolean) as MetadataKeyPair[]; // we don't want package.xml

// this includes polling for sourceMembers
await tracking.updateRemote(successComponentNames);
await tracking.updateRemoteTracking(successComponentKeys);
return result.getFileResponses();
}
}
116 changes: 79 additions & 37 deletions src/commands/source/status.ts
Expand Up @@ -9,24 +9,11 @@ import { FlagsConfig, flags, SfdxCommand } from '@salesforce/command';
import { SfdxProject, Org } from '@salesforce/core';

import { ChangeResult, SourceTracking } from '../../sourceTracking';

// array members for status results
// https://isomorphic-git.org/docs/en/statusMatrix#docsNav
// const FILE = 0;
// const HEAD = 1;
// const WORKDIR = 2;

interface TemporaryOutput {
local?: {
adds: ChangeResult[];
deletes: ChangeResult[];
modifies: ChangeResult[];
};
remote?: {
deletes: ChangeResult[];
modifies: ChangeResult[];
};
conflicts?: ChangeResult[];
export interface StatusResult {
state: string;
fullName: string;
type: string;
filePath?: string;
}

export default class SourceStatus extends SfdxCommand {
Expand All @@ -41,50 +28,105 @@ export default class SourceStatus extends SfdxCommand {
protected project!: SfdxProject;
protected org!: Org;

public async run(): Promise<TemporaryOutput> {
protected localAdds: ChangeResult[] = [];

public async run(): Promise<StatusResult[]> {
this.logger.debug(
`project is ${this.project.getPath()} and pkgDirs are ${this.project
.getPackageDirectories()
.map((dir) => dir.path)
.join(',')}`
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const output: TemporaryOutput = {};
const tracking = new SourceTracking({
org: this.org,
project: this.project,
});
const outputRows: StatusResult[] = [];

if (this.flags.local || this.flags.all || (!this.flags.remote && !this.flags.all)) {
await tracking.ensureLocalTracking();
const [deletes, modifies, adds] = await Promise.all([
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' }),
]);
output.local = {
deletes,
modifies,
adds,
};
outputRows.concat(localAdds.map((item) => this.statusResultToOutputRows(item, 'add')).flat());
outputRows.concat(localModifies.map((item) => this.statusResultToOutputRows(item, 'changed')).flat());
outputRows.concat(localDeletes.map((item) => this.statusResultToOutputRows(item, 'delete')).flat());
}

if (this.flags.remote || this.flags.all || (!this.flags.local && !this.flags.all)) {
await tracking.ensureRemoteTracking();

const deletes = await tracking.getChanges({ origin: 'remote', state: 'delete' });
const modifies = await tracking.getChanges({ origin: 'remote', state: 'changed' });
output.remote = {
deletes: tracking.populateFilePaths(deletes),
modifies: tracking.populateFilePaths(modifies),
};
const [remoteDeletes, remoteModifies] = await Promise.all([
tracking.getChanges({ origin: 'remote', state: 'delete' }),
tracking.getChanges({ origin: 'remote', state: 'changed' }),
]);
outputRows.concat(remoteDeletes.map((item) => this.statusResultToOutputRows(item, 'delete')).flat());
outputRows.concat(
remoteModifies
.filter((item) => item.modified)
.map((item) => this.statusResultToOutputRows(item, 'delete'))
.flat()
);
outputRows.concat(
remoteModifies
.filter((item) => !item.modified)
.map((item) => this.statusResultToOutputRows(item, 'delete'))
.flat()
);
}

output.conflicts = await tracking.getConflicts();
if (!this.flags.json) {
this.ux.logJson(output);
if (!this.flags.local && !this.flags.remote) {
// a flat array of conflict filenames
const conflictFilenames = (await tracking.getConflicts()).map((conflict) => conflict.filenames).flat();
if (conflictFilenames.length > 0) {
outputRows.map((row) =>
conflictFilenames.includes(row.filePath) ? { ...row, state: `${row.state} (Conflict)` } : row
);
}
}
this.ux.table(outputRows, {
columns: [
{ label: 'STATE', key: 'state' },
{ label: 'FULL NAME', key: 'name' },
{ label: 'TYPE', key: 'type' },
{ label: 'PROJECT PATH', key: 'filenames' },
],
});

// convert things into the output format to match the existing command
return outputRows;
}

return output;
private statusResultToOutputRows(input: ChangeResult, localType?: 'delete' | 'changed' | 'add'): StatusResult[] {
this.logger.debug(input);

const state = (): string => {
if (localType) {
return localType[0].toUpperCase() + localType.substring(1);
}
if (input.deleted) {
return 'Delete';
}
if (input.modified) {
return 'Changed';
}
return 'Add';
};
this.logger.debug(state);
const baseObject = {
type: input.type || '',
state: `${input.origin} ${state()}`,
fullName: input.name || '',
};

if (!input.filenames) {
return [baseObject];
}
return input.filenames.map((filename) => ({
...baseObject,
filepath: filename,
}));
}
}

0 comments on commit f98ecf1

Please sign in to comment.