Skip to content

Commit

Permalink
feat: finish status, add clear/reset
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Aug 19, 2021
1 parent f98ecf1 commit c71e66f
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 61 deletions.
13 changes: 12 additions & 1 deletion README.md
Expand Up @@ -8,6 +8,17 @@ You should use the class named sourceTracking.

## TODO

can migrate maxRevision.json to its new home

This code in SourceTracking.ts is making identical queries in parallel, which could be really expensive

````ts
if (options?.origin === 'remote') {
await this.ensureRemoteTracking();
const remoteChanges = await this.remoteSourceTrackingService.retrieveUpdates();

tracking:clear may not handle errors where it fails to delete local or remote

integration testing

Push can have partial successes and needs a proper status code ex:
Expand Down Expand Up @@ -88,4 +99,4 @@ Push can have partial successes and needs a proper status code ex:
"status": "Failed",
"success": false
}
```
````
24 changes: 24 additions & 0 deletions messages/source_tracking.js
@@ -0,0 +1,24 @@
const warning =
'WARNING: This command deletes or overwrites all existing source tracking files. Use with extreme caution.';

module.exports = {
resetDescription: `reset local and remote source tracking
${warning}
Resets local and remote source tracking so that the CLI no longer registers differences between your local files and those in the org. When you next run force:source:status, the CLI returns no results, even though conflicts might actually exist. The CLI then resumes tracking new source changes as usual.
Use the --revision parameter to reset source tracking to a specific revision number of an org source member. To get the revision number, query the SourceMember Tooling API object with the force:data:soql:query command. For example:
$ sfdx force:data:soql:query -q "SELECT MemberName, MemberType, RevisionCounter FROM SourceMember" -t`,

clearDescription: `clear all local source tracking information
${warning}
Clears all local source tracking information. When you next run force:source:status, the CLI displays all local and remote files as changed, and any files with the same name are listed as conflicts.`,

nopromptDescription: 'do not prompt for source tracking override confirmation',
revisionDescription: 'reset to a specific SourceMember revision counter number',
promptMessage:
'WARNING: This operation will modify all your local source tracking files. The operation can have unintended consequences on all the force:source commands. Are you sure you want to proceed (y/n)?',
};
9 changes: 8 additions & 1 deletion src/commands/source/push.ts
Expand Up @@ -39,7 +39,14 @@ export default class SourcePush extends SfdxCommand {
// tracking.getChanges({ origin: 'local', state: 'delete' }),
// tracking.getChanges({ origin: 'local', state: 'changed' }),
// ]);
const nonDeletes = (await tracking.getChanges({ origin: 'local', state: 'changed' }))
await tracking.ensureLocalTracking();
const nonDeletes = (
await Promise.all([
tracking.getChanges({ origin: 'local', state: 'changed' }),
tracking.getChanges({ origin: 'local', state: 'add' }),
])
)
.flat()
.map((change) => change.filenames as string[])
.flat();
const deletes = (await tracking.getChanges({ origin: 'local', state: 'delete' }))
Expand Down
47 changes: 27 additions & 20 deletions src/commands/source/status.ts
Expand Up @@ -42,18 +42,24 @@ export default class SourceStatus extends SfdxCommand {
org: this.org,
project: this.project,
});
const outputRows: StatusResult[] = [];
let outputRows: StatusResult[] = [];

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' }),
]);
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());
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));
outputRows = outputRows.concat(localAdds.map((item) => this.statusResultToOutputRows(item, 'add')).flat());
outputRows = outputRows.concat(
localModifies.map((item) => this.statusResultToOutputRows(item, 'changed')).flat()
);
outputRows = outputRows.concat(localDeletes.map((item) => this.statusResultToOutputRows(item, 'delete')).flat());
}

if (this.flags.remote || this.flags.all || (!this.flags.local && !this.flags.all)) {
Expand All @@ -62,17 +68,17 @@ export default class SourceStatus extends SfdxCommand {
tracking.getChanges({ origin: 'remote', state: 'delete' }),
tracking.getChanges({ origin: 'remote', state: 'changed' }),
]);
outputRows.concat(remoteDeletes.map((item) => this.statusResultToOutputRows(item, 'delete')).flat());
outputRows.concat(
outputRows = outputRows.concat(remoteDeletes.map((item) => this.statusResultToOutputRows(item)).flat());
outputRows = outputRows.concat(
remoteModifies
.filter((item) => item.modified)
.map((item) => this.statusResultToOutputRows(item, 'delete'))
.map((item) => this.statusResultToOutputRows(item))
.flat()
);
outputRows.concat(
outputRows = outputRows.concat(
remoteModifies
.filter((item) => !item.modified)
.map((item) => this.statusResultToOutputRows(item, 'delete'))
.map((item) => this.statusResultToOutputRows(item))
.flat()
);
}
Expand All @@ -86,12 +92,13 @@ export default class SourceStatus extends SfdxCommand {
);
}
}

this.ux.table(outputRows, {
columns: [
{ label: 'STATE', key: 'state' },
{ label: 'FULL NAME', key: 'name' },
{ label: 'FULL NAME', key: 'fullName' },
{ label: 'TYPE', key: 'type' },
{ label: 'PROJECT PATH', key: 'filenames' },
{ label: 'PROJECT PATH', key: 'filepath' },
],
});

Expand All @@ -100,7 +107,7 @@ export default class SourceStatus extends SfdxCommand {
}

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

const state = (): string => {
if (localType) {
Expand All @@ -114,12 +121,12 @@ export default class SourceStatus extends SfdxCommand {
}
return 'Add';
};
this.logger.debug(state);
const baseObject = {
type: input.type || '',
type: input.type || 'TODO',
state: `${input.origin} ${state()}`,
fullName: input.name || '',
fullName: input.name || 'TODO',
};
this.logger.debug(baseObject);

if (!input.filenames) {
return [baseObject];
Expand Down
47 changes: 47 additions & 0 deletions src/commands/source/tracking/clear.ts
@@ -0,0 +1,47 @@
/*
* 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 { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
import { Messages, Org, SfdxProject } from '@salesforce/core';
import * as chalk from 'chalk';
import { SourceTracking } from '../../../sourceTracking';

Messages.importMessagesDirectory(__dirname);
const messages: Messages = Messages.loadMessages('@salesforce/source-tracking', 'source_tracking');

export type SourceTrackingClearResult = {
clearedFiles: string[];
};

export class SourceTrackingClearCommand extends SfdxCommand {
public static readonly description = messages.getMessage('clearDescription');

public static readonly requiresProject = true;
public static readonly requiresUsername = true;

public static readonly flagsConfig: FlagsConfig = {
noprompt: flags.boolean({
char: 'p',
description: messages.getMessage('nopromptDescription'),
required: false,
}),
};

// valid assertions with ! because requiresProject and requiresUsername
protected org!: Org;
protected project!: SfdxProject;

public async run(): Promise<SourceTrackingClearResult> {
let clearedFiles: string[] = [];
if (this.flags.noprompt || (await this.ux.confirm(chalk.dim(messages.getMessage('promptMessage'))))) {
const sourceTracking = new SourceTracking({ project: this.project, org: this.org });
clearedFiles = await Promise.all([sourceTracking.clearLocalTracking(), sourceTracking.clearRemoteTracking()]);
this.ux.log('Cleared local tracking files.');
}
return { clearedFiles };
}
}
67 changes: 67 additions & 0 deletions src/commands/source/tracking/reset.ts
@@ -0,0 +1,67 @@
/*
* 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 { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
import { Messages, Org, SfdxProject } from '@salesforce/core';
import * as chalk from 'chalk';
import { SourceTracking } from '../../../sourceTracking';

Messages.importMessagesDirectory(__dirname);
const messages: Messages = Messages.loadMessages('@salesforce/source-tracking', 'source_tracking');

export type SourceTrackingResetResult = {
sourceMembersSynced: number;
localPathsSynced: number;
};

export class SourceTrackingResetCommand extends SfdxCommand {
public static readonly description = messages.getMessage('resetDescription');

public static readonly requiresProject = true;
public static readonly requiresUsername = true;

public static readonly flagsConfig: FlagsConfig = {
revision: flags.integer({
char: 'r',
description: messages.getMessage('revisionDescription'),
min: 0,
}),
noprompt: flags.boolean({
char: 'p',
description: messages.getMessage('nopromptDescription'),
}),
};

// valid assertions with ! because requiresProject and requiresUsername
protected org!: Org;
protected project!: SfdxProject;

public async run(): Promise<SourceTrackingResetResult> {
if (this.flags.noprompt || (await this.ux.confirm(chalk.dim(messages.getMessage('promptMessage'))))) {
const sourceTracking = new SourceTracking({ project: this.project, org: this.org });

const [remoteResets, localResets] = await Promise.all([
sourceTracking.resetRemoteTracking(this.flags.revision as number),
sourceTracking.resetLocalTracking(),
]);

this.ux.log(
`Reset local tracking files${this.flags.revision ? ` to revision ${this.flags.revision as number}` : ''}.`
);

return {
sourceMembersSynced: remoteResets,
localPathsSynced: localResets.length,
};
}

return {
sourceMembersSynced: 0,
localPathsSynced: 0,
};
}
}
30 changes: 18 additions & 12 deletions src/shared/localShadowRepo.ts
Expand Up @@ -6,17 +6,17 @@
*/
/* eslint-disable no-console */

import * as path from 'path';
import { join as pathJoin } from 'path';
import * as fs from 'fs';
import { AsyncCreatable } from '@salesforce/kit';
import { NamedPackageDir, fs as fsCore, Logger } from '@salesforce/core';
import { NamedPackageDir, Logger } from '@salesforce/core';
import * as git from 'isomorphic-git';

/**
* returns the full path to where we store the shadow repo
*/
const getGitDir = (orgId: string, projectPath: string): string => {
return path.join(projectPath, '.sfdx', 'orgs', orgId);
return pathJoin(projectPath, '.sfdx', 'orgs', orgId, 'localSourceTracking');
};

const toFilenames = (rows: StatusRow[]): string[] => rows.map((file) => file[FILE] as string);
Expand Down Expand Up @@ -74,10 +74,14 @@ export class ShadowRepo extends AsyncCreatable<ShadowRepoOptions> {
*
*/
public async gitInit(): Promise<void> {
await fsCore.mkdirp(this.gitDir);
await fs.promises.mkdir(this.gitDir, { recursive: true });
await git.init({ fs, dir: this.projectPath, gitdir: this.gitDir, defaultBranch: 'main' });
}

public async delete(): Promise<string> {
await fs.promises.rm(this.gitDir, { recursive: true, force: true });
return this.gitDir;
}
/**
* If the status already exists, return it. Otherwise, set the status before returning.
* It's kinda like a cache
Expand All @@ -95,6 +99,8 @@ export class ShadowRepo extends AsyncCreatable<ShadowRepoOptions> {
dir: this.projectPath,
gitdir: this.gitDir,
filepaths: this.packageDirs.map((dir) => dir.path),
// filter out hidden files
filter: (f) => !f.includes('/.'),
});
await this.unStashIgnoreFile();
}
Expand All @@ -108,6 +114,9 @@ export class ShadowRepo extends AsyncCreatable<ShadowRepoOptions> {
return (await this.getStatus()).filter((file) => file[HEAD] !== file[WORKDIR]);
}

/**
* returns any change (add, modify, delete)
*/
public async getChangedFilenames(): Promise<string[]> {
return toFilenames(await this.getChangedRows());
}
Expand All @@ -127,6 +136,9 @@ export class ShadowRepo extends AsyncCreatable<ShadowRepoOptions> {
return (await this.getStatus()).filter((file) => file[WORKDIR] === 2);
}

/**
* returns adds and modifies but not deletes
*/
public async getNonDeleteFilenames(): Promise<string[]> {
return toFilenames(await this.getNonDeletes());
}
Expand Down Expand Up @@ -197,20 +209,14 @@ export class ShadowRepo extends AsyncCreatable<ShadowRepoOptions> {
private async stashIgnoreFile(): Promise<void> {
if (!this.stashed) {
this.stashed = true;
await fs.promises.rename(
path.join(this.projectPath, '.gitignore'),
path.join(this.projectPath, '.BAK.gitignore')
);
await fs.promises.rename(pathJoin(this.projectPath, '.gitignore'), pathJoin(this.projectPath, '.BAK.gitignore'));
}
}

private async unStashIgnoreFile(): Promise<void> {
if (this.stashed) {
this.stashed = false;
await fs.promises.rename(
path.join(this.projectPath, '.BAK.gitignore'),
path.join(this.projectPath, '.gitignore')
);
await fs.promises.rename(pathJoin(this.projectPath, '.BAK.gitignore'), pathJoin(this.projectPath, '.gitignore'));
}
}
}

0 comments on commit c71e66f

Please sign in to comment.