Skip to content

Commit c71e66f

Browse files
committed
feat: finish status, add clear/reset
1 parent f98ecf1 commit c71e66f

File tree

9 files changed

+320
-61
lines changed

9 files changed

+320
-61
lines changed

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ You should use the class named sourceTracking.
88

99
## TODO
1010

11+
can migrate maxRevision.json to its new home
12+
13+
This code in SourceTracking.ts is making identical queries in parallel, which could be really expensive
14+
15+
````ts
16+
if (options?.origin === 'remote') {
17+
await this.ensureRemoteTracking();
18+
const remoteChanges = await this.remoteSourceTrackingService.retrieveUpdates();
19+
20+
tracking:clear may not handle errors where it fails to delete local or remote
21+
1122
integration testing
1223

1324
Push can have partial successes and needs a proper status code ex:
@@ -88,4 +99,4 @@ Push can have partial successes and needs a proper status code ex:
8899
"status": "Failed",
89100
"success": false
90101
}
91-
```
102+
````

messages/source_tracking.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const warning =
2+
'WARNING: This command deletes or overwrites all existing source tracking files. Use with extreme caution.';
3+
4+
module.exports = {
5+
resetDescription: `reset local and remote source tracking
6+
7+
${warning}
8+
9+
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.
10+
11+
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:
12+
$ sfdx force:data:soql:query -q "SELECT MemberName, MemberType, RevisionCounter FROM SourceMember" -t`,
13+
14+
clearDescription: `clear all local source tracking information
15+
16+
${warning}
17+
18+
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.`,
19+
20+
nopromptDescription: 'do not prompt for source tracking override confirmation',
21+
revisionDescription: 'reset to a specific SourceMember revision counter number',
22+
promptMessage:
23+
'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)?',
24+
};

src/commands/source/push.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ export default class SourcePush extends SfdxCommand {
3939
// tracking.getChanges({ origin: 'local', state: 'delete' }),
4040
// tracking.getChanges({ origin: 'local', state: 'changed' }),
4141
// ]);
42-
const nonDeletes = (await tracking.getChanges({ origin: 'local', state: 'changed' }))
42+
await tracking.ensureLocalTracking();
43+
const nonDeletes = (
44+
await Promise.all([
45+
tracking.getChanges({ origin: 'local', state: 'changed' }),
46+
tracking.getChanges({ origin: 'local', state: 'add' }),
47+
])
48+
)
49+
.flat()
4350
.map((change) => change.filenames as string[])
4451
.flat();
4552
const deletes = (await tracking.getChanges({ origin: 'local', state: 'delete' }))

src/commands/source/status.ts

+27-20
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,24 @@ export default class SourceStatus extends SfdxCommand {
4242
org: this.org,
4343
project: this.project,
4444
});
45-
const outputRows: StatusResult[] = [];
45+
let outputRows: StatusResult[] = [];
4646

4747
if (this.flags.local || this.flags.all || (!this.flags.remote && !this.flags.all)) {
4848
await tracking.ensureLocalTracking();
49-
const [localDeletes, localModifies, localAdds] = await Promise.all([
50-
tracking.getChanges({ origin: 'local', state: 'delete' }),
51-
tracking.getChanges({ origin: 'local', state: 'changed' }),
52-
tracking.getChanges({ origin: 'local', state: 'add' }),
53-
]);
54-
outputRows.concat(localAdds.map((item) => this.statusResultToOutputRows(item, 'add')).flat());
55-
outputRows.concat(localModifies.map((item) => this.statusResultToOutputRows(item, 'changed')).flat());
56-
outputRows.concat(localDeletes.map((item) => this.statusResultToOutputRows(item, 'delete')).flat());
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));
58+
outputRows = outputRows.concat(localAdds.map((item) => this.statusResultToOutputRows(item, 'add')).flat());
59+
outputRows = outputRows.concat(
60+
localModifies.map((item) => this.statusResultToOutputRows(item, 'changed')).flat()
61+
);
62+
outputRows = outputRows.concat(localDeletes.map((item) => this.statusResultToOutputRows(item, 'delete')).flat());
5763
}
5864

5965
if (this.flags.remote || this.flags.all || (!this.flags.local && !this.flags.all)) {
@@ -62,17 +68,17 @@ export default class SourceStatus extends SfdxCommand {
6268
tracking.getChanges({ origin: 'remote', state: 'delete' }),
6369
tracking.getChanges({ origin: 'remote', state: 'changed' }),
6470
]);
65-
outputRows.concat(remoteDeletes.map((item) => this.statusResultToOutputRows(item, 'delete')).flat());
66-
outputRows.concat(
71+
outputRows = outputRows.concat(remoteDeletes.map((item) => this.statusResultToOutputRows(item)).flat());
72+
outputRows = outputRows.concat(
6773
remoteModifies
6874
.filter((item) => item.modified)
69-
.map((item) => this.statusResultToOutputRows(item, 'delete'))
75+
.map((item) => this.statusResultToOutputRows(item))
7076
.flat()
7177
);
72-
outputRows.concat(
78+
outputRows = outputRows.concat(
7379
remoteModifies
7480
.filter((item) => !item.modified)
75-
.map((item) => this.statusResultToOutputRows(item, 'delete'))
81+
.map((item) => this.statusResultToOutputRows(item))
7682
.flat()
7783
);
7884
}
@@ -86,12 +92,13 @@ export default class SourceStatus extends SfdxCommand {
8692
);
8793
}
8894
}
95+
8996
this.ux.table(outputRows, {
9097
columns: [
9198
{ label: 'STATE', key: 'state' },
92-
{ label: 'FULL NAME', key: 'name' },
99+
{ label: 'FULL NAME', key: 'fullName' },
93100
{ label: 'TYPE', key: 'type' },
94-
{ label: 'PROJECT PATH', key: 'filenames' },
101+
{ label: 'PROJECT PATH', key: 'filepath' },
95102
],
96103
});
97104

@@ -100,7 +107,7 @@ export default class SourceStatus extends SfdxCommand {
100107
}
101108

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

105112
const state = (): string => {
106113
if (localType) {
@@ -114,12 +121,12 @@ export default class SourceStatus extends SfdxCommand {
114121
}
115122
return 'Add';
116123
};
117-
this.logger.debug(state);
118124
const baseObject = {
119-
type: input.type || '',
125+
type: input.type || 'TODO',
120126
state: `${input.origin} ${state()}`,
121-
fullName: input.name || '',
127+
fullName: input.name || 'TODO',
122128
};
129+
this.logger.debug(baseObject);
123130

124131
if (!input.filenames) {
125132
return [baseObject];

src/commands/source/tracking/clear.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
9+
import { Messages, Org, SfdxProject } from '@salesforce/core';
10+
import * as chalk from 'chalk';
11+
import { SourceTracking } from '../../../sourceTracking';
12+
13+
Messages.importMessagesDirectory(__dirname);
14+
const messages: Messages = Messages.loadMessages('@salesforce/source-tracking', 'source_tracking');
15+
16+
export type SourceTrackingClearResult = {
17+
clearedFiles: string[];
18+
};
19+
20+
export class SourceTrackingClearCommand extends SfdxCommand {
21+
public static readonly description = messages.getMessage('clearDescription');
22+
23+
public static readonly requiresProject = true;
24+
public static readonly requiresUsername = true;
25+
26+
public static readonly flagsConfig: FlagsConfig = {
27+
noprompt: flags.boolean({
28+
char: 'p',
29+
description: messages.getMessage('nopromptDescription'),
30+
required: false,
31+
}),
32+
};
33+
34+
// valid assertions with ! because requiresProject and requiresUsername
35+
protected org!: Org;
36+
protected project!: SfdxProject;
37+
38+
public async run(): Promise<SourceTrackingClearResult> {
39+
let clearedFiles: string[] = [];
40+
if (this.flags.noprompt || (await this.ux.confirm(chalk.dim(messages.getMessage('promptMessage'))))) {
41+
const sourceTracking = new SourceTracking({ project: this.project, org: this.org });
42+
clearedFiles = await Promise.all([sourceTracking.clearLocalTracking(), sourceTracking.clearRemoteTracking()]);
43+
this.ux.log('Cleared local tracking files.');
44+
}
45+
return { clearedFiles };
46+
}
47+
}

src/commands/source/tracking/reset.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
9+
import { Messages, Org, SfdxProject } from '@salesforce/core';
10+
import * as chalk from 'chalk';
11+
import { SourceTracking } from '../../../sourceTracking';
12+
13+
Messages.importMessagesDirectory(__dirname);
14+
const messages: Messages = Messages.loadMessages('@salesforce/source-tracking', 'source_tracking');
15+
16+
export type SourceTrackingResetResult = {
17+
sourceMembersSynced: number;
18+
localPathsSynced: number;
19+
};
20+
21+
export class SourceTrackingResetCommand extends SfdxCommand {
22+
public static readonly description = messages.getMessage('resetDescription');
23+
24+
public static readonly requiresProject = true;
25+
public static readonly requiresUsername = true;
26+
27+
public static readonly flagsConfig: FlagsConfig = {
28+
revision: flags.integer({
29+
char: 'r',
30+
description: messages.getMessage('revisionDescription'),
31+
min: 0,
32+
}),
33+
noprompt: flags.boolean({
34+
char: 'p',
35+
description: messages.getMessage('nopromptDescription'),
36+
}),
37+
};
38+
39+
// valid assertions with ! because requiresProject and requiresUsername
40+
protected org!: Org;
41+
protected project!: SfdxProject;
42+
43+
public async run(): Promise<SourceTrackingResetResult> {
44+
if (this.flags.noprompt || (await this.ux.confirm(chalk.dim(messages.getMessage('promptMessage'))))) {
45+
const sourceTracking = new SourceTracking({ project: this.project, org: this.org });
46+
47+
const [remoteResets, localResets] = await Promise.all([
48+
sourceTracking.resetRemoteTracking(this.flags.revision as number),
49+
sourceTracking.resetLocalTracking(),
50+
]);
51+
52+
this.ux.log(
53+
`Reset local tracking files${this.flags.revision ? ` to revision ${this.flags.revision as number}` : ''}.`
54+
);
55+
56+
return {
57+
sourceMembersSynced: remoteResets,
58+
localPathsSynced: localResets.length,
59+
};
60+
}
61+
62+
return {
63+
sourceMembersSynced: 0,
64+
localPathsSynced: 0,
65+
};
66+
}
67+
}

src/shared/localShadowRepo.ts

+18-12
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66
*/
77
/* eslint-disable no-console */
88

9-
import * as path from 'path';
9+
import { join as pathJoin } from 'path';
1010
import * as fs from 'fs';
1111
import { AsyncCreatable } from '@salesforce/kit';
12-
import { NamedPackageDir, fs as fsCore, Logger } from '@salesforce/core';
12+
import { NamedPackageDir, Logger } from '@salesforce/core';
1313
import * as git from 'isomorphic-git';
1414

1515
/**
1616
* returns the full path to where we store the shadow repo
1717
*/
1818
const getGitDir = (orgId: string, projectPath: string): string => {
19-
return path.join(projectPath, '.sfdx', 'orgs', orgId);
19+
return pathJoin(projectPath, '.sfdx', 'orgs', orgId, 'localSourceTracking');
2020
};
2121

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

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

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

139+
/**
140+
* returns adds and modifies but not deletes
141+
*/
130142
public async getNonDeleteFilenames(): Promise<string[]> {
131143
return toFilenames(await this.getNonDeletes());
132144
}
@@ -197,20 +209,14 @@ export class ShadowRepo extends AsyncCreatable<ShadowRepoOptions> {
197209
private async stashIgnoreFile(): Promise<void> {
198210
if (!this.stashed) {
199211
this.stashed = true;
200-
await fs.promises.rename(
201-
path.join(this.projectPath, '.gitignore'),
202-
path.join(this.projectPath, '.BAK.gitignore')
203-
);
212+
await fs.promises.rename(pathJoin(this.projectPath, '.gitignore'), pathJoin(this.projectPath, '.BAK.gitignore'));
204213
}
205214
}
206215

207216
private async unStashIgnoreFile(): Promise<void> {
208217
if (this.stashed) {
209218
this.stashed = false;
210-
await fs.promises.rename(
211-
path.join(this.projectPath, '.BAK.gitignore'),
212-
path.join(this.projectPath, '.gitignore')
213-
);
219+
await fs.promises.rename(pathJoin(this.projectPath, '.BAK.gitignore'), pathJoin(this.projectPath, '.gitignore'));
214220
}
215221
}
216222
}

0 commit comments

Comments
 (0)