Skip to content

Commit 40616f0

Browse files
authored
Merge pull request #627 from salesforcecli/wr/retrieveTargetDir
--output-dir
2 parents a84e5ac + dc26d21 commit 40616f0

File tree

10 files changed

+445
-525
lines changed

10 files changed

+445
-525
lines changed

CHANGELOG.md

Lines changed: 164 additions & 493 deletions
Large diffs are not rendered by default.

command-snapshot.json

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
"command": "deploy",
44
"plugin": "@salesforce/plugin-deploy-retrieve",
55
"flags": ["interactive"],
6-
"alias": []
6+
"alias": [],
7+
"flagChars": [],
8+
"flagAliases": []
79
},
810
{
911
"command": "project:convert:mdapi",
1012
"plugin": "@salesforce/plugin-deploy-retrieve",
1113
"flags": ["api-version", "json", "loglevel", "manifest", "metadata", "metadata-dir", "output-dir", "root-dir"],
12-
"alias": ["force:mdapi:convert"]
14+
"alias": ["force:mdapi:convert"],
15+
"flagChars": ["d", "m", "p", "r", "x"],
16+
"flagAliases": ["apiversion", "metadatapath", "outputdir", "rootdir"]
1317
},
1418
{
1519
"command": "project:convert:source",
@@ -25,7 +29,9 @@
2529
"root-dir",
2630
"source-dir"
2731
],
28-
"alias": ["force:source:convert"]
32+
"alias": ["force:source:convert"],
33+
"flagChars": ["d", "m", "n", "p", "r", "x"],
34+
"flagAliases": ["apiversion", "outputdir", "packagename", "rootdir", "sourcepath"]
2935
},
3036
{
3137
"command": "project:delete:source",
@@ -45,37 +51,59 @@
4551
"verbose",
4652
"wait"
4753
],
48-
"alias": ["force:source:delete"]
54+
"alias": ["force:source:delete"],
55+
"flagChars": ["c", "f", "l", "m", "o", "p", "r", "t", "w"],
56+
"flagAliases": [
57+
"apiversion",
58+
"checkonly",
59+
"forceoverwrite",
60+
"noprompt",
61+
"sourcepath",
62+
"targetusername",
63+
"testlevel",
64+
"tracksource",
65+
"u"
66+
]
4967
},
5068
{
5169
"command": "project:delete:tracking",
5270
"plugin": "@salesforce/plugin-deploy-retrieve",
5371
"flags": ["api-version", "json", "loglevel", "no-prompt", "target-org"],
54-
"alias": ["force:source:tracking:clear"]
72+
"alias": ["force:source:tracking:clear"],
73+
"flagChars": ["o", "p"],
74+
"flagAliases": ["apiversion", "noprompt", "targetusername", "u"]
5575
},
5676
{
5777
"command": "project:deploy:cancel",
5878
"plugin": "@salesforce/plugin-deploy-retrieve",
5979
"flags": ["async", "job-id", "json", "use-most-recent", "wait"],
60-
"alias": ["deploy:metadata:cancel"]
80+
"alias": ["deploy:metadata:cancel"],
81+
"flagChars": ["i", "r", "w"],
82+
"flagAliases": []
6183
},
6284
{
6385
"command": "project:deploy:preview",
6486
"plugin": "@salesforce/plugin-deploy-retrieve",
6587
"flags": ["ignore-conflicts", "json", "manifest", "metadata", "source-dir", "target-org"],
66-
"alias": ["deploy:metadata:preview"]
88+
"alias": ["deploy:metadata:preview"],
89+
"flagChars": ["c", "d", "m", "o", "x"],
90+
"flagAliases": []
6791
},
6892
{
6993
"command": "project:deploy:quick",
7094
"plugin": "@salesforce/plugin-deploy-retrieve",
7195
"flags": ["api-version", "async", "concise", "job-id", "json", "target-org", "use-most-recent", "verbose", "wait"],
72-
"alias": ["deploy:metadata:quick"]
96+
"alias": ["deploy:metadata:quick"],
97+
"flagChars": ["a", "i", "o", "r", "w"],
98+
"flagAliases": []
7399
},
74100
{
75101
"command": "project:deploy:report",
76102
"plugin": "@salesforce/plugin-deploy-retrieve",
77103
"flags": ["coverage-formatters", "job-id", "json", "junit", "results-dir", "use-most-recent"],
78-
"alias": ["deploy:metadata:report"]
104+
"alias": ["deploy:metadata:report"],
105+
"flagChars": ["i", "r"],
106+
"flagAliases": []
79107
},
80108
{
81109
"command": "project:deploy:resume",
@@ -91,7 +119,9 @@
91119
"verbose",
92120
"wait"
93121
],
94-
"alias": ["deploy:metadata:resume"]
122+
"alias": ["deploy:metadata:resume"],
123+
"flagChars": ["i", "r", "w"],
124+
"flagAliases": []
95125
},
96126
{
97127
"command": "project:deploy:start",
@@ -122,7 +152,9 @@
122152
"verbose",
123153
"wait"
124154
],
125-
"alias": ["deploy:metadata"]
155+
"alias": ["deploy:metadata"],
156+
"flagChars": ["a", "c", "d", "g", "l", "m", "o", "r", "t", "w", "x"],
157+
"flagAliases": []
126158
},
127159
{
128160
"command": "project:deploy:validate",
@@ -143,7 +175,9 @@
143175
"verbose",
144176
"wait"
145177
],
146-
"alias": ["deploy:metadata:validate"]
178+
"alias": ["deploy:metadata:validate"],
179+
"flagChars": ["a", "d", "l", "m", "o", "t", "w", "x"],
180+
"flagAliases": []
147181
},
148182
{
149183
"command": "project:generate:manifest",
@@ -160,25 +194,42 @@
160194
"source-dir",
161195
"type"
162196
],
163-
"alias": ["force:source:manifest:create"]
197+
"alias": ["force:source:manifest:create"],
198+
"flagChars": ["c", "d", "m", "n", "p", "t"],
199+
"flagAliases": [
200+
"apiversion",
201+
"fromorg",
202+
"includepackages",
203+
"manifestname",
204+
"manifesttype",
205+
"o",
206+
"outputdir",
207+
"sourcepath"
208+
]
164209
},
165210
{
166211
"command": "project:list:ignored",
167212
"plugin": "@salesforce/plugin-deploy-retrieve",
168213
"flags": ["json", "source-dir"],
169-
"alias": ["force:source:ignored:list"]
214+
"alias": ["force:source:ignored:list"],
215+
"flagChars": ["p"],
216+
"flagAliases": ["sourcepath"]
170217
},
171218
{
172219
"command": "project:reset:tracking",
173220
"plugin": "@salesforce/plugin-deploy-retrieve",
174221
"flags": ["api-version", "json", "loglevel", "no-prompt", "revision", "target-org"],
175-
"alias": ["force:source:tracking:reset"]
222+
"alias": ["force:source:tracking:reset"],
223+
"flagChars": ["o", "p", "r"],
224+
"flagAliases": ["apiversion", "noprompt", "targetusername", "u"]
176225
},
177226
{
178227
"command": "project:retrieve:preview",
179228
"plugin": "@salesforce/plugin-deploy-retrieve",
180229
"flags": ["ignore-conflicts", "json", "target-org"],
181-
"alias": ["retrieve:metadata:preview"]
230+
"alias": ["retrieve:metadata:preview"],
231+
"flagChars": ["c", "o"],
232+
"flagAliases": []
182233
},
183234
{
184235
"command": "project:retrieve:start",
@@ -189,6 +240,7 @@
189240
"json",
190241
"manifest",
191242
"metadata",
243+
"output-dir",
192244
"package-name",
193245
"single-package",
194246
"source-dir",
@@ -198,6 +250,8 @@
198250
"wait",
199251
"zip-file-name"
200252
],
201-
"alias": ["retrieve:metadata"]
253+
"alias": ["retrieve:metadata"],
254+
"flagChars": ["a", "c", "d", "m", "n", "o", "r", "t", "w", "x", "z"],
255+
"flagAliases": []
202256
}
203257
]

