From 0091091be8cb3fff314338d166875ed348da4120 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 25 Oct 2022 09:29:16 -0600 Subject: [PATCH 1/9] chire: wip --- command-snapshot.json | 965 +++++++++++------- messages/retrieve.json | 8 +- src/commands/force/source/retrieve.ts | 214 ++-- test/commands/source/retrieve.test.ts | 304 +++--- .../seeds/retrieve.retrievetargetdir.seed.ts | 51 + test/nuts/testMatrix.ts | 8 + 6 files changed, 936 insertions(+), 614 deletions(-) create mode 100644 test/nuts/seeds/retrieve.retrievetargetdir.seed.ts diff --git a/command-snapshot.json b/command-snapshot.json index bd60dcca1..4f12d8cb1 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,392 +1,575 @@ [ - { - "command": "force", - "plugin": "@salesforce/plugin-source", - "flags": ["json", "loglevel"], - "alias": [] - }, - { - "command": "force:mdapi:beta:convert", - "plugin": "@salesforce/plugin-source", - "flags": ["json", "loglevel", "manifest", "metadata", "metadatapath", "outputdir", "rootdir"], - "alias": ["force:mdapi:beta:convert"] - }, - { - "command": "force:mdapi:beta:deploy", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "checkonly", - "concise", - "coverageformatters", - "deploydir", - "ignoreerrors", - "ignorewarnings", - "json", - "junit", - "loglevel", - "purgeondelete", - "resultsdir", - "runtests", - "singlepackage", - "soapdeploy", - "targetusername", - "testlevel", - "validateddeployrequestid", - "verbose", - "wait", - "zipfile" - ], - "alias": ["force:mdapi:beta:deploy"] - }, - { - "command": "force:mdapi:beta:deploy:report", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "concise", - "coverageformatters", - "jobid", - "json", - "junit", - "loglevel", - "resultsdir", - "targetusername", - "verbose", - "wait" - ], - "alias": ["force:mdapi:beta:deploy:report"] - }, - { - "command": "force:mdapi:beta:retrieve", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "json", - "loglevel", - "packagenames", - "retrievetargetdir", - "singlepackage", - "sourcedir", - "targetusername", - "unpackaged", - "unzip", - "verbose", - "wait", - "zipfilename" - ], - "alias": ["force:mdapi:beta:retrieve"] - }, - { - "command": "force:mdapi:beta:retrieve:report", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "jobid", - "json", - "loglevel", - "retrievetargetdir", - "targetusername", - "unzip", - "verbose", - "wait", - "zipfilename" - ], - "alias": ["force:mdapi:beta:retrieve:report"] - }, - { - "command": "force:mdapi:convert", - "plugin": "@salesforce/plugin-source", - "flags": ["json", "loglevel", "manifest", "metadata", "metadatapath", "outputdir", "rootdir"], - "alias": ["force:mdapi:beta:convert"] - }, - { - "command": "force:mdapi:deploy", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "checkonly", - "concise", - "coverageformatters", - "deploydir", - "ignoreerrors", - "ignorewarnings", - "json", - "junit", - "loglevel", - "purgeondelete", - "resultsdir", - "runtests", - "singlepackage", - "soapdeploy", - "targetusername", - "testlevel", - "validateddeployrequestid", - "verbose", - "wait", - "zipfile" - ], - "alias": ["force:mdapi:beta:deploy"] - }, - { - "command": "force:mdapi:deploy:cancel", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait"], - "alias": [] - }, - { - "command": "force:mdapi:deploy:report", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "concise", - "coverageformatters", - "jobid", - "json", - "junit", - "loglevel", - "resultsdir", - "targetusername", - "verbose", - "wait" - ], - "alias": ["force:mdapi:beta:deploy:report"] - }, - { - "command": "force:mdapi:describemetadata", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "filterknown", "json", "loglevel", "resultfile", "targetusername"], - "alias": [] - }, - { - "command": "force:mdapi:listmetadata", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "folder", "json", "loglevel", "metadatatype", "resultfile", "targetusername"], - "alias": [] - }, - { - "command": "force:mdapi:retrieve", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "json", - "loglevel", - "packagenames", - "retrievetargetdir", - "singlepackage", - "sourcedir", - "targetusername", - "unpackaged", - "unzip", - "verbose", - "wait", - "zipfilename" - ], - "alias": ["force:mdapi:beta:retrieve"] - }, - { - "command": "force:mdapi:retrieve:report", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "jobid", - "json", - "loglevel", - "retrievetargetdir", - "targetusername", - "unzip", - "verbose", - "wait", - "zipfilename" - ], - "alias": ["force:mdapi:beta:retrieve:report"] - }, - { - "command": "force:source:beta:pull", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "forceoverwrite", "json", "loglevel", "targetusername", "wait"], - "alias": ["force:source:beta:pull"] - }, - { - "command": "force:source:beta:push", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "forceoverwrite", "ignorewarnings", "json", "loglevel", "quiet", "targetusername", "wait"], - "alias": ["force:source:beta:push"] - }, - { - "command": "force:source:beta:status", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "concise", "json", "local", "loglevel", "remote", "targetusername"], - "alias": ["force:source:beta:status"] - }, - { - "command": "force:source:beta:tracking:clear", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "json", "loglevel", "noprompt", "targetusername"], - "alias": ["force:source:beta:tracking:clear"] - }, - { - "command": "force:source:beta:tracking:reset", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "json", "loglevel", "noprompt", "revision", "targetusername"], - "alias": ["force:source:beta:tracking:reset"] - }, - { - "command": "force:source:convert", - "plugin": "@salesforce/plugin-source", - "flags": ["json", "loglevel", "manifest", "metadata", "outputdir", "packagename", "rootdir", "sourcepath"], - "alias": [] - }, - { - "command": "force:source:delete", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "checkonly", - "forceoverwrite", - "json", - "loglevel", - "metadata", - "noprompt", - "sourcepath", - "targetusername", - "testlevel", - "tracksource", - "verbose", - "wait" - ], - "alias": [] - }, - { - "command": "force:source:deploy", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "checkonly", - "coverageformatters", - "forceoverwrite", - "ignoreerrors", - "ignorewarnings", - "json", - "junit", - "loglevel", - "manifest", - "metadata", - "postdestructivechanges", - "predestructivechanges", - "purgeondelete", - "resultsdir", - "runtests", - "soapdeploy", - "sourcepath", - "targetusername", - "testlevel", - "tracksource", - "validateddeployrequestid", - "verbose", - "wait" - ], - "alias": [] - }, - { - "command": "force:source:deploy:cancel", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait"], - "alias": [] - }, - { - "command": "force:source:deploy:report", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "coverageformatters", - "jobid", - "json", - "junit", - "loglevel", - "resultsdir", - "targetusername", - "verbose", - "wait" - ], - "alias": [] - }, - { - "command": "force:source:ignored:list", - "plugin": "@salesforce/plugin-source", - "flags": ["json", "loglevel", "sourcepath"], - "alias": [] - }, - { - "command": "force:source:manifest:create", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "fromorg", - "includepackages", - "json", - "loglevel", - "manifestname", - "manifesttype", - "metadata", - "outputdir", - "sourcepath" - ], - "alias": [] - }, - { - "command": "force:source:open", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "json", "loglevel", "sourcefile", "targetusername", "urlonly"], - "alias": [] - }, - { - "command": "force:source:pull", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "forceoverwrite", "json", "loglevel", "targetusername", "wait"], - "alias": ["force:source:beta:pull"] - }, - { - "command": "force:source:push", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "forceoverwrite", "ignorewarnings", "json", "loglevel", "quiet", "targetusername", "wait"], - "alias": ["force:source:beta:push"] - }, - { - "command": "force:source:retrieve", - "plugin": "@salesforce/plugin-source", - "flags": [ - "apiversion", - "forceoverwrite", - "json", - "loglevel", - "manifest", - "metadata", - "packagenames", - "sourcepath", - "targetusername", - "tracksource", - "verbose", - "wait" - ], - "alias": [] - }, - { - "command": "force:source:status", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "concise", "json", "local", "loglevel", "remote", "targetusername"], - "alias": ["force:source:beta:status"] - }, - { - "command": "force:source:tracking:clear", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "json", "loglevel", "noprompt", "targetusername"], - "alias": ["force:source:beta:tracking:clear"] - }, - { - "command": "force:source:tracking:reset", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "json", "loglevel", "noprompt", "revision", "targetusername"], - "alias": ["force:source:beta:tracking:reset"] - } -] + { + "command": "force", + "plugin": "@salesforce/plugin-source", + "flags": [ + "json", + "loglevel" + ], + "alias": [] + }, + { + "command": "force:mdapi:beta:convert", + "plugin": "@salesforce/plugin-source", + "flags": [ + "json", + "loglevel", + "manifest", + "metadata", + "metadatapath", + "outputdir", + "rootdir" + ], + "alias": [ + "force:mdapi:beta:convert" + ] + }, + { + "command": "force:mdapi:beta:deploy", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "checkonly", + "concise", + "coverageformatters", + "deploydir", + "ignoreerrors", + "ignorewarnings", + "json", + "junit", + "loglevel", + "purgeondelete", + "resultsdir", + "runtests", + "singlepackage", + "soapdeploy", + "targetusername", + "testlevel", + "validateddeployrequestid", + "verbose", + "wait", + "zipfile" + ], + "alias": [ + "force:mdapi:beta:deploy" + ] + }, + { + "command": "force:mdapi:beta:deploy:report", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "concise", + "coverageformatters", + "jobid", + "json", + "junit", + "loglevel", + "resultsdir", + "targetusername", + "verbose", + "wait" + ], + "alias": [ + "force:mdapi:beta:deploy:report" + ] + }, + { + "command": "force:mdapi:beta:retrieve", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "json", + "loglevel", + "packagenames", + "retrievetargetdir", + "singlepackage", + "sourcedir", + "targetusername", + "unpackaged", + "unzip", + "verbose", + "wait", + "zipfilename" + ], + "alias": [ + "force:mdapi:beta:retrieve" + ] + }, + { + "command": "force:mdapi:beta:retrieve:report", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "jobid", + "json", + "loglevel", + "retrievetargetdir", + "targetusername", + "unzip", + "verbose", + "wait", + "zipfilename" + ], + "alias": [ + "force:mdapi:beta:retrieve:report" + ] + }, + { + "command": "force:mdapi:convert", + "plugin": "@salesforce/plugin-source", + "flags": [ + "json", + "loglevel", + "manifest", + "metadata", + "metadatapath", + "outputdir", + "rootdir" + ], + "alias": [ + "force:mdapi:beta:convert" + ] + }, + { + "command": "force:mdapi:deploy", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "checkonly", + "concise", + "coverageformatters", + "deploydir", + "ignoreerrors", + "ignorewarnings", + "json", + "junit", + "loglevel", + "purgeondelete", + "resultsdir", + "runtests", + "singlepackage", + "soapdeploy", + "targetusername", + "testlevel", + "validateddeployrequestid", + "verbose", + "wait", + "zipfile" + ], + "alias": [ + "force:mdapi:beta:deploy" + ] + }, + { + "command": "force:mdapi:deploy:cancel", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "jobid", + "json", + "loglevel", + "targetusername", + "wait" + ], + "alias": [] + }, + { + "command": "force:mdapi:deploy:report", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "concise", + "coverageformatters", + "jobid", + "json", + "junit", + "loglevel", + "resultsdir", + "targetusername", + "verbose", + "wait" + ], + "alias": [ + "force:mdapi:beta:deploy:report" + ] + }, + { + "command": "force:mdapi:describemetadata", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "filterknown", + "json", + "loglevel", + "resultfile", + "targetusername" + ], + "alias": [] + }, + { + "command": "force:mdapi:listmetadata", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "folder", + "json", + "loglevel", + "metadatatype", + "resultfile", + "targetusername" + ], + "alias": [] + }, + { + "command": "force:mdapi:retrieve", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "json", + "loglevel", + "packagenames", + "retrievetargetdir", + "singlepackage", + "sourcedir", + "targetusername", + "unpackaged", + "unzip", + "verbose", + "wait", + "zipfilename" + ], + "alias": [ + "force:mdapi:beta:retrieve" + ] + }, + { + "command": "force:mdapi:retrieve:report", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "jobid", + "json", + "loglevel", + "retrievetargetdir", + "targetusername", + "unzip", + "verbose", + "wait", + "zipfilename" + ], + "alias": [ + "force:mdapi:beta:retrieve:report" + ] + }, + { + "command": "force:source:beta:pull", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "forceoverwrite", + "json", + "loglevel", + "targetusername", + "wait" + ], + "alias": [ + "force:source:beta:pull" + ] + }, + { + "command": "force:source:beta:push", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "forceoverwrite", + "ignorewarnings", + "json", + "loglevel", + "quiet", + "targetusername", + "wait" + ], + "alias": [ + "force:source:beta:push" + ] + }, + { + "command": "force:source:beta:status", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "concise", + "json", + "local", + "loglevel", + "remote", + "targetusername" + ], + "alias": [ + "force:source:beta:status" + ] + }, + { + "command": "force:source:beta:tracking:clear", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "json", + "loglevel", + "noprompt", + "targetusername" + ], + "alias": [ + "force:source:beta:tracking:clear" + ] + }, + { + "command": "force:source:beta:tracking:reset", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "json", + "loglevel", + "noprompt", + "revision", + "targetusername" + ], + "alias": [ + "force:source:beta:tracking:reset" + ] + }, + { + "command": "force:source:convert", + "plugin": "@salesforce/plugin-source", + "flags": [ + "json", + "loglevel", + "manifest", + "metadata", + "outputdir", + "packagename", + "rootdir", + "sourcepath" + ], + "alias": [] + }, + { + "command": "force:source:delete", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "checkonly", + "forceoverwrite", + "json", + "loglevel", + "metadata", + "noprompt", + "sourcepath", + "targetusername", + "testlevel", + "tracksource", + "verbose", + "wait" + ], + "alias": [] + }, + { + "command": "force:source:deploy", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "checkonly", + "coverageformatters", + "forceoverwrite", + "ignoreerrors", + "ignorewarnings", + "json", + "junit", + "loglevel", + "manifest", + "metadata", + "postdestructivechanges", + "predestructivechanges", + "purgeondelete", + "resultsdir", + "runtests", + "soapdeploy", + "sourcepath", + "targetusername", + "testlevel", + "tracksource", + "validateddeployrequestid", + "verbose", + "wait" + ], + "alias": [] + }, + { + "command": "force:source:deploy:cancel", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "jobid", + "json", + "loglevel", + "targetusername", + "wait" + ], + "alias": [] + }, + { + "command": "force:source:deploy:report", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "coverageformatters", + "jobid", + "json", + "junit", + "loglevel", + "resultsdir", + "targetusername", + "verbose", + "wait" + ], + "alias": [] + }, + { + "command": "force:source:ignored:list", + "plugin": "@salesforce/plugin-source", + "flags": [ + "json", + "loglevel", + "sourcepath" + ], + "alias": [] + }, + { + "command": "force:source:manifest:create", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "fromorg", + "includepackages", + "json", + "loglevel", + "manifestname", + "manifesttype", + "metadata", + "outputdir", + "sourcepath" + ], + "alias": [] + }, + { + "command": "force:source:open", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "json", + "loglevel", + "sourcefile", + "targetusername", + "urlonly" + ], + "alias": [] + }, + { + "command": "force:source:pull", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "forceoverwrite", + "json", + "loglevel", + "targetusername", + "wait" + ], + "alias": [ + "force:source:beta:pull" + ] + }, + { + "command": "force:source:push", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "forceoverwrite", + "ignorewarnings", + "json", + "loglevel", + "quiet", + "targetusername", + "wait" + ], + "alias": [ + "force:source:beta:push" + ] + }, + { + "command": "force:source:retrieve", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "forceoverwrite", + "json", + "loglevel", + "manifest", + "metadata", + "packagenames", + "retrievetargetdir", + "sourcepath", + "targetusername", + "tracksource", + "verbose", + "wait" + ], + "alias": [] + }, + { + "command": "force:source:status", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "concise", + "json", + "local", + "loglevel", + "remote", + "targetusername" + ], + "alias": [ + "force:source:beta:status" + ] + }, + { + "command": "force:source:tracking:clear", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "json", + "loglevel", + "noprompt", + "targetusername" + ], + "alias": [ + "force:source:beta:tracking:clear" + ] + }, + { + "command": "force:source:tracking:reset", + "plugin": "@salesforce/plugin-source", + "flags": [ + "apiversion", + "json", + "loglevel", + "noprompt", + "revision", + "targetusername" + ], + "alias": [ + "force:source:beta:tracking:reset" + ] + } +] \ No newline at end of file diff --git a/messages/retrieve.json b/messages/retrieve.json index 241a65e5b..bc6234e8c 100644 --- a/messages/retrieve.json +++ b/messages/retrieve.json @@ -11,9 +11,11 @@ "To retrieve all Apex classes and two specific profiles (one of which has a space in its name):\n $ sfdx force:source:retrieve -m \"ApexClass, Profile:My Profile, Profile: AnotherProfile\"", "To retrieve all metadata components listed in a manifest:\n $ sfdx force:source:retrieve -x path/to/package.xml", "To retrieve metadata from a package or multiple packages:\n $ sfdx force:source:retrieve -n MyPackageName\n $ sfdx force:source:retrieve -n \"Package1, PackageName With Spaces, Package3\"", - "To retrieve all metadata from a package and specific components that aren’t in the package, specify both -n | --packagenames and one other scoping parameter:\n $ sfdx force:source:retrieve -n MyPackageName -p path/to/apex/classes\n $ sfdx force:source:retrieve -n MyPackageName -m ApexClass:MyApexClass\n $ sfdx force:source:retrieve -n MyPackageName -x path/to/package.xml" + "To retrieve all metadata from a package and specific components that aren’t in the package, specify both -n | --packagenames and one other scoping parameter:\n $ sfdx force:source:retrieve -n MyPackageName -p path/to/apex/classes\n $ sfdx force:source:retrieve -n MyPackageName -m ApexClass:MyApexClass\n $ sfdx force:source:retrieve -n MyPackageName -x path/to/package.xml", + "To retrieve source files to a given directory instead of the default package directory specified in sfdx-project.json:\n $ sfdx force:source:retrieve -m \"StandardValueSet:TaskStatus\" -r path/to/unpackaged" ], "flags": { + "retrievetargetdir": "directory root for the retrieved source files", "sourcePath": "comma-separated list of source file paths to retrieve", "wait": "wait time for command to finish in minutes", "manifest": "file path for manifest (package.xml) of components to retrieve", @@ -24,6 +26,7 @@ "forceoverwrite": "ignore conflict warnings and overwrite changes to the project" }, "flagsLong": { + "retrievetargetdir": "The root of the directory structure into which the source files are retrieved.", "wait": "Number of minutes to wait for the command to complete and display results to the terminal window. If the command continues to run after the wait period, the CLI returns control of the terminal window to you.", "manifest": [ "The complete path for the manifest (package.xml) file that specifies the components to retrieve.", @@ -52,5 +55,6 @@ "errorColumn": "PROBLEM", "nothingToRetrieve": "Specify a source path, manifest, metadata, or package names to retrieve.", "wantsToRetrieveCustomFields": "Because you're retrieving one or more CustomFields, we're also retrieving the CustomObject to which it's associated.", - "retrieveWontDelete": "You currently have files deleted in your org. The retrieve command will NOT delete them from your local project" + "retrieveWontDelete": "You currently have files deleted in your org. The retrieve command will NOT delete them from your local project", + "retrieveTargetDirOverlapsPackageDir": "The retrieve target directory [%s] overlaps with the package directory [%s]." } diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index 36c615747..7e6346d97 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -5,79 +5,86 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import * as os from 'os'; -import { join } from 'path'; -import { flags, FlagsConfig } from '@salesforce/command'; -import { Messages, SfError, SfProject } from '@salesforce/core'; -import { Duration } from '@salesforce/kit'; -import { ComponentSet, ComponentSetBuilder, RequestStatus, RetrieveResult } from '@salesforce/source-deploy-retrieve'; -import { SourceTracking } from '@salesforce/source-tracking'; -import { SourceCommand } from '../../../sourceCommand'; +import * as os from "os"; +import { join, resolve, dirname } from "path"; +import * as fs from "fs"; +import { flags, FlagsConfig } from "@salesforce/command"; +import { Messages, SfError, SfProject } from "@salesforce/core"; +import { Duration } from "@salesforce/kit"; +import { ComponentSet, ComponentSetBuilder, RequestStatus, RetrieveResult } from "@salesforce/source-deploy-retrieve"; +import { SourceTracking } from "@salesforce/source-tracking"; +import { SourceCommand } from "../../../sourceCommand"; import { PackageRetrieval, RetrieveCommandResult, - RetrieveResultFormatter, -} from '../../../formatters/retrieveResultFormatter'; -import { filterConflictsByComponentSet, trackingSetup, updateTracking } from '../../../trackingFunctions'; + RetrieveResultFormatter +} from "../../../formatters/retrieveResultFormatter"; +import { filterConflictsByComponentSet, trackingSetup, updateTracking } from "../../../trackingFunctions"; Messages.importMessagesDirectory(__dirname); -const messages = Messages.loadMessages('@salesforce/plugin-source', 'retrieve'); -const spinnerMessages = Messages.loadMessages('@salesforce/plugin-source', 'spinner'); +const messages = Messages.loadMessages("@salesforce/plugin-source", "retrieve"); +const spinnerMessages = Messages.loadMessages("@salesforce/plugin-source", "spinner"); export class Retrieve extends SourceCommand { - public static readonly description = messages.getMessage('description'); - public static readonly examples = messages.getMessage('examples').split(os.EOL); + public static readonly description = messages.getMessage("description"); + public static readonly examples = messages.getMessage("examples").split(os.EOL); public static readonly requiresProject = true; public static readonly requiresUsername = true; public static readonly flagsConfig: FlagsConfig = { + retrievetargetdir: flags.directory({ + char: "r", + description: messages.getMessage("flags.retrievetargetdir"), + longDescription: messages.getMessage("flagsLong.retrievetargetdir"), + exclusive: ["packagenames", "sourcepath"] + }), apiversion: flags.builtin({ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ // @ts-ignore force char override for backward compat - char: 'a', + char: "a" }), sourcepath: flags.array({ - char: 'p', - description: messages.getMessage('flags.sourcePath'), - longDescription: messages.getMessage('flagsLong.sourcePath'), - exclusive: ['manifest', 'metadata'], + char: "p", + description: messages.getMessage("flags.sourcePath"), + longDescription: messages.getMessage("flagsLong.sourcePath"), + exclusive: ["manifest", "metadata"] }), wait: flags.minutes({ - char: 'w', + char: "w", default: Duration.minutes(SourceCommand.DEFAULT_WAIT_MINUTES), min: Duration.minutes(1), - description: messages.getMessage('flags.wait'), - longDescription: messages.getMessage('flagsLong.wait'), + description: messages.getMessage("flags.wait"), + longDescription: messages.getMessage("flagsLong.wait") }), manifest: flags.filepath({ - char: 'x', - description: messages.getMessage('flags.manifest'), - longDescription: messages.getMessage('flagsLong.manifest'), - exclusive: ['metadata', 'sourcepath'], + char: "x", + description: messages.getMessage("flags.manifest"), + longDescription: messages.getMessage("flagsLong.manifest"), + exclusive: ["metadata", "sourcepath"] }), metadata: flags.array({ - char: 'm', - description: messages.getMessage('flags.metadata'), - longDescription: messages.getMessage('flagsLong.metadata'), - exclusive: ['manifest', 'sourcepath'], + char: "m", + description: messages.getMessage("flags.metadata"), + longDescription: messages.getMessage("flagsLong.metadata"), + exclusive: ["manifest", "sourcepath"] }), packagenames: flags.array({ - char: 'n', - description: messages.getMessage('flags.packagename'), + char: "n", + description: messages.getMessage("flags.packagename") }), tracksource: flags.boolean({ - char: 't', - description: messages.getMessage('flags.tracksource'), + char: "t", + description: messages.getMessage("flags.tracksource") }), forceoverwrite: flags.boolean({ - char: 'f', - description: messages.getMessage('flags.forceoverwrite'), - dependsOn: ['tracksource'], + char: "f", + description: messages.getMessage("flags.forceoverwrite"), + dependsOn: ["tracksource"] }), verbose: flags.builtin({ - description: messages.getMessage('flags.verbose'), - }), + description: messages.getMessage("flags.verbose") + }) }; - protected readonly lifecycleEventNames = ['preretrieve', 'postretrieve']; + protected readonly lifecycleEventNames = ["preretrieve", "postretrieve"]; protected retrieveResult: RetrieveResult; protected tracking: SourceTracking; @@ -86,6 +93,7 @@ export class Retrieve extends SourceCommand { await this.retrieve(); this.resolveSuccess(); await this.maybeUpdateTracking(); + await this.moveResultsForRetrieveTargetDir(); return this.formatResult(); } @@ -93,7 +101,7 @@ export class Retrieve extends SourceCommand { // we need something to retrieve const retrieveInputs = [this.flags.manifest, this.flags.metadata, this.flags.sourcepath, this.flags.packagenames]; if (!retrieveInputs.some((x) => x)) { - throw new SfError(messages.getMessage('nothingToRetrieve')); + throw new SfError(messages.getMessage("nothingToRetrieve")); } if (this.flags.tracksource) { this.tracking = await trackingSetup({ @@ -101,68 +109,68 @@ export class Retrieve extends SourceCommand { org: this.org, project: this.project, ignoreConflicts: true, - commandName: 'force:source:retrieve', + commandName: "force:source:retrieve" }); } } protected async retrieve(): Promise { - this.ux.startSpinner(spinnerMessages.getMessage('retrieve.componentSetBuild')); + this.ux.startSpinner(spinnerMessages.getMessage("retrieve.componentSetBuild")); this.componentSet = await ComponentSetBuilder.build({ - apiversion: this.getFlag('apiversion'), + apiversion: this.getFlag("apiversion"), sourceapiversion: await this.getSourceApiVersion(), - packagenames: this.getFlag('packagenames'), - sourcepath: this.getFlag('sourcepath'), + packagenames: this.getFlag("packagenames"), + sourcepath: this.getFlag("sourcepath"), manifest: this.flags.manifest && { - manifestPath: this.getFlag('manifest'), - directoryPaths: this.getPackageDirs(), + manifestPath: this.getFlag("manifest"), + directoryPaths: this.flags.retrievetargetdir ? [] : this.getPackageDirs() }, metadata: this.flags.metadata && { - metadataEntries: this.getFlag('metadata'), - directoryPaths: this.getPackageDirs(), - }, + metadataEntries: this.getFlag("metadata"), + directoryPaths: this.flags.retrievetargetdir ? [] : this.getPackageDirs() + } }); - if (this.getFlag('manifest') || this.getFlag('metadata')) { + if (this.getFlag("manifest") || this.getFlag("metadata")) { if (this.wantsToRetrieveCustomFields()) { - this.ux.warn(messages.getMessage('wantsToRetrieveCustomFields')); - this.componentSet.add({ fullName: ComponentSet.WILDCARD, type: { id: 'customobject', name: 'CustomObject' } }); + this.ux.warn(messages.getMessage("wantsToRetrieveCustomFields")); + this.componentSet.add({ fullName: ComponentSet.WILDCARD, type: { id: "customobject", name: "CustomObject" } }); } } - if (this.getFlag('tracksource')) { + if (this.getFlag("tracksource")) { // will throw if conflicts exist - if (!this.getFlag('forceoverwrite')) { + if (!this.getFlag("forceoverwrite")) { await filterConflictsByComponentSet({ tracking: this.tracking, components: this.componentSet, ux: this.ux }); } const remoteDeletes = await this.tracking.getChanges({ - origin: 'remote', - state: 'delete', - format: 'string', + origin: "remote", + state: "delete", + format: "string" }); if (remoteDeletes.length) { - this.ux.warn(messages.getMessage('retrieveWontDelete')); + this.ux.warn(messages.getMessage("retrieveWontDelete")); } } - await this.lifecycle.emit('preretrieve', this.componentSet.toArray()); + await this.lifecycle.emit("preretrieve", this.componentSet.toArray()); this.ux.setSpinnerStatus( - spinnerMessages.getMessage('retrieve.sendingRequest', [ - this.componentSet.sourceApiVersion || this.componentSet.apiVersion, + spinnerMessages.getMessage("retrieve.sendingRequest", [ + this.componentSet.sourceApiVersion || this.componentSet.apiVersion ]) ); const mdapiRetrieve = await this.componentSet.retrieve({ usernameOrConnection: this.org.getUsername(), merge: true, - output: this.project.getDefaultPackage().fullPath, - packageOptions: this.getFlag('packagenames'), + output: this.getFlag("retrievetargetdir") || this.project.getDefaultPackage().fullPath, + packageOptions: this.getFlag("packagenames") }); - this.ux.setSpinnerStatus(spinnerMessages.getMessage('retrieve.polling')); - this.retrieveResult = await mdapiRetrieve.pollStatus({ timeout: this.getFlag('wait') }); + this.ux.setSpinnerStatus(spinnerMessages.getMessage("retrieve.polling")); + this.retrieveResult = await mdapiRetrieve.pollStatus({ timeout: this.getFlag("wait") }); - await this.lifecycle.emit('postretrieve', this.retrieveResult.getFileResponses()); + await this.lifecycle.emit("postretrieve", this.retrieveResult.getFileResponses()); this.ux.stopSpinner(); } @@ -173,7 +181,7 @@ export class Retrieve extends SourceCommand { [RequestStatus.Failed, 1], [RequestStatus.InProgress, 69], [RequestStatus.Pending, 69], - [RequestStatus.Canceling, 69], + [RequestStatus.Canceling, 69] ]); this.setExitCode(StatusCodeMap.get(this.retrieveResult.response.status) ?? 1); @@ -183,14 +191,14 @@ export class Retrieve extends SourceCommand { const packages: PackageRetrieval[] = []; const projectPath = await SfProject.resolveProjectPath(); - this.getFlag('packagenames', []).forEach((name) => { + this.getFlag("packagenames", []).forEach((name) => { packages.push({ name, path: join(projectPath, name) }); }); const formatterOptions = { - waitTime: this.getFlag('wait').quantity, - verbose: this.getFlag('verbose', false), - packages, + waitTime: this.getFlag("wait").quantity, + verbose: this.getFlag("verbose", false), + packages }; const formatter = new RetrieveResultFormatter(this.logger, this.ux, formatterOptions, this.retrieveResult); @@ -203,21 +211,69 @@ export class Retrieve extends SourceCommand { } private async maybeUpdateTracking(): Promise { - if (this.getFlag('tracksource', false)) { + if (this.getFlag("tracksource", false)) { return updateTracking({ tracking: this.tracking, result: this.retrieveResult, ux: this.ux }); } } private wantsToRetrieveCustomFields(): boolean { const hasCustomField = this.componentSet.has({ - type: { name: 'CustomField', id: 'customfield' }, - fullName: ComponentSet.WILDCARD, + type: { name: "CustomField", id: "customfield" }, + fullName: ComponentSet.WILDCARD }); const hasCustomObject = this.componentSet.has({ - type: { name: 'CustomObject', id: 'customobject' }, - fullName: ComponentSet.WILDCARD, + type: { name: "CustomObject", id: "customobject" }, + fullName: ComponentSet.WILDCARD }); return hasCustomField && !hasCustomObject; } + + private async moveResultsForRetrieveTargetDir(): Promise { + async function mv(src: string, dest: string): Promise { + // remove dest if it exists + try{ + const destStat = await fs.promises.stat(dest); + if (destStat.isDirectory()) { + await fs.promises.rmdir(dest, { recursive: true }); + } else { + await fs.promises.rm(dest, { force: true }); + } + } catch(err) { + if ((err as Error)['code'] !== 'ENOENT') { + throw err; + } + } + const srcStat = await fs.promises.stat(src); + if (srcStat.isDirectory()) { + await fs.promises.rename(src, dest); + } else { + await fs.promises.mkdir(dirname(dest), { recursive: true }); + await fs.promises.cp(src, dest); + } + }; + + if (!this.flags.retrievetargetdir) { + return; + } + let overlapsProject = true; + const resolvedTargetDir = resolve(this.flags.retrievetargetdir as string); + overlapsProject = !!this.project.getPackageDirectories().find((pkgDir) => { + if (pkgDir.fullPath) { + return pkgDir.fullPath.includes(resolvedTargetDir); + } + return false; + }); + if (overlapsProject) { + return; + } + // move contents of 'main/default' to 'retrievetargetdir' + const contents = await fs.promises.readdir(join(this.flags.retrievetargetdir as string, 'main', 'default')); + await Promise.all(contents.map((file) => mv(join(this.flags.retrievetargetdir as string, 'main', 'default', file), join(this.flags.retrievetargetdir as string, file)))); + // remove 'main/default' + await fs.promises.rmdir(join(this.flags.retrievetargetdir as string, 'main'), { recursive: true }); + this.retrieveResult.getFileResponses().forEach((fileResponse) => { + fileResponse.filePath = fileResponse.filePath.replace(join('main', 'default'), ''); + }); + } } diff --git a/test/commands/source/retrieve.test.ts b/test/commands/source/retrieve.test.ts index 0dc4511eb..2ffa44ea1 100644 --- a/test/commands/source/retrieve.test.ts +++ b/test/commands/source/retrieve.test.ts @@ -5,43 +5,43 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { join } from 'path'; -import * as sinon from 'sinon'; -import { expect } from 'chai'; +import { join } from "path"; +import * as sinon from "sinon"; +import { expect } from "chai"; import { ComponentLike, ComponentSet, ComponentSetBuilder, ComponentSetOptions, MetadataType, - RetrieveOptions, -} from '@salesforce/source-deploy-retrieve'; -import { Lifecycle, Messages, Org, SfProject } from '@salesforce/core'; -import { fromStub, stubInterface, stubMethod } from '@salesforce/ts-sinon'; -import { Config } from '@oclif/core'; -import { UX } from '@salesforce/command'; -import { Retrieve } from '../../../src/commands/force/source/retrieve'; -import { RetrieveCommandResult, RetrieveResultFormatter } from '../../../src/formatters/retrieveResultFormatter'; -import { getRetrieveResult } from './retrieveResponses'; -import { exampleSourceComponent } from './testConsts'; + RetrieveOptions +} from "@salesforce/source-deploy-retrieve"; +import { Lifecycle, Messages, Org, SfProject } from "@salesforce/core"; +import { fromStub, stubInterface, stubMethod } from "@salesforce/ts-sinon"; +import { Config } from "@oclif/core"; +import { UX } from "@salesforce/command"; +import { Retrieve } from "../../../src/commands/force/source/retrieve"; +import { RetrieveCommandResult, RetrieveResultFormatter } from "../../../src/formatters/retrieveResultFormatter"; +import { getRetrieveResult } from "./retrieveResponses"; +import { exampleSourceComponent } from "./testConsts"; Messages.importMessagesDirectory(__dirname); -const messages = Messages.loadMessages('@salesforce/plugin-source', 'retrieve'); +const messages = Messages.loadMessages("@salesforce/plugin-source", "retrieve"); -describe('force:source:retrieve', () => { +describe("force:source:retrieve", () => { const sandbox = sinon.createSandbox(); - const username = 'retrieve-test@org.com'; - const packageXml = 'package.xml'; - const defaultPackagePath = 'defaultPackagePath'; + const username = "retrieve-test@org.com"; + const packageXml = "package.xml"; + const defaultPackagePath = "defaultPackagePath"; const oclifConfigStub = fromStub(stubInterface(sandbox)); - const retrieveResult = getRetrieveResult('success'); + const retrieveResult = getRetrieveResult("success"); const expectedResults: RetrieveCommandResult = { response: retrieveResult.response, inboundFiles: retrieveResult.getFileResponses(), packages: [], - warnings: [], + warnings: [] }; // Stubs @@ -57,9 +57,11 @@ describe('force:source:retrieve', () => { await this.init(); return this.run(); } + public setOrg(org: Org) { this.org = org; } + public setProject(project: SfProject) { this.project = project; } @@ -67,32 +69,33 @@ describe('force:source:retrieve', () => { const runRetrieveCmd = async (params: string[]) => { const cmd = new TestRetrieve(params, oclifConfigStub); - stubMethod(sandbox, SfProject, 'resolveProjectPath').resolves(join('path', 'to', 'package')); - stubMethod(sandbox, cmd, 'assignProject').callsFake(() => { + stubMethod(sandbox, SfProject, "resolveProjectPath").resolves(join("path", "to", "package")); + stubMethod(sandbox, cmd, "assignProject").callsFake(() => { const SfProjectStub = fromStub( stubInterface(sandbox, { getDefaultPackage: () => ({ fullPath: defaultPackagePath }), getUniquePackageDirectories: () => [{ fullPath: defaultPackagePath }], - resolveProjectConfig: resolveProjectConfigStub, + resolveProjectConfig: resolveProjectConfigStub }) ); cmd.setProject(SfProjectStub); }); - stubMethod(sandbox, cmd, 'assignOrg').callsFake(() => { + stubMethod(sandbox, cmd, "assignOrg").callsFake(() => { const orgStub = fromStub( stubInterface(sandbox, { - getUsername: () => username, + getUsername: () => username }) ); cmd.setOrg(orgStub); }); // keep the stdout from showing up in the test output - stubMethod(sandbox, UX.prototype, 'log'); - stubMethod(sandbox, UX.prototype, 'setSpinnerStatus'); - stubMethod(sandbox, UX.prototype, 'startSpinner'); - stubMethod(sandbox, UX.prototype, 'stopSpinner'); - stubMethod(sandbox, UX.prototype, 'styledHeader'); - stubMethod(sandbox, UX.prototype, 'table'); + stubMethod(sandbox, UX.prototype, "log"); + stubMethod(sandbox, UX.prototype, "setSpinnerStatus"); + stubMethod(sandbox, UX.prototype, "startSpinner"); + stubMethod(sandbox, UX.prototype, "stopSpinner"); + stubMethod(sandbox, UX.prototype, "styledHeader"); + stubMethod(sandbox, UX.prototype, "table"); + stubMethod(sandbox, Retrieve.prototype, "moveResultsForRetrieveTargetDir"); return cmd.runIt(); }; @@ -101,16 +104,16 @@ describe('force:source:retrieve', () => { pollStub = sandbox.stub().resolves(retrieveResult); retrieveStub = sandbox.stub().resolves({ pollStatus: pollStub, - retrieveId: retrieveResult.response.id, + retrieveId: retrieveResult.response.id }); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], - has: () => false, + has: () => false }); - lifecycleEmitStub = sandbox.stub(Lifecycle.prototype, 'emit'); - warnStub = stubMethod(sandbox, UX.prototype, 'warn'); + lifecycleEmitStub = sandbox.stub(Lifecycle.prototype, "emit"); + warnStub = stubMethod(sandbox, UX.prototype, "warn"); }); afterEach(() => { @@ -125,7 +128,7 @@ describe('force:source:retrieve', () => { manifest: undefined, metadata: undefined, apiversion: undefined, - sourceapiversion: undefined, + sourceapiversion: undefined }; const expectedArgs = { ...defaultArgs, ...overrides }; @@ -139,7 +142,7 @@ describe('force:source:retrieve', () => { usernameOrConnection: username, merge: true, output: defaultPackagePath, - packageOptions: undefined, + packageOptions: undefined }; const expectedRetrieveArgs = { ...defaultRetrieveArgs, ...overrides }; @@ -149,96 +152,113 @@ describe('force:source:retrieve', () => { // Ensure Lifecycle hooks are called properly const ensureHookArgs = () => { - const failureMsg = 'Lifecycle.emit() should be called for preretrieve and postretrieve'; + const failureMsg = "Lifecycle.emit() should be called for preretrieve and postretrieve"; expect(lifecycleEmitStub.calledTwice, failureMsg).to.equal(true); - expect(lifecycleEmitStub.firstCall.args[0]).to.equal('preretrieve'); + expect(lifecycleEmitStub.firstCall.args[0]).to.equal("preretrieve"); expect(lifecycleEmitStub.firstCall.args[1]).to.deep.equal([exampleSourceComponent]); - expect(lifecycleEmitStub.secondCall.args[0]).to.equal('postretrieve'); + expect(lifecycleEmitStub.secondCall.args[0]).to.equal("postretrieve"); expect(lifecycleEmitStub.secondCall.args[1]).to.deep.equal(expectedResults.inboundFiles); }; - it('should pass along sourcepath', async () => { - const sourcepath = ['somepath']; - const result = await runRetrieveCmd(['--sourcepath', sourcepath[0], '--json']); + it("should pass along sourcepath", async () => { + const sourcepath = ["somepath"]; + const result = await runRetrieveCmd(["--sourcepath", sourcepath[0], "--json"]); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ sourcepath }); ensureRetrieveArgs(); ensureHookArgs(); }); + it("should pass along retrievetargetdir", async () => { + const sourcepath = ["somepath"]; + const metadata = ["ApexClass:MyClass"]; + const result = await runRetrieveCmd(["--retrievetargetdir", sourcepath[0], "--metadata", metadata[0], "--json"]); + expect(result).to.deep.equal(expectedResults); + ensureCreateComponentSetArgs({ + sourcepath: undefined, + metadata: { + directoryPaths: [], + metadataEntries: [ + "ApexClass:MyClass" + ] + } + }); + ensureRetrieveArgs({output: sourcepath[0]}); + ensureHookArgs(); + }); - it('should pass along metadata', async () => { - const metadata = ['ApexClass:MyClass']; - const result = await runRetrieveCmd(['--metadata', metadata[0], '--json']); + it("should pass along metadata", async () => { + const metadata = ["ApexClass:MyClass"]; + const result = await runRetrieveCmd(["--metadata", metadata[0], "--json"]); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ metadata: { metadataEntries: metadata, - directoryPaths: [defaultPackagePath], - }, + directoryPaths: [defaultPackagePath] + } }); ensureRetrieveArgs(); ensureHookArgs(); }); - it('should pass along manifest', async () => { - const manifest = 'package.xml'; - const result = await runRetrieveCmd(['--manifest', manifest, '--json']); + it("should pass along manifest", async () => { + const manifest = "package.xml"; + const result = await runRetrieveCmd(["--manifest", manifest, "--json"]); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath], - }, + directoryPaths: [defaultPackagePath] + } }); ensureRetrieveArgs(); ensureHookArgs(); }); - it('should pass along apiversion', async () => { - const manifest = 'package.xml'; - const apiversion = '50.0'; - const result = await runRetrieveCmd(['--manifest', manifest, '--apiversion', apiversion, '--json']); + it("should pass along apiversion", async () => { + const manifest = "package.xml"; + const apiversion = "50.0"; + const result = await runRetrieveCmd(["--manifest", manifest, "--apiversion", apiversion, "--json"]); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ apiversion, manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath], - }, + directoryPaths: [defaultPackagePath] + } }); ensureRetrieveArgs(); ensureHookArgs(); }); - it('should pass along sourceapiversion', async () => { - const sourceApiVersion = '50.0'; + it("should pass along sourceapiversion", async () => { + const sourceApiVersion = "50.0"; resolveProjectConfigStub.resolves({ sourceApiVersion }); - const manifest = 'package.xml'; - const result = await runRetrieveCmd(['--manifest', manifest, '--json']); + const manifest = "package.xml"; + const result = await runRetrieveCmd(["--manifest", manifest, "--json"]); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ sourceapiversion: sourceApiVersion, manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath], - }, + directoryPaths: [defaultPackagePath] + } }); ensureRetrieveArgs(); ensureHookArgs(); }); - it('should pass along packagenames', async () => { - const manifest = 'package.xml'; - const packagenames = ['package1']; - const result = await runRetrieveCmd(['--manifest', manifest, '--packagenames', packagenames[0], '--json']); - expectedResults.packages.push({ name: packagenames[0], path: join('path', 'to', 'package', packagenames[0]) }); + it("should pass along packagenames", async () => { + const manifest = "package.xml"; + const packagenames = ["package1"]; + const result = await runRetrieveCmd(["--manifest", manifest, "--packagenames", packagenames[0], "--json"]); + expectedResults.packages.push({ name: packagenames[0], path: join("path", "to", "package", packagenames[0]) }); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ packagenames, manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath], - }, + directoryPaths: [defaultPackagePath] + } }); ensureRetrieveArgs({ packageOptions: packagenames }); ensureHookArgs(); @@ -246,20 +266,20 @@ describe('force:source:retrieve', () => { expectedResults.packages = []; }); - it('should pass along multiple packagenames', async () => { - const manifest = 'package.xml'; - const packagenames = ['package1', 'package2']; - const result = await runRetrieveCmd(['--manifest', manifest, '--packagenames', packagenames.join(','), '--json']); + it("should pass along multiple packagenames", async () => { + const manifest = "package.xml"; + const packagenames = ["package1", "package2"]; + const result = await runRetrieveCmd(["--manifest", manifest, "--packagenames", packagenames.join(","), "--json"]); packagenames.forEach((pkg) => { - expectedResults.packages.push({ name: pkg, path: join('path', 'to', 'package', pkg) }); + expectedResults.packages.push({ name: pkg, path: join("path", "to", "package", pkg) }); }); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ packagenames, manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath], - }, + directoryPaths: [defaultPackagePath] + } }); ensureRetrieveArgs({ packageOptions: packagenames }); ensureHookArgs(); @@ -267,141 +287,141 @@ describe('force:source:retrieve', () => { expectedResults.packages = []; }); - it('should display output with no --json', async () => { - const displayStub = sandbox.stub(RetrieveResultFormatter.prototype, 'display'); - const getJsonStub = sandbox.stub(RetrieveResultFormatter.prototype, 'getJson'); - await runRetrieveCmd(['--sourcepath', 'somepath']); + it("should display output with no --json", async () => { + const displayStub = sandbox.stub(RetrieveResultFormatter.prototype, "display"); + const getJsonStub = sandbox.stub(RetrieveResultFormatter.prototype, "getJson"); + await runRetrieveCmd(["--sourcepath", "somepath"]); expect(displayStub.calledOnce).to.equal(true); expect(getJsonStub.calledOnce).to.equal(true); }); - it('should NOT display output with --json', async () => { - const displayStub = sandbox.stub(RetrieveResultFormatter.prototype, 'display'); - const getJsonStub = sandbox.stub(RetrieveResultFormatter.prototype, 'getJson'); - await runRetrieveCmd(['--sourcepath', 'somepath', '--json']); + it("should NOT display output with --json", async () => { + const displayStub = sandbox.stub(RetrieveResultFormatter.prototype, "display"); + const getJsonStub = sandbox.stub(RetrieveResultFormatter.prototype, "getJson"); + await runRetrieveCmd(["--sourcepath", "somepath", "--json"]); expect(displayStub.calledOnce).to.equal(false); expect(getJsonStub.calledOnce).to.equal(true); }); - it('should warn users when retrieving CustomField with --metadata', async () => { - const metadata = 'CustomField'; + it("should warn users when retrieving CustomField with --metadata", async () => { + const metadata = "CustomField"; buildComponentSetStub.restore(); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], add: (component: ComponentLike) => { expect(component) - .to.be.a('object') - .and.to.have.property('type') - .and.to.deep.equal({ id: 'customobject', name: 'CustomObject' }); - expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); + .to.be.a("object") + .and.to.have.property("type") + .and.to.deep.equal({ id: "customobject", name: "CustomObject" }); + expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); }, has: (component: ComponentLike) => { - expect(component).to.be.a('object').and.to.have.property('type'); - expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); + expect(component).to.be.a("object").and.to.have.property("type"); + expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); const type = component.type as MetadataType; - if (type.name === 'CustomField') { + if (type.name === "CustomField") { return true; } - if (type.name === 'CustomObject') { + if (type.name === "CustomObject") { return false; } - }, + } }); - await runRetrieveCmd(['--metadata', metadata]); + await runRetrieveCmd(["--metadata", metadata]); expect(warnStub.calledOnce); - expect(warnStub.firstCall.firstArg).to.equal(messages.getMessage('wantsToRetrieveCustomFields')); + expect(warnStub.firstCall.firstArg).to.equal(messages.getMessage("wantsToRetrieveCustomFields")); }); - it('should not warn users when retrieving CustomField,CustomObject with --metadata', async () => { - const metadata = 'CustomField,CustomObject'; + it("should not warn users when retrieving CustomField,CustomObject with --metadata", async () => { + const metadata = "CustomField,CustomObject"; buildComponentSetStub.restore(); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], add: (component: ComponentLike) => { expect(component) - .to.be.a('object') - .and.to.have.property('type') - .and.to.deep.equal({ id: 'customobject', name: 'CustomObject' }); - expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); + .to.be.a("object") + .and.to.have.property("type") + .and.to.deep.equal({ id: "customobject", name: "CustomObject" }); + expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); }, has: (component: ComponentLike) => { - expect(component).to.be.a('object').and.to.have.property('type'); - expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); + expect(component).to.be.a("object").and.to.have.property("type"); + expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); const type = component.type as MetadataType; - if (type.name === 'CustomField') { + if (type.name === "CustomField") { return true; } - if (type.name === 'CustomObject') { + if (type.name === "CustomObject") { return true; } - }, + } }); - await runRetrieveCmd(['--metadata', metadata]); + await runRetrieveCmd(["--metadata", metadata]); expect(warnStub.callCount).to.be.equal(0); }); - it('should warn users when retrieving CustomField with --manifest', async () => { - const manifest = 'package.xml'; + it("should warn users when retrieving CustomField with --manifest", async () => { + const manifest = "package.xml"; buildComponentSetStub.restore(); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], add: (component: ComponentLike) => { expect(component) - .to.be.a('object') - .and.to.have.property('type') - .and.to.deep.equal({ id: 'customobject', name: 'CustomObject' }); - expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); + .to.be.a("object") + .and.to.have.property("type") + .and.to.deep.equal({ id: "customobject", name: "CustomObject" }); + expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); }, has: (component: ComponentLike) => { - expect(component).to.be.a('object').and.to.have.property('type'); - expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); + expect(component).to.be.a("object").and.to.have.property("type"); + expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); const type = component.type as MetadataType; - if (type.name === 'CustomField') { + if (type.name === "CustomField") { return true; } - if (type.name === 'CustomObject') { + if (type.name === "CustomObject") { return false; } - }, + } }); - await runRetrieveCmd(['--manifest', manifest]); + await runRetrieveCmd(["--manifest", manifest]); expect(warnStub.calledOnce); - expect(warnStub.firstCall.firstArg).to.equal(messages.getMessage('wantsToRetrieveCustomFields')); + expect(warnStub.firstCall.firstArg).to.equal(messages.getMessage("wantsToRetrieveCustomFields")); }); - it('should not be warn users when retrieving CustomField,CustomObject with --manifest', async () => { - const manifest = 'package.xml'; + it("should not be warn users when retrieving CustomField,CustomObject with --manifest", async () => { + const manifest = "package.xml"; buildComponentSetStub.restore(); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], add: (component: ComponentLike) => { expect(component) - .to.be.a('object') - .and.to.have.property('type') - .and.to.deep.equal({ id: 'customobject', name: 'CustomObject' }); - expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); + .to.be.a("object") + .and.to.have.property("type") + .and.to.deep.equal({ id: "customobject", name: "CustomObject" }); + expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); }, has: (component: ComponentLike) => { - expect(component).to.be.a('object').and.to.have.property('type'); - expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); + expect(component).to.be.a("object").and.to.have.property("type"); + expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); const type = component.type as MetadataType; - if (type.name === 'CustomField') { + if (type.name === "CustomField") { return true; } - if (type.name === 'CustomObject') { + if (type.name === "CustomObject") { return true; } - }, + } }); - await runRetrieveCmd(['--manifest', manifest]); + await runRetrieveCmd(["--manifest", manifest]); expect(warnStub.callCount).to.be.equal(0); }); }); diff --git a/test/nuts/seeds/retrieve.retrievetargetdir.seed.ts b/test/nuts/seeds/retrieve.retrievetargetdir.seed.ts new file mode 100644 index 000000000..3df7db993 --- /dev/null +++ b/test/nuts/seeds/retrieve.retrievetargetdir.seed.ts @@ -0,0 +1,51 @@ +/* + * 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 { SourceTestkit } from '@salesforce/source-testkit'; +import { JsonMap } from '@salesforce/ts-types'; +import { TEST_REPOS_MAP } from '../testMatrix'; + +// DO NOT TOUCH. generateNuts.ts will insert these values +const REPO = TEST_REPOS_MAP.get('%REPO_URL%'); + +context('Retrieve metadata NUTs [name: %REPO_NAME%]', () => { + let testkit: SourceTestkit; + + before(async () => { + testkit = await SourceTestkit.create({ + repository: REPO.gitUrl, + nut: __filename, + }); + await testkit.trackGlobs(testkit.packageGlobs); + await testkit.deploy({ args: `--sourcepath ${testkit.packageNames.join(',')}` }); + }); + + after(async () => { + try { + await testkit?.clean(); + } catch (e) { + // if the it fails to clean, don't throw so NUTs will pass + // eslint-disable-next-line no-console + console.log('Clean Failed: ', e); + } + }); + + describe('--retrievetargetdir flag', () => { + for (const testCase of REPO.retrieve.retrievetargetdir) { + it(`should retrieve ${testCase.toRetrieve}`, async () => { + await testkit.modifyLocalGlobs(testCase.toVerify); + await testkit.retrieve({ args: `--retrievetargetdir targetdir --metadata ${testCase.toRetrieve}` }); + await testkit.expect.filesToBeRetrieved(testCase.toVerify, testCase.toIgnore); + }); + } + + it('should throw an error if the metadata is not valid', async () => { + const retrieve = (await testkit.retrieve({ args: '--retrievetargetdir targetdir --metadata DOES_NOT_EXIST', exitCode: 1 })) as JsonMap; + testkit.expect.errorToHaveName(retrieve, 'SfError'); + }); + }); +}); diff --git a/test/nuts/testMatrix.ts b/test/nuts/testMatrix.ts index a8cc97d2e..b0792accd 100644 --- a/test/nuts/testMatrix.ts +++ b/test/nuts/testMatrix.ts @@ -83,6 +83,9 @@ const testRepos: RepoConfig[] = [ toIgnore: ['foo-bar/app/lwc/mycomponent/mycomponent.js-meta.xml'], }, ], + retrievetargetdir: [ + { toRetrieve: 'ApexClass', toVerify: ['targetdir/**/*.cls'] }, + ], }, convert: { sourcepath: [ @@ -193,6 +196,10 @@ const testRepos: RepoConfig[] = [ ], }, ], + retrievetargetdir: [ + { toRetrieve: 'ApexClass', toVerify: ['targetdir/classes/*'] }, + ], + }, convert: { sourcepath: [ @@ -264,6 +271,7 @@ export type RepoConfig = { metadata: RetrieveTestCase[]; sourcepath: RetrieveTestCase[]; manifest: RetrieveTestCase[]; + retrievetargetdir: RetrieveTestCase[]; }; convert: { metadata: ConvertTestCase[]; From ededda5b7efa8e62d8abb3e6ea5285ea560a7713 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 25 Oct 2022 11:43:38 -0600 Subject: [PATCH 2/9] feat: add retrievetargetdir to source:retrieve @W-10735446@ --- messages/retrieve.json | 9 +- src/commands/force/source/retrieve.ts | 216 +++++++++++++------------- 2 files changed, 118 insertions(+), 107 deletions(-) diff --git a/messages/retrieve.json b/messages/retrieve.json index bc6234e8c..60f4edbdd 100644 --- a/messages/retrieve.json +++ b/messages/retrieve.json @@ -26,7 +26,11 @@ "forceoverwrite": "ignore conflict warnings and overwrite changes to the project" }, "flagsLong": { - "retrievetargetdir": "The root of the directory structure into which the source files are retrieved.", + "retrievetargetdir": [ + "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 retrieved source files are placed in the matching package directory.", + "Running the command multiple times with the same target will add new files and overwrite existing files." + ], "wait": "Number of minutes to wait for the command to complete and display results to the terminal window. If the command continues to run after the wait period, the CLI returns control of the terminal window to you.", "manifest": [ "The complete path for the manifest (package.xml) file that specifies the components to retrieve.", @@ -55,6 +59,5 @@ "errorColumn": "PROBLEM", "nothingToRetrieve": "Specify a source path, manifest, metadata, or package names to retrieve.", "wantsToRetrieveCustomFields": "Because you're retrieving one or more CustomFields, we're also retrieving the CustomObject to which it's associated.", - "retrieveWontDelete": "You currently have files deleted in your org. The retrieve command will NOT delete them from your local project", - "retrieveTargetDirOverlapsPackageDir": "The retrieve target directory [%s] overlaps with the package directory [%s]." + "retrieveWontDelete": "You currently have files deleted in your org. The retrieve command will NOT delete them from your local project" } diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index 7e6346d97..b2b648205 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -5,86 +5,86 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import * as os from "os"; -import { join, resolve, dirname } from "path"; -import * as fs from "fs"; -import { flags, FlagsConfig } from "@salesforce/command"; -import { Messages, SfError, SfProject } from "@salesforce/core"; -import { Duration } from "@salesforce/kit"; -import { ComponentSet, ComponentSetBuilder, RequestStatus, RetrieveResult } from "@salesforce/source-deploy-retrieve"; -import { SourceTracking } from "@salesforce/source-tracking"; -import { SourceCommand } from "../../../sourceCommand"; +import * as os from 'os'; +import { dirname, join, resolve } from 'path'; +import * as fs from 'fs'; +import { flags, FlagsConfig } from '@salesforce/command'; +import { Messages, SfError, SfProject } from '@salesforce/core'; +import { Duration } from '@salesforce/kit'; +import { ComponentSet, ComponentSetBuilder, RequestStatus, RetrieveResult } from '@salesforce/source-deploy-retrieve'; +import { SourceTracking } from '@salesforce/source-tracking'; +import { SourceCommand } from '../../../sourceCommand'; import { PackageRetrieval, RetrieveCommandResult, - RetrieveResultFormatter -} from "../../../formatters/retrieveResultFormatter"; -import { filterConflictsByComponentSet, trackingSetup, updateTracking } from "../../../trackingFunctions"; + RetrieveResultFormatter, +} from '../../../formatters/retrieveResultFormatter'; +import { filterConflictsByComponentSet, trackingSetup, updateTracking } from '../../../trackingFunctions'; Messages.importMessagesDirectory(__dirname); -const messages = Messages.loadMessages("@salesforce/plugin-source", "retrieve"); -const spinnerMessages = Messages.loadMessages("@salesforce/plugin-source", "spinner"); +const messages = Messages.loadMessages('@salesforce/plugin-source', 'retrieve'); +const spinnerMessages = Messages.loadMessages('@salesforce/plugin-source', 'spinner'); export class Retrieve extends SourceCommand { - public static readonly description = messages.getMessage("description"); - public static readonly examples = messages.getMessage("examples").split(os.EOL); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessage('examples').split(os.EOL); public static readonly requiresProject = true; public static readonly requiresUsername = true; public static readonly flagsConfig: FlagsConfig = { retrievetargetdir: flags.directory({ - char: "r", - description: messages.getMessage("flags.retrievetargetdir"), - longDescription: messages.getMessage("flagsLong.retrievetargetdir"), - exclusive: ["packagenames", "sourcepath"] + char: 'r', + description: messages.getMessage('flags.retrievetargetdir'), + longDescription: messages.getMessage('flagsLong.retrievetargetdir'), + exclusive: ['packagenames', 'sourcepath'], }), apiversion: flags.builtin({ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ // @ts-ignore force char override for backward compat - char: "a" + char: 'a', }), sourcepath: flags.array({ - char: "p", - description: messages.getMessage("flags.sourcePath"), - longDescription: messages.getMessage("flagsLong.sourcePath"), - exclusive: ["manifest", "metadata"] + char: 'p', + description: messages.getMessage('flags.sourcePath'), + longDescription: messages.getMessage('flagsLong.sourcePath'), + exclusive: ['manifest', 'metadata'], }), wait: flags.minutes({ - char: "w", + char: 'w', default: Duration.minutes(SourceCommand.DEFAULT_WAIT_MINUTES), min: Duration.minutes(1), - description: messages.getMessage("flags.wait"), - longDescription: messages.getMessage("flagsLong.wait") + description: messages.getMessage('flags.wait'), + longDescription: messages.getMessage('flagsLong.wait'), }), manifest: flags.filepath({ - char: "x", - description: messages.getMessage("flags.manifest"), - longDescription: messages.getMessage("flagsLong.manifest"), - exclusive: ["metadata", "sourcepath"] + char: 'x', + description: messages.getMessage('flags.manifest'), + longDescription: messages.getMessage('flagsLong.manifest'), + exclusive: ['metadata', 'sourcepath'], }), metadata: flags.array({ - char: "m", - description: messages.getMessage("flags.metadata"), - longDescription: messages.getMessage("flagsLong.metadata"), - exclusive: ["manifest", "sourcepath"] + char: 'm', + description: messages.getMessage('flags.metadata'), + longDescription: messages.getMessage('flagsLong.metadata'), + exclusive: ['manifest', 'sourcepath'], }), packagenames: flags.array({ - char: "n", - description: messages.getMessage("flags.packagename") + char: 'n', + description: messages.getMessage('flags.packagename'), }), tracksource: flags.boolean({ - char: "t", - description: messages.getMessage("flags.tracksource") + char: 't', + description: messages.getMessage('flags.tracksource'), }), forceoverwrite: flags.boolean({ - char: "f", - description: messages.getMessage("flags.forceoverwrite"), - dependsOn: ["tracksource"] + char: 'f', + description: messages.getMessage('flags.forceoverwrite'), + dependsOn: ['tracksource'], }), verbose: flags.builtin({ - description: messages.getMessage("flags.verbose") - }) + description: messages.getMessage('flags.verbose'), + }), }; - protected readonly lifecycleEventNames = ["preretrieve", "postretrieve"]; + protected readonly lifecycleEventNames = ['preretrieve', 'postretrieve']; protected retrieveResult: RetrieveResult; protected tracking: SourceTracking; @@ -101,7 +101,7 @@ export class Retrieve extends SourceCommand { // we need something to retrieve const retrieveInputs = [this.flags.manifest, this.flags.metadata, this.flags.sourcepath, this.flags.packagenames]; if (!retrieveInputs.some((x) => x)) { - throw new SfError(messages.getMessage("nothingToRetrieve")); + throw new SfError(messages.getMessage('nothingToRetrieve')); } if (this.flags.tracksource) { this.tracking = await trackingSetup({ @@ -109,68 +109,68 @@ export class Retrieve extends SourceCommand { org: this.org, project: this.project, ignoreConflicts: true, - commandName: "force:source:retrieve" + commandName: 'force:source:retrieve', }); } } protected async retrieve(): Promise { - this.ux.startSpinner(spinnerMessages.getMessage("retrieve.componentSetBuild")); + this.ux.startSpinner(spinnerMessages.getMessage('retrieve.componentSetBuild')); this.componentSet = await ComponentSetBuilder.build({ - apiversion: this.getFlag("apiversion"), + apiversion: this.getFlag('apiversion'), sourceapiversion: await this.getSourceApiVersion(), - packagenames: this.getFlag("packagenames"), - sourcepath: this.getFlag("sourcepath"), + packagenames: this.getFlag('packagenames'), + sourcepath: this.getFlag('sourcepath'), manifest: this.flags.manifest && { - manifestPath: this.getFlag("manifest"), - directoryPaths: this.flags.retrievetargetdir ? [] : this.getPackageDirs() + manifestPath: this.getFlag('manifest'), + directoryPaths: this.flags.retrievetargetdir ? [] : this.getPackageDirs(), }, metadata: this.flags.metadata && { - metadataEntries: this.getFlag("metadata"), - directoryPaths: this.flags.retrievetargetdir ? [] : this.getPackageDirs() - } + metadataEntries: this.getFlag('metadata'), + directoryPaths: this.flags.retrievetargetdir ? [] : this.getPackageDirs(), + }, }); - if (this.getFlag("manifest") || this.getFlag("metadata")) { + if (this.getFlag('manifest') || this.getFlag('metadata')) { if (this.wantsToRetrieveCustomFields()) { - this.ux.warn(messages.getMessage("wantsToRetrieveCustomFields")); - this.componentSet.add({ fullName: ComponentSet.WILDCARD, type: { id: "customobject", name: "CustomObject" } }); + this.ux.warn(messages.getMessage('wantsToRetrieveCustomFields')); + this.componentSet.add({ fullName: ComponentSet.WILDCARD, type: { id: 'customobject', name: 'CustomObject' } }); } } - if (this.getFlag("tracksource")) { + if (this.getFlag('tracksource')) { // will throw if conflicts exist - if (!this.getFlag("forceoverwrite")) { + if (!this.getFlag('forceoverwrite')) { await filterConflictsByComponentSet({ tracking: this.tracking, components: this.componentSet, ux: this.ux }); } const remoteDeletes = await this.tracking.getChanges({ - origin: "remote", - state: "delete", - format: "string" + origin: 'remote', + state: 'delete', + format: 'string', }); if (remoteDeletes.length) { - this.ux.warn(messages.getMessage("retrieveWontDelete")); + this.ux.warn(messages.getMessage('retrieveWontDelete')); } } - await this.lifecycle.emit("preretrieve", this.componentSet.toArray()); + await this.lifecycle.emit('preretrieve', this.componentSet.toArray()); this.ux.setSpinnerStatus( - spinnerMessages.getMessage("retrieve.sendingRequest", [ - this.componentSet.sourceApiVersion || this.componentSet.apiVersion + spinnerMessages.getMessage('retrieve.sendingRequest', [ + this.componentSet.sourceApiVersion || this.componentSet.apiVersion, ]) ); const mdapiRetrieve = await this.componentSet.retrieve({ usernameOrConnection: this.org.getUsername(), merge: true, - output: this.getFlag("retrievetargetdir") || this.project.getDefaultPackage().fullPath, - packageOptions: this.getFlag("packagenames") + output: this.getFlag('retrievetargetdir') || this.project.getDefaultPackage().fullPath, + packageOptions: this.getFlag('packagenames'), }); - this.ux.setSpinnerStatus(spinnerMessages.getMessage("retrieve.polling")); - this.retrieveResult = await mdapiRetrieve.pollStatus({ timeout: this.getFlag("wait") }); + this.ux.setSpinnerStatus(spinnerMessages.getMessage('retrieve.polling')); + this.retrieveResult = await mdapiRetrieve.pollStatus({ timeout: this.getFlag('wait') }); - await this.lifecycle.emit("postretrieve", this.retrieveResult.getFileResponses()); + await this.lifecycle.emit('postretrieve', this.retrieveResult.getFileResponses()); this.ux.stopSpinner(); } @@ -181,7 +181,7 @@ export class Retrieve extends SourceCommand { [RequestStatus.Failed, 1], [RequestStatus.InProgress, 69], [RequestStatus.Pending, 69], - [RequestStatus.Canceling, 69] + [RequestStatus.Canceling, 69], ]); this.setExitCode(StatusCodeMap.get(this.retrieveResult.response.status) ?? 1); @@ -191,14 +191,14 @@ export class Retrieve extends SourceCommand { const packages: PackageRetrieval[] = []; const projectPath = await SfProject.resolveProjectPath(); - this.getFlag("packagenames", []).forEach((name) => { + this.getFlag('packagenames', []).forEach((name) => { packages.push({ name, path: join(projectPath, name) }); }); const formatterOptions = { - waitTime: this.getFlag("wait").quantity, - verbose: this.getFlag("verbose", false), - packages + waitTime: this.getFlag('wait').quantity, + verbose: this.getFlag('verbose', false), + packages, }; const formatter = new RetrieveResultFormatter(this.logger, this.ux, formatterOptions, this.retrieveResult); @@ -211,47 +211,46 @@ export class Retrieve extends SourceCommand { } private async maybeUpdateTracking(): Promise { - if (this.getFlag("tracksource", false)) { + if (this.getFlag('tracksource', false)) { return updateTracking({ tracking: this.tracking, result: this.retrieveResult, ux: this.ux }); } } private wantsToRetrieveCustomFields(): boolean { const hasCustomField = this.componentSet.has({ - type: { name: "CustomField", id: "customfield" }, - fullName: ComponentSet.WILDCARD + type: { name: 'CustomField', id: 'customfield' }, + fullName: ComponentSet.WILDCARD, }); const hasCustomObject = this.componentSet.has({ - type: { name: "CustomObject", id: "customobject" }, - fullName: ComponentSet.WILDCARD + type: { name: 'CustomObject', id: 'customobject' }, + fullName: ComponentSet.WILDCARD, }); return hasCustomField && !hasCustomObject; } private async moveResultsForRetrieveTargetDir(): Promise { - async function mv(src: string, dest: string): Promise { - // remove dest if it exists - try{ - const destStat = await fs.promises.stat(dest); - if (destStat.isDirectory()) { - await fs.promises.rmdir(dest, { recursive: true }); - } else { - await fs.promises.rm(dest, { force: true }); - } - } catch(err) { - if ((err as Error)['code'] !== 'ENOENT') { - throw err; - } - } + async function mv(src: string): Promise { + let directories: string[] = []; + let files: string[] = []; const srcStat = await fs.promises.stat(src); if (srcStat.isDirectory()) { - await fs.promises.rename(src, dest); + const contents = await fs.promises.readdir(src); + directories = contents.filter((c) => fs.statSync(join(src, c)).isDirectory()); + files = contents.filter((c) => !fs.statSync(join(src, c)).isDirectory()); } else { - await fs.promises.mkdir(dirname(dest), { recursive: true }); - await fs.promises.cp(src, dest); + files.push(src); } - }; + await Promise.all( + files.map(async (file): Promise => { + const dest = join(src.replace(join('main', 'default'), ''), file); + const destDir = dirname(dest); + await fs.promises.mkdir(destDir, { recursive: true }); + await fs.promises.cp(join(src, file), dest); + }) + ); + return directories; + } if (!this.flags.retrievetargetdir) { return; @@ -267,9 +266,18 @@ export class Retrieve extends SourceCommand { if (overlapsProject) { return; } + + const moveQueue: string[] = []; // move contents of 'main/default' to 'retrievetargetdir' - const contents = await fs.promises.readdir(join(this.flags.retrievetargetdir as string, 'main', 'default')); - await Promise.all(contents.map((file) => mv(join(this.flags.retrievetargetdir as string, 'main', 'default', file), join(this.flags.retrievetargetdir as string, file)))); + moveQueue.push(join(resolvedTargetDir, 'main', 'default')); + while (moveQueue.length) { + const src = moveQueue.shift(); + if (src) { + // eslint-disable-next-line no-await-in-loop + const dirs = await mv(src); + moveQueue.push(...dirs.map((dir) => join(src, dir))); + } + } // remove 'main/default' await fs.promises.rmdir(join(this.flags.retrievetargetdir as string, 'main'), { recursive: true }); this.retrieveResult.getFileResponses().forEach((fileResponse) => { From 267611978973d90328161d3fbc04b7b947462587 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 25 Oct 2022 12:28:29 -0600 Subject: [PATCH 3/9] chore: use rename instead of copy --- src/commands/force/source/retrieve.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index b2b648205..659f20df5 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -246,7 +246,7 @@ export class Retrieve extends SourceCommand { const dest = join(src.replace(join('main', 'default'), ''), file); const destDir = dirname(dest); await fs.promises.mkdir(destDir, { recursive: true }); - await fs.promises.cp(join(src, file), dest); + await fs.promises.rename(join(src, file), dest); }) ); return directories; From c5ec4f68eb0bfb5cd97d4522b6b4c777b51ad4d5 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Wed, 26 Oct 2022 13:14:31 -0600 Subject: [PATCH 4/9] chore: cleaup and add ut for changes @W-10735446@ --- src/commands/force/source/retrieve.ts | 21 ++++----- src/promiseQueue.ts | 43 +++++++++++++++++++ test/promisesQueue.test.ts | 62 +++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 src/promiseQueue.ts create mode 100644 test/promisesQueue.test.ts diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index 659f20df5..51ff260fb 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -20,6 +20,7 @@ import { RetrieveResultFormatter, } from '../../../formatters/retrieveResultFormatter'; import { filterConflictsByComponentSet, trackingSetup, updateTracking } from '../../../trackingFunctions'; +import { promisesQueue } from '../../../promiseQueue'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-source', 'retrieve'); @@ -236,18 +237,21 @@ export class Retrieve extends SourceCommand { const srcStat = await fs.promises.stat(src); if (srcStat.isDirectory()) { const contents = await fs.promises.readdir(src); - directories = contents.filter((c) => fs.statSync(join(src, c)).isDirectory()); + directories = contents.filter((c) => fs.statSync(join(src, c)).isDirectory()).map((c) => join(src, c)); files = contents.filter((c) => !fs.statSync(join(src, c)).isDirectory()); } else { files.push(src); } - await Promise.all( - files.map(async (file): Promise => { + await promisesQueue( + files, + async (file: string): Promise => { 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; } @@ -270,14 +274,7 @@ export class Retrieve extends SourceCommand { const moveQueue: string[] = []; // move contents of 'main/default' to 'retrievetargetdir' moveQueue.push(join(resolvedTargetDir, 'main', 'default')); - while (moveQueue.length) { - const src = moveQueue.shift(); - if (src) { - // eslint-disable-next-line no-await-in-loop - const dirs = await mv(src); - moveQueue.push(...dirs.map((dir) => join(src, dir))); - } - } + await promisesQueue(moveQueue, mv, 5, true); // remove 'main/default' await fs.promises.rmdir(join(this.flags.retrievetargetdir as string, 'main'), { recursive: true }); this.retrieveResult.getFileResponses().forEach((fileResponse) => { diff --git a/src/promiseQueue.ts b/src/promiseQueue.ts new file mode 100644 index 000000000..44b3c6baa --- /dev/null +++ b/src/promiseQueue.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022, 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 + */ +/** + * Function to throttle a list of promises. + * + * @param sourceQueue - The list of items to process. + * @param producer - The function to produce a promise from an item. + * @param concurrency - The number of promises to run at a time. + * @param queueResults - Whether to queue the results of the promises. + */ +export async function promisesQueue( + sourceQueue: T[], + producer: (T) => Promise, + concurrency: number, + queueResults = false +): Promise { + const results: T[] = []; + let queue = [...sourceQueue]; + while (queue.length > 0) { + const next = queue.slice(0, concurrency); + queue = queue.slice(concurrency); + // eslint-disable-next-line no-await-in-loop + const nextResults = (await Promise.all(next.map(producer))) + .reduce((acc, val) => { + if (Array.isArray(val)) { + acc.push(...val); + } else { + acc.push(val); + } + return acc; + }, []) + .filter((val) => val !== undefined); + if (queueResults) { + queue.push(...nextResults); + } + results.push(...nextResults); + } + return results; +} diff --git a/test/promisesQueue.test.ts b/test/promisesQueue.test.ts new file mode 100644 index 000000000..4a53da360 --- /dev/null +++ b/test/promisesQueue.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022, 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 { expect } from 'chai'; +import { promisesQueue } from '../src/promiseQueue'; +describe('promisesQueue', () => { + const numberResolver = (n: number) => Promise.resolve(n); + it('should handle 0 queue entries', async () => { + const results = await promisesQueue([], numberResolver, 1); + expect(results).to.deep.equal([]); + }); + it('should handle many queue entries', async () => { + const results = await promisesQueue([1], numberResolver, 1); + expect(results).to.have.length(1); + expect(results[0]).to.equal(1); + }); + it('should handle 500 queue entry one at a time', async () => { + const a = Array.from({ length: 500 }, (v, i) => i); + const results = await promisesQueue(a, numberResolver, 1); + expect(results).to.have.length(500); + expect(results[499]).to.equal(499); + }); + it('should handle 500 queue entry 10 at a time', async () => { + const a = Array.from({ length: 500 }, (v, i) => i); + const results = await promisesQueue(a, numberResolver, 10); + expect(results).to.have.length(500); + expect(results[499]).to.equal(499); + }); + it('should handle 500 queue entry 500 at a time', async () => { + const a = Array.from({ length: 500 }, (v, i) => i); + const results = await promisesQueue(a, numberResolver, 500); + expect(results).to.have.length(500); + expect(results[499]).to.equal(499); + }); + it('should reject at entry two', async () => { + await promisesQueue( + [1, 2], + (n: number): Promise => (n === 2 ? Promise.reject(n) : Promise.resolve(n)), + 1 + ).catch((e) => expect(e).to.equal(2)); + }); + it('should queue 250 more with a total of 750 promises', async () => { + let count = 0; + const moreResolver = (n: number): Promise => { + const rn = n === 0 && count === 0 ? Array.from({ length: 250 }, (v, i) => i + 500) : n; + count++; + return Promise.resolve(count < 502 ? rn : []); + }; + const a = Array.from({ length: 500 }, (v, i) => i); + const results = await promisesQueue(a, moreResolver, 500, true); + expect(results).to.have.length(750); + }); + it('should handle 5000 queue entry 100 at a time', async () => { + const a = Array.from({ length: 5000 }, (v, i) => i); + const results = await promisesQueue(a, numberResolver, 100); + expect(results).to.have.length(5000); + expect(results[499]).to.equal(499); + }); +}); From df8d554feffe6c5e5d633a8867c5ff6a8c3a67ba Mon Sep 17 00:00:00 2001 From: peternhale Date: Thu, 27 Oct 2022 12:25:20 -0600 Subject: [PATCH 5/9] Update src/commands/force/source/retrieve.ts Co-authored-by: Shane McLaughlin --- src/commands/force/source/retrieve.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index 51ff260fb..a3076d75b 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -271,10 +271,8 @@ export class Retrieve extends SourceCommand { return; } - const moveQueue: string[] = []; // move contents of 'main/default' to 'retrievetargetdir' - moveQueue.push(join(resolvedTargetDir, 'main', 'default')); - await promisesQueue(moveQueue, mv, 5, true); + await promisesQueue([join(resolvedTargetDir, 'main', 'default')], mv, 5, true); // remove 'main/default' await fs.promises.rmdir(join(this.flags.retrievetargetdir as string, 'main'), { recursive: true }); this.retrieveResult.getFileResponses().forEach((fileResponse) => { From b25ac349ce3fc7598007da46e14cd7069566d4be Mon Sep 17 00:00:00 2001 From: peternhale Date: Thu, 27 Oct 2022 13:05:23 -0600 Subject: [PATCH 6/9] Update src/commands/force/source/retrieve.ts Co-authored-by: Shane McLaughlin --- src/commands/force/source/retrieve.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index a3076d75b..3a1718710 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -259,9 +259,8 @@ export class Retrieve extends SourceCommand { if (!this.flags.retrievetargetdir) { return; } - let overlapsProject = true; const resolvedTargetDir = resolve(this.flags.retrievetargetdir as string); - overlapsProject = !!this.project.getPackageDirectories().find((pkgDir) => { + const overlapsProject = !!this.project.getPackageDirectories().find((pkgDir) => { if (pkgDir.fullPath) { return pkgDir.fullPath.includes(resolvedTargetDir); } From a121b017731f8d2189f3362afac5f03740a238b5 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Thu, 27 Oct 2022 14:31:42 -0600 Subject: [PATCH 7/9] chore: address requested changes --- messages/retrieve.json | 5 +- src/commands/force/source/retrieve.ts | 45 ++-- src/promiseQueue.ts | 15 +- test/commands/source/retrieve.test.ts | 303 +++++++++++++------------- 4 files changed, 190 insertions(+), 178 deletions(-) diff --git a/messages/retrieve.json b/messages/retrieve.json index 60f4edbdd..7e0df9933 100644 --- a/messages/retrieve.json +++ b/messages/retrieve.json @@ -28,7 +28,7 @@ "flagsLong": { "retrievetargetdir": [ "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 retrieved source files are placed in the matching package directory.", + "If the target directory matches one of the package directories in your sfdx-project.json file, the command will fail.", "Running the command multiple times with the same target will add new files and overwrite existing files." ], "wait": "Number of minutes to wait for the command to complete and display results to the terminal window. If the command continues to run after the wait period, the CLI returns control of the terminal window to you.", @@ -59,5 +59,6 @@ "errorColumn": "PROBLEM", "nothingToRetrieve": "Specify a source path, manifest, metadata, or package names to retrieve.", "wantsToRetrieveCustomFields": "Because you're retrieving one or more CustomFields, we're also retrieving the CustomObject to which it's associated.", - "retrieveWontDelete": "You currently have files deleted in your org. The retrieve command will NOT delete them from your local project" + "retrieveWontDelete": "You currently have files deleted in your org. The retrieve command will NOT delete them from your local project", + "retrieveTargetDirOverlapsPackage": "The retrieve target directory [%s] overlaps one of your package directories. Please specify a different retrieve target directory." } diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index 3a1718710..843b9eb3a 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -88,6 +88,7 @@ export class Retrieve extends SourceCommand { protected readonly lifecycleEventNames = ['preretrieve', 'postretrieve']; protected retrieveResult: RetrieveResult; protected tracking: SourceTracking; + private resolvedTargetDir: string; public async run(): Promise { await this.preChecks(); @@ -99,6 +100,12 @@ export class Retrieve extends SourceCommand { } protected async preChecks(): Promise { + if (this.flags.retrievetargetdir) { + this.resolvedTargetDir = resolve(this.flags.retrievetargetdir as string); + if (this.overlapsPackage()) { + throw messages.createError('retrieveTargetDirOverlapsPackage', [this.flags.retrievetargetdir as string]); + } + } // we need something to retrieve const retrieveInputs = [this.flags.manifest, this.flags.metadata, this.flags.sourcepath, this.flags.packagenames]; if (!retrieveInputs.some((x) => x)) { @@ -236,9 +243,20 @@ export class Retrieve extends SourceCommand { let files: string[] = []; const srcStat = await fs.promises.stat(src); if (srcStat.isDirectory()) { - const contents = await fs.promises.readdir(src); - directories = contents.filter((c) => fs.statSync(join(src, c)).isDirectory()).map((c) => join(src, c)); - files = contents.filter((c) => !fs.statSync(join(src, c)).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); } @@ -259,23 +277,22 @@ export class Retrieve extends SourceCommand { if (!this.flags.retrievetargetdir) { return; } - const resolvedTargetDir = resolve(this.flags.retrievetargetdir as string); - const overlapsProject = !!this.project.getPackageDirectories().find((pkgDir) => { - if (pkgDir.fullPath) { - return pkgDir.fullPath.includes(resolvedTargetDir); - } - return false; - }); - if (overlapsProject) { - return; - } // move contents of 'main/default' to 'retrievetargetdir' - await promisesQueue([join(resolvedTargetDir, 'main', 'default')], mv, 5, true); + await promisesQueue([join(this.resolvedTargetDir, 'main', 'default')], mv, 5, true); // remove 'main/default' await fs.promises.rmdir(join(this.flags.retrievetargetdir as string, 'main'), { recursive: true }); this.retrieveResult.getFileResponses().forEach((fileResponse) => { fileResponse.filePath = fileResponse.filePath.replace(join('main', 'default'), ''); }); } + + private overlapsPackage(): boolean { + return !!this.project.getPackageDirectories().find((pkgDir) => { + if (pkgDir.fullPath) { + return pkgDir.fullPath.includes(this.resolvedTargetDir); + } + return false; + }); + } } diff --git a/src/promiseQueue.ts b/src/promiseQueue.ts index 44b3c6baa..b03888025 100644 --- a/src/promiseQueue.ts +++ b/src/promiseQueue.ts @@ -4,6 +4,8 @@ * 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 { ensureArray } from '@salesforce/kit'; + /** * Function to throttle a list of promises. * @@ -24,16 +26,9 @@ export async function promisesQueue( const next = queue.slice(0, concurrency); queue = queue.slice(concurrency); // eslint-disable-next-line no-await-in-loop - const nextResults = (await Promise.all(next.map(producer))) - .reduce((acc, val) => { - if (Array.isArray(val)) { - acc.push(...val); - } else { - acc.push(val); - } - return acc; - }, []) - .filter((val) => val !== undefined); + const nextResults = (await Promise.all(ensureArray(next.map(producer)))) + .flat(1) + .filter((val) => val !== undefined) as T[]; if (queueResults) { queue.push(...nextResults); } diff --git a/test/commands/source/retrieve.test.ts b/test/commands/source/retrieve.test.ts index 2ffa44ea1..b555923fc 100644 --- a/test/commands/source/retrieve.test.ts +++ b/test/commands/source/retrieve.test.ts @@ -5,43 +5,43 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { join } from "path"; -import * as sinon from "sinon"; -import { expect } from "chai"; +import { join } from 'path'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; import { ComponentLike, ComponentSet, ComponentSetBuilder, ComponentSetOptions, MetadataType, - RetrieveOptions -} from "@salesforce/source-deploy-retrieve"; -import { Lifecycle, Messages, Org, SfProject } from "@salesforce/core"; -import { fromStub, stubInterface, stubMethod } from "@salesforce/ts-sinon"; -import { Config } from "@oclif/core"; -import { UX } from "@salesforce/command"; -import { Retrieve } from "../../../src/commands/force/source/retrieve"; -import { RetrieveCommandResult, RetrieveResultFormatter } from "../../../src/formatters/retrieveResultFormatter"; -import { getRetrieveResult } from "./retrieveResponses"; -import { exampleSourceComponent } from "./testConsts"; + RetrieveOptions, +} from '@salesforce/source-deploy-retrieve'; +import { Lifecycle, Messages, Org, SfProject } from '@salesforce/core'; +import { fromStub, stubInterface, stubMethod } from '@salesforce/ts-sinon'; +import { Config } from '@oclif/core'; +import { UX } from '@salesforce/command'; +import { Retrieve } from '../../../src/commands/force/source/retrieve'; +import { RetrieveCommandResult, RetrieveResultFormatter } from '../../../src/formatters/retrieveResultFormatter'; +import { getRetrieveResult } from './retrieveResponses'; +import { exampleSourceComponent } from './testConsts'; Messages.importMessagesDirectory(__dirname); -const messages = Messages.loadMessages("@salesforce/plugin-source", "retrieve"); +const messages = Messages.loadMessages('@salesforce/plugin-source', 'retrieve'); -describe("force:source:retrieve", () => { +describe('force:source:retrieve', () => { const sandbox = sinon.createSandbox(); - const username = "retrieve-test@org.com"; - const packageXml = "package.xml"; - const defaultPackagePath = "defaultPackagePath"; + const username = 'retrieve-test@org.com'; + const packageXml = 'package.xml'; + const defaultPackagePath = 'defaultPackagePath'; const oclifConfigStub = fromStub(stubInterface(sandbox)); - const retrieveResult = getRetrieveResult("success"); + const retrieveResult = getRetrieveResult('success'); const expectedResults: RetrieveCommandResult = { response: retrieveResult.response, inboundFiles: retrieveResult.getFileResponses(), packages: [], - warnings: [] + warnings: [], }; // Stubs @@ -69,33 +69,34 @@ describe("force:source:retrieve", () => { const runRetrieveCmd = async (params: string[]) => { const cmd = new TestRetrieve(params, oclifConfigStub); - stubMethod(sandbox, SfProject, "resolveProjectPath").resolves(join("path", "to", "package")); - stubMethod(sandbox, cmd, "assignProject").callsFake(() => { + stubMethod(sandbox, SfProject, 'resolveProjectPath').resolves(join('path', 'to', 'package')); + stubMethod(sandbox, cmd, 'assignProject').callsFake(() => { const SfProjectStub = fromStub( stubInterface(sandbox, { getDefaultPackage: () => ({ fullPath: defaultPackagePath }), getUniquePackageDirectories: () => [{ fullPath: defaultPackagePath }], - resolveProjectConfig: resolveProjectConfigStub + getPackageDirectories: () => [{ fullPath: defaultPackagePath }], + resolveProjectConfig: resolveProjectConfigStub, }) ); cmd.setProject(SfProjectStub); }); - stubMethod(sandbox, cmd, "assignOrg").callsFake(() => { + stubMethod(sandbox, cmd, 'assignOrg').callsFake(() => { const orgStub = fromStub( stubInterface(sandbox, { - getUsername: () => username + getUsername: () => username, }) ); cmd.setOrg(orgStub); }); // keep the stdout from showing up in the test output - stubMethod(sandbox, UX.prototype, "log"); - stubMethod(sandbox, UX.prototype, "setSpinnerStatus"); - stubMethod(sandbox, UX.prototype, "startSpinner"); - stubMethod(sandbox, UX.prototype, "stopSpinner"); - stubMethod(sandbox, UX.prototype, "styledHeader"); - stubMethod(sandbox, UX.prototype, "table"); - stubMethod(sandbox, Retrieve.prototype, "moveResultsForRetrieveTargetDir"); + stubMethod(sandbox, UX.prototype, 'log'); + stubMethod(sandbox, UX.prototype, 'setSpinnerStatus'); + stubMethod(sandbox, UX.prototype, 'startSpinner'); + stubMethod(sandbox, UX.prototype, 'stopSpinner'); + stubMethod(sandbox, UX.prototype, 'styledHeader'); + stubMethod(sandbox, UX.prototype, 'table'); + stubMethod(sandbox, Retrieve.prototype, 'moveResultsForRetrieveTargetDir'); return cmd.runIt(); }; @@ -104,16 +105,16 @@ describe("force:source:retrieve", () => { pollStub = sandbox.stub().resolves(retrieveResult); retrieveStub = sandbox.stub().resolves({ pollStatus: pollStub, - retrieveId: retrieveResult.response.id + retrieveId: retrieveResult.response.id, }); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], - has: () => false + has: () => false, }); - lifecycleEmitStub = sandbox.stub(Lifecycle.prototype, "emit"); - warnStub = stubMethod(sandbox, UX.prototype, "warn"); + lifecycleEmitStub = sandbox.stub(Lifecycle.prototype, 'emit'); + warnStub = stubMethod(sandbox, UX.prototype, 'warn'); }); afterEach(() => { @@ -128,7 +129,7 @@ describe("force:source:retrieve", () => { manifest: undefined, metadata: undefined, apiversion: undefined, - sourceapiversion: undefined + sourceapiversion: undefined, }; const expectedArgs = { ...defaultArgs, ...overrides }; @@ -142,7 +143,7 @@ describe("force:source:retrieve", () => { usernameOrConnection: username, merge: true, output: defaultPackagePath, - packageOptions: undefined + packageOptions: undefined, }; const expectedRetrieveArgs = { ...defaultRetrieveArgs, ...overrides }; @@ -152,113 +153,111 @@ describe("force:source:retrieve", () => { // Ensure Lifecycle hooks are called properly const ensureHookArgs = () => { - const failureMsg = "Lifecycle.emit() should be called for preretrieve and postretrieve"; + const failureMsg = 'Lifecycle.emit() should be called for preretrieve and postretrieve'; expect(lifecycleEmitStub.calledTwice, failureMsg).to.equal(true); - expect(lifecycleEmitStub.firstCall.args[0]).to.equal("preretrieve"); + expect(lifecycleEmitStub.firstCall.args[0]).to.equal('preretrieve'); expect(lifecycleEmitStub.firstCall.args[1]).to.deep.equal([exampleSourceComponent]); - expect(lifecycleEmitStub.secondCall.args[0]).to.equal("postretrieve"); + expect(lifecycleEmitStub.secondCall.args[0]).to.equal('postretrieve'); expect(lifecycleEmitStub.secondCall.args[1]).to.deep.equal(expectedResults.inboundFiles); }; - it("should pass along sourcepath", async () => { - const sourcepath = ["somepath"]; - const result = await runRetrieveCmd(["--sourcepath", sourcepath[0], "--json"]); + it('should pass along sourcepath', async () => { + const sourcepath = ['somepath']; + const result = await runRetrieveCmd(['--sourcepath', sourcepath[0], '--json']); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ sourcepath }); ensureRetrieveArgs(); ensureHookArgs(); }); - it("should pass along retrievetargetdir", async () => { - const sourcepath = ["somepath"]; - const metadata = ["ApexClass:MyClass"]; - const result = await runRetrieveCmd(["--retrievetargetdir", sourcepath[0], "--metadata", metadata[0], "--json"]); + it('should pass along retrievetargetdir', async () => { + const sourcepath = ['somepath']; + const metadata = ['ApexClass:MyClass']; + const result = await runRetrieveCmd(['--retrievetargetdir', sourcepath[0], '--metadata', metadata[0], '--json']); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ sourcepath: undefined, metadata: { directoryPaths: [], - metadataEntries: [ - "ApexClass:MyClass" - ] - } + metadataEntries: ['ApexClass:MyClass'], + }, }); - ensureRetrieveArgs({output: sourcepath[0]}); + ensureRetrieveArgs({ output: sourcepath[0] }); ensureHookArgs(); }); - it("should pass along metadata", async () => { - const metadata = ["ApexClass:MyClass"]; - const result = await runRetrieveCmd(["--metadata", metadata[0], "--json"]); + it('should pass along metadata', async () => { + const metadata = ['ApexClass:MyClass']; + const result = await runRetrieveCmd(['--metadata', metadata[0], '--json']); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ metadata: { metadataEntries: metadata, - directoryPaths: [defaultPackagePath] - } + directoryPaths: [defaultPackagePath], + }, }); ensureRetrieveArgs(); ensureHookArgs(); }); - it("should pass along manifest", async () => { - const manifest = "package.xml"; - const result = await runRetrieveCmd(["--manifest", manifest, "--json"]); + it('should pass along manifest', async () => { + const manifest = 'package.xml'; + const result = await runRetrieveCmd(['--manifest', manifest, '--json']); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath] - } + directoryPaths: [defaultPackagePath], + }, }); ensureRetrieveArgs(); ensureHookArgs(); }); - it("should pass along apiversion", async () => { - const manifest = "package.xml"; - const apiversion = "50.0"; - const result = await runRetrieveCmd(["--manifest", manifest, "--apiversion", apiversion, "--json"]); + it('should pass along apiversion', async () => { + const manifest = 'package.xml'; + const apiversion = '50.0'; + const result = await runRetrieveCmd(['--manifest', manifest, '--apiversion', apiversion, '--json']); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ apiversion, manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath] - } + directoryPaths: [defaultPackagePath], + }, }); ensureRetrieveArgs(); ensureHookArgs(); }); - it("should pass along sourceapiversion", async () => { - const sourceApiVersion = "50.0"; + it('should pass along sourceapiversion', async () => { + const sourceApiVersion = '50.0'; resolveProjectConfigStub.resolves({ sourceApiVersion }); - const manifest = "package.xml"; - const result = await runRetrieveCmd(["--manifest", manifest, "--json"]); + const manifest = 'package.xml'; + const result = await runRetrieveCmd(['--manifest', manifest, '--json']); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ sourceapiversion: sourceApiVersion, manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath] - } + directoryPaths: [defaultPackagePath], + }, }); ensureRetrieveArgs(); ensureHookArgs(); }); - it("should pass along packagenames", async () => { - const manifest = "package.xml"; - const packagenames = ["package1"]; - const result = await runRetrieveCmd(["--manifest", manifest, "--packagenames", packagenames[0], "--json"]); - expectedResults.packages.push({ name: packagenames[0], path: join("path", "to", "package", packagenames[0]) }); + it('should pass along packagenames', async () => { + const manifest = 'package.xml'; + const packagenames = ['package1']; + const result = await runRetrieveCmd(['--manifest', manifest, '--packagenames', packagenames[0], '--json']); + expectedResults.packages.push({ name: packagenames[0], path: join('path', 'to', 'package', packagenames[0]) }); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ packagenames, manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath] - } + directoryPaths: [defaultPackagePath], + }, }); ensureRetrieveArgs({ packageOptions: packagenames }); ensureHookArgs(); @@ -266,20 +265,20 @@ describe("force:source:retrieve", () => { expectedResults.packages = []; }); - it("should pass along multiple packagenames", async () => { - const manifest = "package.xml"; - const packagenames = ["package1", "package2"]; - const result = await runRetrieveCmd(["--manifest", manifest, "--packagenames", packagenames.join(","), "--json"]); + it('should pass along multiple packagenames', async () => { + const manifest = 'package.xml'; + const packagenames = ['package1', 'package2']; + const result = await runRetrieveCmd(['--manifest', manifest, '--packagenames', packagenames.join(','), '--json']); packagenames.forEach((pkg) => { - expectedResults.packages.push({ name: pkg, path: join("path", "to", "package", pkg) }); + expectedResults.packages.push({ name: pkg, path: join('path', 'to', 'package', pkg) }); }); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ packagenames, manifest: { manifestPath: manifest, - directoryPaths: [defaultPackagePath] - } + directoryPaths: [defaultPackagePath], + }, }); ensureRetrieveArgs({ packageOptions: packagenames }); ensureHookArgs(); @@ -287,141 +286,141 @@ describe("force:source:retrieve", () => { expectedResults.packages = []; }); - it("should display output with no --json", async () => { - const displayStub = sandbox.stub(RetrieveResultFormatter.prototype, "display"); - const getJsonStub = sandbox.stub(RetrieveResultFormatter.prototype, "getJson"); - await runRetrieveCmd(["--sourcepath", "somepath"]); + it('should display output with no --json', async () => { + const displayStub = sandbox.stub(RetrieveResultFormatter.prototype, 'display'); + const getJsonStub = sandbox.stub(RetrieveResultFormatter.prototype, 'getJson'); + await runRetrieveCmd(['--sourcepath', 'somepath']); expect(displayStub.calledOnce).to.equal(true); expect(getJsonStub.calledOnce).to.equal(true); }); - it("should NOT display output with --json", async () => { - const displayStub = sandbox.stub(RetrieveResultFormatter.prototype, "display"); - const getJsonStub = sandbox.stub(RetrieveResultFormatter.prototype, "getJson"); - await runRetrieveCmd(["--sourcepath", "somepath", "--json"]); + it('should NOT display output with --json', async () => { + const displayStub = sandbox.stub(RetrieveResultFormatter.prototype, 'display'); + const getJsonStub = sandbox.stub(RetrieveResultFormatter.prototype, 'getJson'); + await runRetrieveCmd(['--sourcepath', 'somepath', '--json']); expect(displayStub.calledOnce).to.equal(false); expect(getJsonStub.calledOnce).to.equal(true); }); - it("should warn users when retrieving CustomField with --metadata", async () => { - const metadata = "CustomField"; + it('should warn users when retrieving CustomField with --metadata', async () => { + const metadata = 'CustomField'; buildComponentSetStub.restore(); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], add: (component: ComponentLike) => { expect(component) - .to.be.a("object") - .and.to.have.property("type") - .and.to.deep.equal({ id: "customobject", name: "CustomObject" }); - expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); + .to.be.a('object') + .and.to.have.property('type') + .and.to.deep.equal({ id: 'customobject', name: 'CustomObject' }); + expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); }, has: (component: ComponentLike) => { - expect(component).to.be.a("object").and.to.have.property("type"); - expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); + expect(component).to.be.a('object').and.to.have.property('type'); + expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); const type = component.type as MetadataType; - if (type.name === "CustomField") { + if (type.name === 'CustomField') { return true; } - if (type.name === "CustomObject") { + if (type.name === 'CustomObject') { return false; } - } + }, }); - await runRetrieveCmd(["--metadata", metadata]); + await runRetrieveCmd(['--metadata', metadata]); expect(warnStub.calledOnce); - expect(warnStub.firstCall.firstArg).to.equal(messages.getMessage("wantsToRetrieveCustomFields")); + expect(warnStub.firstCall.firstArg).to.equal(messages.getMessage('wantsToRetrieveCustomFields')); }); - it("should not warn users when retrieving CustomField,CustomObject with --metadata", async () => { - const metadata = "CustomField,CustomObject"; + it('should not warn users when retrieving CustomField,CustomObject with --metadata', async () => { + const metadata = 'CustomField,CustomObject'; buildComponentSetStub.restore(); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], add: (component: ComponentLike) => { expect(component) - .to.be.a("object") - .and.to.have.property("type") - .and.to.deep.equal({ id: "customobject", name: "CustomObject" }); - expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); + .to.be.a('object') + .and.to.have.property('type') + .and.to.deep.equal({ id: 'customobject', name: 'CustomObject' }); + expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); }, has: (component: ComponentLike) => { - expect(component).to.be.a("object").and.to.have.property("type"); - expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); + expect(component).to.be.a('object').and.to.have.property('type'); + expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); const type = component.type as MetadataType; - if (type.name === "CustomField") { + if (type.name === 'CustomField') { return true; } - if (type.name === "CustomObject") { + if (type.name === 'CustomObject') { return true; } - } + }, }); - await runRetrieveCmd(["--metadata", metadata]); + await runRetrieveCmd(['--metadata', metadata]); expect(warnStub.callCount).to.be.equal(0); }); - it("should warn users when retrieving CustomField with --manifest", async () => { - const manifest = "package.xml"; + it('should warn users when retrieving CustomField with --manifest', async () => { + const manifest = 'package.xml'; buildComponentSetStub.restore(); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], add: (component: ComponentLike) => { expect(component) - .to.be.a("object") - .and.to.have.property("type") - .and.to.deep.equal({ id: "customobject", name: "CustomObject" }); - expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); + .to.be.a('object') + .and.to.have.property('type') + .and.to.deep.equal({ id: 'customobject', name: 'CustomObject' }); + expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); }, has: (component: ComponentLike) => { - expect(component).to.be.a("object").and.to.have.property("type"); - expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); + expect(component).to.be.a('object').and.to.have.property('type'); + expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); const type = component.type as MetadataType; - if (type.name === "CustomField") { + if (type.name === 'CustomField') { return true; } - if (type.name === "CustomObject") { + if (type.name === 'CustomObject') { return false; } - } + }, }); - await runRetrieveCmd(["--manifest", manifest]); + await runRetrieveCmd(['--manifest', manifest]); expect(warnStub.calledOnce); - expect(warnStub.firstCall.firstArg).to.equal(messages.getMessage("wantsToRetrieveCustomFields")); + expect(warnStub.firstCall.firstArg).to.equal(messages.getMessage('wantsToRetrieveCustomFields')); }); - it("should not be warn users when retrieving CustomField,CustomObject with --manifest", async () => { - const manifest = "package.xml"; + it('should not be warn users when retrieving CustomField,CustomObject with --manifest', async () => { + const manifest = 'package.xml'; buildComponentSetStub.restore(); - buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, "build").resolves({ + buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, toArray: () => [exampleSourceComponent], add: (component: ComponentLike) => { expect(component) - .to.be.a("object") - .and.to.have.property("type") - .and.to.deep.equal({ id: "customobject", name: "CustomObject" }); - expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); + .to.be.a('object') + .and.to.have.property('type') + .and.to.deep.equal({ id: 'customobject', name: 'CustomObject' }); + expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); }, has: (component: ComponentLike) => { - expect(component).to.be.a("object").and.to.have.property("type"); - expect(component).and.to.have.property("fullName").and.to.be.equal(ComponentSet.WILDCARD); + expect(component).to.be.a('object').and.to.have.property('type'); + expect(component).and.to.have.property('fullName').and.to.be.equal(ComponentSet.WILDCARD); const type = component.type as MetadataType; - if (type.name === "CustomField") { + if (type.name === 'CustomField') { return true; } - if (type.name === "CustomObject") { + if (type.name === 'CustomObject') { return true; } - } + }, }); - await runRetrieveCmd(["--manifest", manifest]); + await runRetrieveCmd(['--manifest', manifest]); expect(warnStub.callCount).to.be.equal(0); }); }); From 8bc15964eaa389e33372a223b171c894b5ea3973 Mon Sep 17 00:00:00 2001 From: peternhale Date: Fri, 28 Oct 2022 13:56:14 -0600 Subject: [PATCH 8/9] Apply suggestions from code review Co-authored-by: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> --- messages/retrieve.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/messages/retrieve.json b/messages/retrieve.json index 7e0df9933..4f79a0f68 100644 --- a/messages/retrieve.json +++ b/messages/retrieve.json @@ -28,8 +28,8 @@ "flagsLong": { "retrievetargetdir": [ "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 will fail.", - "Running the command multiple times with the same target will add new files and overwrite existing files." + "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." ], "wait": "Number of minutes to wait for the command to complete and display results to the terminal window. If the command continues to run after the wait period, the CLI returns control of the terminal window to you.", "manifest": [ @@ -60,5 +60,5 @@ "nothingToRetrieve": "Specify a source path, manifest, metadata, or package names to retrieve.", "wantsToRetrieveCustomFields": "Because you're retrieving one or more CustomFields, we're also retrieving the CustomObject to which it's associated.", "retrieveWontDelete": "You currently have files deleted in your org. The retrieve command will NOT delete them from your local project", - "retrieveTargetDirOverlapsPackage": "The retrieve target directory [%s] overlaps one of your package directories. Please specify a different retrieve target directory." + "retrieveTargetDirOverlapsPackage": "The retrieve target directory [%s] overlaps one of your package directories. Specify a different retrieve target directory and try again." } From 461b9e28cc1504f17fb1e56da6edf39b8b33c192 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 1 Nov 2022 06:46:41 -0600 Subject: [PATCH 9/9] chore: address qa edge cases --- src/commands/force/source/retrieve.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index 843b9eb3a..6fd799e18 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -288,11 +288,6 @@ export class Retrieve extends SourceCommand { } private overlapsPackage(): boolean { - return !!this.project.getPackageDirectories().find((pkgDir) => { - if (pkgDir.fullPath) { - return pkgDir.fullPath.includes(this.resolvedTargetDir); - } - return false; - }); + return !!this.project.getPackageNameFromPath(this.resolvedTargetDir); } }