Skip to content

Commit

Permalink
Merge pull request #627 from salesforcecli/wr/retrieveTargetDir
Browse files Browse the repository at this point in the history
--output-dir
  • Loading branch information
shetzel authored May 23, 2023
2 parents a84e5ac + dc26d21 commit 40616f0
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 525 deletions.
657 changes: 164 additions & 493 deletions CHANGELOG.md

Large diffs are not rendered by default.

88 changes: 71 additions & 17 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
"command": "deploy",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["interactive"],
"alias": []
"alias": [],
"flagChars": [],
"flagAliases": []
},
{
"command": "project:convert:mdapi",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["api-version", "json", "loglevel", "manifest", "metadata", "metadata-dir", "output-dir", "root-dir"],
"alias": ["force:mdapi:convert"]
"alias": ["force:mdapi:convert"],
"flagChars": ["d", "m", "p", "r", "x"],
"flagAliases": ["apiversion", "metadatapath", "outputdir", "rootdir"]
},
{
"command": "project:convert:source",
Expand All @@ -25,7 +29,9 @@
"root-dir",
"source-dir"
],
"alias": ["force:source:convert"]
"alias": ["force:source:convert"],
"flagChars": ["d", "m", "n", "p", "r", "x"],
"flagAliases": ["apiversion", "outputdir", "packagename", "rootdir", "sourcepath"]
},
{
"command": "project:delete:source",
Expand All @@ -45,37 +51,59 @@
"verbose",
"wait"
],
"alias": ["force:source:delete"]
"alias": ["force:source:delete"],
"flagChars": ["c", "f", "l", "m", "o", "p", "r", "t", "w"],
"flagAliases": [
"apiversion",
"checkonly",
"forceoverwrite",
"noprompt",
"sourcepath",
"targetusername",
"testlevel",
"tracksource",
"u"
]
},
{
"command": "project:delete:tracking",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["api-version", "json", "loglevel", "no-prompt", "target-org"],
"alias": ["force:source:tracking:clear"]
"alias": ["force:source:tracking:clear"],
"flagChars": ["o", "p"],
"flagAliases": ["apiversion", "noprompt", "targetusername", "u"]
},
{
"command": "project:deploy:cancel",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["async", "job-id", "json", "use-most-recent", "wait"],
"alias": ["deploy:metadata:cancel"]
"alias": ["deploy:metadata:cancel"],
"flagChars": ["i", "r", "w"],
"flagAliases": []
},
{
"command": "project:deploy:preview",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["ignore-conflicts", "json", "manifest", "metadata", "source-dir", "target-org"],
"alias": ["deploy:metadata:preview"]
"alias": ["deploy:metadata:preview"],
"flagChars": ["c", "d", "m", "o", "x"],
"flagAliases": []
},
{
"command": "project:deploy:quick",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["api-version", "async", "concise", "job-id", "json", "target-org", "use-most-recent", "verbose", "wait"],
"alias": ["deploy:metadata:quick"]
"alias": ["deploy:metadata:quick"],
"flagChars": ["a", "i", "o", "r", "w"],
"flagAliases": []
},
{
"command": "project:deploy:report",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["coverage-formatters", "job-id", "json", "junit", "results-dir", "use-most-recent"],
"alias": ["deploy:metadata:report"]
"alias": ["deploy:metadata:report"],
"flagChars": ["i", "r"],
"flagAliases": []
},
{
"command": "project:deploy:resume",
Expand All @@ -91,7 +119,9 @@
"verbose",
"wait"
],
"alias": ["deploy:metadata:resume"]
"alias": ["deploy:metadata:resume"],
"flagChars": ["i", "r", "w"],
"flagAliases": []
},
{
"command": "project:deploy:start",
Expand Down Expand Up @@ -122,7 +152,9 @@
"verbose",
"wait"
],
"alias": ["deploy:metadata"]
"alias": ["deploy:metadata"],
"flagChars": ["a", "c", "d", "g", "l", "m", "o", "r", "t", "w", "x"],
"flagAliases": []
},
{
"command": "project:deploy:validate",
Expand All @@ -143,7 +175,9 @@
"verbose",
"wait"
],
"alias": ["deploy:metadata:validate"]
"alias": ["deploy:metadata:validate"],
"flagChars": ["a", "d", "l", "m", "o", "t", "w", "x"],
"flagAliases": []
},
{
"command": "project:generate:manifest",
Expand All @@ -160,25 +194,42 @@
"source-dir",
"type"
],
"alias": ["force:source:manifest:create"]
"alias": ["force:source:manifest:create"],
"flagChars": ["c", "d", "m", "n", "p", "t"],
"flagAliases": [
"apiversion",
"fromorg",
"includepackages",
"manifestname",
"manifesttype",
"o",
"outputdir",
"sourcepath"
]
},
{
"command": "project:list:ignored",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["json", "source-dir"],
"alias": ["force:source:ignored:list"]
"alias": ["force:source:ignored:list"],
"flagChars": ["p"],
"flagAliases": ["sourcepath"]
},
{
"command": "project:reset:tracking",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["api-version", "json", "loglevel", "no-prompt", "revision", "target-org"],
"alias": ["force:source:tracking:reset"]
"alias": ["force:source:tracking:reset"],
"flagChars": ["o", "p", "r"],
"flagAliases": ["apiversion", "noprompt", "targetusername", "u"]
},
{
"command": "project:retrieve:preview",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["ignore-conflicts", "json", "target-org"],
"alias": ["retrieve:metadata:preview"]
"alias": ["retrieve:metadata:preview"],
"flagChars": ["c", "o"],
"flagAliases": []
},
{
"command": "project:retrieve:start",
Expand All @@ -189,6 +240,7 @@
"json",
"manifest",
"metadata",
"output-dir",
"package-name",
"single-package",
"source-dir",
Expand All @@ -198,6 +250,8 @@
"wait",
"zip-file-name"
],
"alias": ["retrieve:metadata"]
"alias": ["retrieve:metadata"],
"flagChars": ["a", "c", "d", "m", "n", "o", "r", "t", "w", "x", "z"],
"flagAliases": []
}
]
14 changes: 14 additions & 0 deletions messages/retrieve.metadata.md → messages/retrieve.start.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,17 @@ Wrote retrieve zip file to %s.
# info.ExtractedZipFile