messages/retrieve.metadata.md renamed to messages/retrieve.start.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,17 @@ Wrote retrieve zip file to %s.
166166
# info.ExtractedZipFile
167167

168168
Extracted %s to %s.
169+
170+
# flags.output-dir.description
171+
172+
The root of the directory structure into which the source files are retrieved.
173+
If the target directory matches one of the package directories in your sfdx-project.json file, the command fails.
174+
Running the command multiple times with the same target adds new files and overwrites existing files.
175+
176+
# flags.output-dir.summary
177+
178+
Directory root for the retrieved source files.
179+
180+
# retrieveTargetDirOverlapsPackage
181+
182+
The retrieve target directory [%s] overlaps one of your package directories. Specify a different retrieve target directory and try again.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"tslib": "^2"
1919
},
2020
"devDependencies": {
21-
"@oclif/plugin-command-snapshot": "^3.3.14",
21+
"@oclif/plugin-command-snapshot": "^4.0.1",
2222
"@salesforce/cli-plugins-testkit": "^3.4.0",
2323
"@salesforce/dev-config": "^3.1.0",
2424
"@salesforce/dev-scripts": "^4.3.1",
@@ -270,4 +270,4 @@
270270
"output": []
271271
}
272272
}
273-
}
273+
}

src/commands/project/retrieve/start.ts

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
*/
77