Extracted %s to %s.

# flags.output-dir.description

The root of the directory structure into which the source files are retrieved.
If the target directory matches one of the package directories in your sfdx-project.json file, the command fails.
Running the command multiple times with the same target adds new files and overwrites existing files.

# flags.output-dir.summary

Directory root for the retrieved source files.

# retrieveTargetDirOverlapsPackage

The retrieve target directory [%s] overlaps one of your package directories. Specify a different retrieve target directory and try again.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"tslib": "^2"
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^3.3.14",
"@oclif/plugin-command-snapshot": "^4.0.1",
"@salesforce/cli-plugins-testkit": "^3.4.0",
"@salesforce/dev-config": "^3.1.0",
"@salesforce/dev-scripts": "^4.3.1",
Expand Down Expand Up @@ -270,4 +270,4 @@
"output": []
}
}
}
}
84 changes: 76 additions & 8 deletions src/commands/project/retrieve/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
*/

import { rm } from 'fs/promises';
import { join, resolve } from 'path';
import { dirname, join, resolve } from 'path';

import * as fs from 'fs';
import { EnvironmentVariable, Messages, OrgConfigProperties, SfError, SfProject } from '@salesforce/core';
import {
RetrieveResult,
Expand All @@ -29,9 +30,10 @@ import { MetadataRetrieveResultFormatter } from '../../../formatters/metadataRet
import { getPackageDirs } from '../../../utils/project';
import { RetrieveResultJson } from '../../../utils/types';
import { writeConflictTable } from '../../../utils/conflicts';
import { promisesQueue } from '../../../utils/promiseQueue';

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

type Format = 'source' | 'metadata';
Expand Down Expand Up @@ -74,6 +76,12 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
summary: messages.getMessage('flags.package-name.summary'),
multiple: true,
}),
'output-dir': Flags.directory({
char: 'r',
summary: messages.getMessage('flags.output-dir.summary'),
description: messages.getMessage('flags.output-dir.description'),
exclusive: ['package-name', 'source-dir'],
}),
'single-package': Flags.boolean({
summary: messages.getMessage('flags.single-package.summary'),
dependsOn: ['target-metadata-dir'],
Expand Down Expand Up @@ -143,6 +151,13 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {

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

Expand All @@ -152,7 +167,7 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
flags,
format
);
const retrieveOpts = await buildRetrieveOptions(flags, format, zipFileName);
const retrieveOpts = await buildRetrieveOptions(flags, format, zipFileName, resolvedTargetDir);

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

this.spinner.stop();

// flags['output-dir'] will set resolvedTargetDir var, so this check is redundant, but allows for nice typings in the moveResultsForRetrieveTargetDir method
if (flags['output-dir'] && resolvedTargetDir) {
await this.moveResultsForRetrieveTargetDir(flags['output-dir'], resolvedTargetDir);
}

// reference the flag instead of `format` so we get correct type
const formatter = flags['target-metadata-dir']
? new MetadataRetrieveResultFormatter(this.retrieveResult, {
Expand Down Expand Up @@ -227,6 +247,52 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {

return super.catch(error);
}

private async moveResultsForRetrieveTargetDir(targetDir: string, resolvedTargetDir: string): Promise<void> {
async function mv(src: string): Promise<string[]> {
let directories: string[] = [];
let files: string[] = [];
const srcStat = await fs.promises.stat(src);
if (srcStat.isDirectory()) {
const contents = await fs.promises.readdir(src, { withFileTypes: true });
[directories, files] = contents.reduce<[string[], string[]]>(
(acc, dirent) => {
if (dirent.isDirectory()) {
acc[0].push(dirent.name);
} else {
acc[1].push(dirent.name);
}
return acc;
},
[[], []]
);

directories = directories.map((dir) => join(src, dir));
} else {
files.push(src);
}
await promisesQueue(
files,
async (file: string): Promise<string> => {
const dest = join(src.replace(join('main', 'default'), ''), file);
const destDir = dirname(dest);
await fs.promises.mkdir(destDir, { recursive: true });
await fs.promises.rename(join(src, file), dest);
return dest;
},
50
);
return directories;
}
// getFileResponses fails once the files have been moved, calculate where they're moved to, and then move them
this.retrieveResult.getFileResponses().forEach((fileResponse) => {
fileResponse.filePath = fileResponse.filePath?.replace(join('main', 'default'), '');
});
// move contents of 'main/default' to 'retrievetargetdir'
await promisesQueue([join(resolvedTargetDir, 'main', 'default')], mv, 5, true);
// remove 'main/default'
await fs.promises.rm(join(targetDir, 'main'), { recursive: true });
}
}

type RetrieveAndDeleteTargets = {
Expand Down Expand Up @@ -267,7 +333,7 @@ const buildRetrieveAndDeleteTargets = async (
manifest: {
manifestPath: flags.manifest,
// if mdapi format, there might not be a project
directoryPaths: format === 'metadata' ? [] : await getPackageDirs(),
directoryPaths: format === 'metadata' || flags['output-dir'] ? [] : await getPackageDirs(),
},
}
: {}),
Expand All @@ -276,7 +342,7 @@ const buildRetrieveAndDeleteTargets = async (
metadata: {
metadataEntries: flags.metadata,
// if mdapi format, there might not be a project
directoryPaths: format === 'metadata' ? [] : await getPackageDirs(),
directoryPaths: format === 'metadata' || flags['output-dir'] ? [] : await getPackageDirs(),
},
}
: {}),
Expand All @@ -289,14 +355,16 @@ const buildRetrieveAndDeleteTargets = async (
*
*
* @param flags
* @param project
* @param format 'metadata' or 'source'
* @param zipFileName
* @param output
* @returns RetrieveSetOptions (an object that can be passed as the options for a ComponentSet retrieve)
*/
const buildRetrieveOptions = async (
flags: Interfaces.InferredFlags<typeof RetrieveMetadata.flags>,
format: Format,
zipFileName: string
zipFileName: string,
output: string | undefined
): Promise<RetrieveSetOptions> => ({
usernameOrConnection: flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']),
merge: true,
Expand All @@ -311,6 +379,6 @@ const buildRetrieveOptions = async (
output: flags['target-metadata-dir'] as string,
}
: {
output: (await SfProject.resolve()).getDefaultPackage().fullPath,
output: output ?? (await SfProject.resolve()).getDefaultPackage().fullPath,
}),
});
Loading

0 comments on commit 40616f0

Please sign in to comment.