88
import { rm } from 'fs/promises';
9-
import { join, resolve } from 'path';
9+
import { dirname, join, resolve } from 'path';
1010

11+
import * as fs from 'fs';
1112
import { EnvironmentVariable, Messages, OrgConfigProperties, SfError, SfProject } from '@salesforce/core';
1213
import {
1314
RetrieveResult,
@@ -29,9 +30,10 @@ import { MetadataRetrieveResultFormatter } from '../../../formatters/metadataRet
2930
import { getPackageDirs } from '../../../utils/project';
3031
import { RetrieveResultJson } from '../../../utils/types';
3132
import { writeConflictTable } from '../../../utils/conflicts';
33+
import { promisesQueue } from '../../../utils/promiseQueue';
3234

3335
Messages.importMessagesDirectory(__dirname);
34-
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'retrieve.metadata');
36+
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'retrieve.start');
3537
const mdTransferMessages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'metadata.transfer');
3638

3739
type Format = 'source' | 'metadata';
@@ -74,6 +76,12 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
7476
summary: messages.getMessage('flags.package-name.summary'),
7577
multiple: true,
7678
}),
79+
'output-dir': Flags.directory({
80+
char: 'r',
81+
summary: messages.getMessage('flags.output-dir.summary'),
82+
description: messages.getMessage('flags.output-dir.description'),
83+
exclusive: ['package-name', 'source-dir'],
84+
}),
7785
'single-package': Flags.boolean({
7886
summary: messages.getMessage('flags.single-package.summary'),
7987
dependsOn: ['target-metadata-dir'],
@@ -143,6 +151,13 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
143151

144152
public async run(): Promise<RetrieveResultJson> {
145153
const { flags } = await this.parse(RetrieveMetadata);
154+
let resolvedTargetDir: string | undefined;
155+
if (flags['output-dir']) {
156+
resolvedTargetDir = resolve(flags['output-dir']);
157+
if (SfProject.getInstance()?.getPackageNameFromPath(resolvedTargetDir)) {
158+
throw messages.createError('retrieveTargetDirOverlapsPackage', [flags['output-dir']]);
159+
}
160+
}
146161
const format: Format = flags['target-metadata-dir'] ? 'metadata' : 'source';
147162
const zipFileName = flags['zip-file-name'] ?? DEFAULT_ZIP_FILE_NAME;
148163

@@ -152,7 +167,7 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
152167
flags,
153168
format
154169
);
155-
const retrieveOpts = await buildRetrieveOptions(flags, format, zipFileName);
170+
const retrieveOpts = await buildRetrieveOptions(flags, format, zipFileName, resolvedTargetDir);
156171

157172
this.spinner.status = messages.getMessage('spinner.sending', [
158173
componentSetFromNonDeletes.sourceApiVersion ?? componentSetFromNonDeletes.apiVersion,
@@ -181,6 +196,11 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
181196

182197
this.spinner.stop();
183198

199+
// flags['output-dir'] will set resolvedTargetDir var, so this check is redundant, but allows for nice typings in the moveResultsForRetrieveTargetDir method
200+
if (flags['output-dir'] && resolvedTargetDir) {
201+
await this.moveResultsForRetrieveTargetDir(flags['output-dir'], resolvedTargetDir);
202+
}
203+
184204
// reference the flag instead of `format` so we get correct type
185205
const formatter = flags['target-metadata-dir']
186206
? new MetadataRetrieveResultFormatter(this.retrieveResult, {
@@ -227,6 +247,52 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
227247

228248
return super.catch(error);
229249
}
250+
251+
private async moveResultsForRetrieveTargetDir(targetDir: string, resolvedTargetDir: string): Promise<void> {
252+
async function mv(src: string): Promise<string[]> {
253+
let directories: string[] = [];
254+
let files: string[] = [];
255+
const srcStat = await fs.promises.stat(src);
256+
if (srcStat.isDirectory()) {
257+
const contents = await fs.promises.readdir(src, { withFileTypes: true });
258+
[directories, files] = contents.reduce<[string[], string[]]>(
259+
(acc, dirent) => {
260+
if (dirent.isDirectory()) {
261+
acc[0].push(dirent.name);
262+
} else {
263+
acc[1].push(dirent.name);
264+
}
265+
return acc;
266+
},
267+
[[], []]
268+
);
269+
270+
directories = directories.map((dir) => join(src, dir));
271+
} else {
272+
files.push(src);
273+
}
274+
await promisesQueue(
275+
files,
276+
async (file: string): Promise<string> => {
277+
const dest = join(src.replace(join('main', 'default'), ''), file);
278+
const destDir = dirname(dest);
279+
await fs.promises.mkdir(destDir, { recursive: true });
280+
await fs.promises.rename(join(src, file), dest);
281+
return dest;
282+
},
283+
50
284+
);
285+
return directories;
286+
}
287+
// getFileResponses fails once the files have been moved, calculate where they're moved to, and then move them
288+
this.retrieveResult.getFileResponses().forEach((fileResponse) => {
289+
fileResponse.filePath = fileResponse.filePath?.replace(join('main', 'default'), '');
290+
});
291+
// move contents of 'main/default' to 'retrievetargetdir'
292+
await promisesQueue([join(resolvedTargetDir, 'main', 'default')], mv, 5, true);
293+
// remove 'main/default'
294+
await fs.promises.rm(join(targetDir, 'main'), { recursive: true });
295+
}
230296
}
231297

232298
type RetrieveAndDeleteTargets = {
@@ -267,7 +333,7 @@ const buildRetrieveAndDeleteTargets = async (
267333
manifest: {
268334
manifestPath: flags.manifest,
269335
// if mdapi format, there might not be a project
270-
directoryPaths: format === 'metadata' ? [] : await getPackageDirs(),
336+
directoryPaths: format === 'metadata' || flags['output-dir'] ? [] : await getPackageDirs(),
271337
},
272338
}
273339
: {}),
@@ -276,7 +342,7 @@ const buildRetrieveAndDeleteTargets = async (
276342
metadata: {
277343
metadataEntries: flags.metadata,
278344
// if mdapi format, there might not be a project
279-
directoryPaths: format === 'metadata' ? [] : await getPackageDirs(),
345+
directoryPaths: format === 'metadata' || flags['output-dir'] ? [] : await getPackageDirs(),
280346
},
281347
}
282348
: {}),
@@ -289,14 +355,16 @@ const buildRetrieveAndDeleteTargets = async (
289355
*
290356
*
291357
* @param flags
292-
* @param project
293358
* @param format 'metadata' or 'source'
359+
* @param zipFileName
360+
* @param output
294361
* @returns RetrieveSetOptions (an object that can be passed as the options for a ComponentSet retrieve)
295362
*/
296363
const buildRetrieveOptions = async (
297364
flags: Interfaces.InferredFlags<typeof RetrieveMetadata.flags>,
298365
format: Format,
299-
zipFileName: string
366+
zipFileName: string,
367+
output: string | undefined
300368
): Promise<RetrieveSetOptions> => ({
301369
usernameOrConnection: flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']),
302370
merge: true,
@@ -311,6 +379,6 @@ const buildRetrieveOptions = async (
311379
output: flags['target-metadata-dir'] as string,
312380
}
313381
: {
314-
output: (await SfProject.resolve()).getDefaultPackage().fullPath,
382+
output: output ?? (await SfProject.resolve()).getDefaultPackage().fullPath,
315383
}),
316384
});

0 commit comments

Comments
 (0)