Skip to content

Commit

Permalink
feat: source tracking beta commands
Browse files Browse the repository at this point in the history
* refactor: redo open for sfdx-core dependency reasons

* test: ebikes for nuts

* feat: source tracking commands in source:beta

* test: nuts for source tracking commands

* docs: new subtopics

* chore: remove imported asserted project/org

* chore: restore breaking comma

* chore: bump source tracking version

* chore: rebuild with latest lockfile

* refactor: feedback from WR

* style: typo and cleaner concats

* chore: pr feedback

* refactor: pull more like retrieve

* refactor: status formatter

* test: remove unused export

* chore: bump stl

* fix: missing header for pull

* fix: show conflicts from both remote and local, including remote filename

* test: better output, workaroun for Audience type

* refactor: consistent naming of command classes

* refactor: push feedback

* style: comment is no longer a question

* test: ebikes from clone instead of fs

* fix: status flags are better

* test: nuts with conn adjusted for git checkout

* chore: bump sdr for Created state fix

* test: unignore profiles

* test: was not using ebikes, revert

* chore: change status' flags in snapshot

* feat: show conflicts from both remote and local, including remote filename

* fix: show conflicts from both remote and local, including remote filename

* test: better output, workaroun for Audience type

* fix: push gets its own formatter for json reasons

* fix: missing pull messages

* chore: bump SDR to 4.5.7 (#253)

* chore(release): 1.2.3 [ci skip]

* Updated/Added CODEOWNERS with ECCN

* chore(release): 1.2.4 [ci skip]

* chore: sync .gitignore [skip-validate-pr] (#251)

Authored via Leif

* test: parallelize nuts, exclude async deploy nut

* test: one NUT for special types

* test: remove tracking NUTs

* ci: custom nut commands

* test: fix folderTypes, cross-env for windows

* test:  cross-env for windows SEED_FILTER, nyc only top-level

* ci: no sha; change nut order

* test: deploy nuts in parallel

* ci: disable async nuts

* test: exclude async nuts

* test: revert async-exclude

* test: revert tsconfig, revert config.yml

* test: exclude env for seeds

* feat: mark ignores

* feat: status shows remote ignores

* refactor: consistent naming of command classes

* refactor: push feedback

* style: comment is no longer a question

* test: ebikes from clone instead of fs

* fix: status flags are better

* test: nuts with conn adjusted for git checkout

* chore: bump sdr for Created state fix

* test: unignore profiles

* test: was not using ebikes, revert

* chore: change status' flags in snapshot

* refactor: move status logic to STL

* test: update ignore nut

* style: formatting line length

* fix: push uses correct json

* test: update tests for new push json format

* test: ut on lts, not node17

* Wr/destructive deploy (#230)

* fix: implementing destructive change deploy, 1 NUT

* chore: add NUTs, fix UTs

* chore: post-review board updates

* chore: fix linting

* chore: minor updates to flag logic, redo NUT query method

* chore: remove unnecessary checks in NUTs

* chore: bump SDR to 5, include deploy:destructive in NUTs

* chore(release): 1.2.5 [ci skip]

* fix: use cross-env for test:nuts script (#260)

* chore(release): 1.2.6 [ci skip]

* chore: unhide beta commands

* chore: bump SDR and STL

* test: and tracking nuts

* ci: windows UT

* test: nuts are hub-auth-agnostic

* ci: windows only

* ci: restore windows tests

Co-authored-by: Willie Ruemmele <[email protected]>
Co-authored-by: SF-CLI-BOT <[email protected]>
Co-authored-by: svc-scm <[email protected]>
Co-authored-by: Steve Hetzel <[email protected]>

* Sm/quiet-flag (#259)

* fix: push gets its own formatter for json reasons

* fix: missing pull messages

* feat: mark ignores

* feat: status shows remote ignores

* refactor: move status logic to STL

* test: update ignore nut

* style: formatting line length

* fix: push uses correct json

* test: update tests for new push json format

* test: ut on lts, not node17

* test: include tracking nuts

* feat: quiet flag on push (for stdout and json)

* test: put the individual tracking nuts back

* feat: quiet shows only failures in json

* refactor: simplify quiet logic

* test: ut for formatter with quiet

* feat: show ignored if any

* refactor: ignored as first column

* fix: no spinner for conflict check on --overwrite

* fix: early exit when there are no results

* fix: handle no-result (deletes only, or no changes)

* chore: bump sdr and stl

Co-authored-by: Willie Ruemmele <[email protected]>
Co-authored-by: SF-CLI-BOT <[email protected]>
Co-authored-by: svc-scm <[email protected]>
Co-authored-by: Steve Hetzel <[email protected]>
  • Loading branch information
5 people authored Oct 28, 2021
1 parent 84396b7 commit b871774
Show file tree
Hide file tree
Showing 35 changed files with 2,226 additions and 149 deletions.
5 changes: 2 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,10 @@ workflows:
- linux
- windows
node_version:
- latest
# - latest
- lts
- maintenance
exclude:
- os: windows
node_version: lts
- os: windows
node_version: maintenance
- release-management/test-nut:
Expand All @@ -79,6 +77,7 @@ workflows:
- 'yarn test:nuts:retrieve'
- 'yarn test:nuts:specialTypes'
- 'yarn test:nuts:deploy:destructive'
- 'yarn test:nuts:tracking'
- release-management/release-package:
sign: true
github-release: true
Expand Down
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
*/
module.exports = {
extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license'],
ignorePatterns: ['test/nuts/ebikes-lwc/**'],
};
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ node_modules
# os specific files
.DS_Store
.idea

# ignore generated nut tests
test/nuts/generated/
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ All notable changes to this project will be documented in this file. See [standa

### [1.2.6](https://github.com/salesforcecli/plugin-source/compare/v1.2.5...v1.2.6) (2021-10-21)


### Bug Fixes

* use cross-env for test:nuts script ([#260](https://github.com/salesforcecli/plugin-source/issues/260)) ([76627fb](https://github.com/salesforcecli/plugin-source/commit/76627fb21c62a4fb140c87ccfc266accd79af3fd))
- use cross-env for test:nuts script ([#260](https://github.com/salesforcecli/plugin-source/issues/260)) ([76627fb](https://github.com/salesforcecli/plugin-source/commit/76627fb21c62a4fb140c87ccfc266accd79af3fd))

### [1.2.5](https://github.com/salesforcecli/plugin-source/compare/v1.2.4...v1.2.5) (2021-10-21)

Expand Down
106 changes: 55 additions & 51 deletions README.md

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
[
{
"command": "force:source:beta:pull",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "forceoverwrite", "json", "loglevel", "targetusername", "wait"]
},
{
"command": "force:source:beta:push",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "forceoverwrite", "ignorewarnings", "json", "loglevel", "quiet", "targetusername", "wait"]
},
{
"command": "force:source:beta:status",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "json", "local", "loglevel", "remote", "targetusername"]
},
{
"command": "force:source:beta:tracking:clear",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "json", "loglevel", "noprompt", "targetusername"]
},
{
"command": "force:source:beta:tracking:reset",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "json", "loglevel", "noprompt", "revision", "targetusername"]
},
{
"command": "force:source:convert",
"plugin": "@salesforce/plugin-source",
Expand Down
17 changes: 17 additions & 0 deletions messages/pull.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"description": "pull source from the scratch org to the project",
"descriptionLong": "Pulls changed source from the scratch org to your project to keep them in sync.",
"help": "If the command detects a conflict, it displays the conflicts but does not complete the process. After reviewing the conflict, rerun the command with the --forceoverwrite parameter.",
"flags": {
"forceoverwrite": "ignore conflict warnings and overwrite changes to the project",
"forceoverwriteLong": "Runs the pull command even if conflicts exist. Changes in the scratch org overwrite changes in the project.",
"waitLong": "The 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. The default is 33 minutes."
},
"NonScratchOrgPull": "We can\"t retrieve your changes. \"force:source:pull\" is only available for orgs that have source tracking enabled. Use \"force:source:retrieve\" or \"force:mdapi:retrieve\" instead.",
"sourceConflictDetected": "Source conflict(s) detected.",
"pull": "Your retrieve request did not complete within the specified wait time [%s minutes]. Try again with a longer wait time.",
"retrievedSourceHeader": "Retrieved Source",
"NoResultsFound": "No results found",
"retrievedSourceWarningsHeader": "Retrieved Source Warnings",
"retrieveTimeout": "Your retrieve request did not complete within the specified wait time [%s minutes]. Try again with a longer wait time."
}
17 changes: 17 additions & 0 deletions messages/push.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"description": "push source to a scratch org from the project",
"descriptionLong": "Pushes changed source from your project to a scratch org to keep them in sync.",
"help": "If the command detects a conflict, it displays the conflicts but does not complete the process. After reviewing the conflict, rerun the command with the --forceoverwrite parameter.",
"flags": {
"waitLong": "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. The default is 33 minutes.",
"forceoverwrite": "ignore conflict warnings and overwrite changes to scratch org",
"forceoverwriteLong": "Runs the push command even if conflicts exist. Changes in the project overwrite changes in the scratch org.",
"replacetokens": "replace tokens in source files prior to deployment",
"replacetokensLong": "Replaces tokens in source files prior to deployment.",
"ignorewarnings": "deploy changes even if warnings are generated",
"ignorewarningsLong": "Completes the deployment even if warnings are generated.",
"quiet": "minimize json and sdtout output on success"
},
"sourcepushFailed": "Push failed.",
"conflictMsg": "We couldn't complete the push operation due to conflicts. Verify that you want to keep the local versions, then run \"sfdx force:source:push -f\" with the --forceoverwrite (-f) option."
}
19 changes: 19 additions & 0 deletions messages/status.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"description": "list local changes and/or changes in a scratch org",
"LongDescription": "Lists changes that have been made locally, in a scratch org, or both.",
"examples": [
"sfdx force:source:status -l",
"sfdx force:source:status -r",
"sfdx force:source:status -a",
"sfdx force:source:status -a -u [email protected] --json"
],
"flags": {
"all": "list all the changes that have been made",
"allLong": "Lists all the changes that have been made.",
"local": "list the changes that have been made locally",
"localLong": "Lists the changes that have been made locally.",
"remote": "list the changes that have been made in the scratch org",
"remoteLong": "Lists the changes that have been made in the scratch org."
},
"humanSuccess": "Source Status"
}
7 changes: 7 additions & 0 deletions messages/tracking.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"resetDescription": "reset local and remote source tracking\n\n WARNING: This command deletes or overwrites all existing source tracking files. Use with extreme caution. \n\nResets local and remote source tracking so that the CLI no longer registers differences between your local files and those in the org. When you next run force:source:status, the CLI returns no results, even though conflicts might actually exist. The CLI then resumes tracking new source changes as usual.\n\nUse the --revision parameter to reset source tracking to a specific revision number of an org source member. To get the revision number, query the SourceMember Tooling API object with the force:data:soql:query command. For example:\n $ sfdx force:data:soql:query -q \"SELECT MemberName, MemberType, RevisionCounter FROM SourceMember\" -t",
"clearDescription": "clear all local source tracking information\n\nWARNING: This command deletes or overwrites all existing source tracking files. Use with extreme caution.\n\nClears all local source tracking information. When you next run force:source:status, the CLI displays all local and remote files as changed, and any files with the same name are listed as conflicts.",
"nopromptDescription": "do not prompt for source tracking override confirmation",
"revisionDescription": "reset to a specific SourceMember revision counter number",
"promptMessage": "WARNING: This operation will modify all your local source tracking files. The operation can have unintended consequences on all the force:source commands. Are you sure you want to proceed (y/n)?"
}
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"@oclif/config": "^1",
"@salesforce/command": "^4.1.3",
"@salesforce/core": "^2.28.0",
"@salesforce/source-deploy-retrieve": "^5.0.0",
"@salesforce/source-deploy-retrieve": "^5.1.0",
"@salesforce/source-tracking": "^0.4.1",
"chalk": "^4.1.2",
"cli-ux": "^5.6.3",
"open": "^8.2.1",
Expand Down Expand Up @@ -142,6 +143,7 @@
"test:nuts:delete": "mocha \"test/nuts/delete.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
"test:nuts:deploy": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy PLUGIN_SOURCE_SEED_EXCLUDE=async ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
"test:nuts:deploy:async": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy.async ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
"test:nuts:deploy:destructive": "mocha \"test/nuts/deployDestructive.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0",
"test:nuts:deploy:manifest": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy.manifest ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
"test:nuts:deploy:metadata": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy.metadata ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
"test:nuts:deploy:quick": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy.quick ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
Expand All @@ -152,7 +154,12 @@
"test:nuts:retrieve": "cross-env PLUGIN_SOURCE_SEED_FILTER=retrieve ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
"test:nuts:specialTypes": "mocha \"test/nuts/territory2.nut.ts\" \"test/nuts/folderTypes.nut.ts\" --slow 4500 --timeout 600000 --retries 0 --parallel",
"test:nuts:territory2": "mocha \"test/nuts/territory2.nut.ts\" --slow 4500 --timeout 600000 --retries 0",
"test:nuts:deploy:destructive": "mocha \"test/nuts/deployDestructive.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0",
"test:nuts:tracking": "mocha \"test/nuts/trackingCommands/*.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0",
"test:nuts:tracking:basics": "mocha \"test/nuts/trackingCommands/basics.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
"test:nuts:tracking:conflicts": "mocha \"test/nuts/trackingCommands/conflicts.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
"test:nuts:tracking:forceignore": "mocha \"test/nuts/trackingCommands/forceIgnore.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
"test:nuts:tracking:remote": "mocha \"test/nuts/trackingCommands/remoteChanges.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
"test:nuts:tracking:resetClear": "mocha \"test/nuts/trackingCommands/resetClear.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
"version": "oclif-dev readme"
},
"husky": {
Expand Down
185 changes: 185 additions & 0 deletions src/commands/force/source/beta/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* 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 { FlagsConfig, flags } from '@salesforce/command';
import { Duration } from '@salesforce/kit';
import { Messages } from '@salesforce/core';
import {
FileResponse,
SourceComponent,
ComponentSet,
RetrieveResult,
RequestStatus,
ComponentStatus,
} from '@salesforce/source-deploy-retrieve';
import { SourceTracking, throwIfInvalid, replaceRenamedCommands, ChangeResult } from '@salesforce/source-tracking';
import { processConflicts } from '../../../../formatters/conflicts';
import { SourceCommand } from '../../../../sourceCommand';
import { PullResponse, PullResultFormatter } from '../../../../formatters/pullFormatter';
Messages.importMessagesDirectory(__dirname);
const messages: Messages = Messages.loadMessages('@salesforce/plugin-source', 'pull');

export default class Pull extends SourceCommand {
public static description = messages.getMessage('description');
public static help = messages.getMessage('help');
protected static readonly flagsConfig: FlagsConfig = {
forceoverwrite: flags.boolean({
char: 'f',
description: messages.getMessage('flags.forceoverwrite'),
}),
// TODO: use shared flags from plugin-source
wait: flags.minutes({
char: 'w',
default: Duration.minutes(33),
min: Duration.minutes(0), // wait=0 means deploy is asynchronous
description: messages.getMessage('flags.waitLong'),
}),
};

protected static requiresUsername = true;
protected static requiresProject = true;
protected readonly lifecycleEventNames = ['preretrieve', 'postretrieve'];
protected tracking: SourceTracking;
protected retrieveResult: RetrieveResult;
protected deleteFileResponses: FileResponse[];

public async run(): Promise<PullResponse[]> {
await this.preChecks();
await this.retrieve();
// do not parallelize delete and retrieve...we only get to delete IF retrieve was successful
await this.doDeletes(); // deletes includes its tracking file operations
await this.updateTrackingFilesWithRetrieve();
this.ux.stopSpinner();

return this.formatResult();
}

protected async preChecks(): Promise<void> {
// checks the source tracking file version and throws if they're toolbelt's old version
throwIfInvalid({
org: this.org,
projectPath: this.project.getPath(),
toValidate: 'plugin-source',
command: replaceRenamedCommands('force:source:pull'),
});

this.ux.startSpinner('Loading source tracking information');
this.tracking = await SourceTracking.create({
org: this.org,
project: this.project,
});

await this.tracking.ensureRemoteTracking(true);

if (!this.flags.forceoverwrite) {
this.ux.setSpinnerStatus('Checking for conflicts');
processConflicts(await this.tracking.getConflicts(), this.ux, messages.getMessage('sourceConflictDetected'));
}
}

protected async doDeletes(): Promise<void> {
this.ux.setSpinnerStatus('Checking for deletes from the org and updating source tracking files');
const changesToDelete = await this.tracking.getChanges<SourceComponent>({
origin: 'remote',
state: 'delete',
format: 'SourceComponent',
});
this.deleteFileResponses = await this.tracking.deleteFilesAndUpdateTracking(changesToDelete);
}

protected async updateTrackingFilesWithRetrieve(): Promise<void> {
this.ux.setSpinnerStatus('Updating source tracking files');

// might not exist if we exited from retrieve early
if (!this.retrieveResult) {
return;
}
const successes = this.retrieveResult
.getFileResponses()
.filter((fileResponse) => fileResponse.state !== ComponentStatus.Failed);

await Promise.all([
// commit the local file successes that the retrieve modified
this.tracking.updateLocalTracking({
files: successes.map((fileResponse) => fileResponse.filePath).filter(Boolean),
}),
this.tracking.updateRemoteTracking(
successes.map(({ state, fullName, type, filePath }) => ({ state, fullName, type, filePath })),
true // skip polling because it's a pull
),
]);
}

protected async retrieve(): Promise<void> {
const componentSet = new ComponentSet();
(
await this.tracking.getChanges<ChangeResult>({
origin: 'remote',
state: 'nondelete',
format: 'ChangeResult',
})
).map((component) => {
if (component.type && component.name) {
componentSet.add({
type: component.type,
fullName: component.name,
});
}
});

if (componentSet.size === 0) {
return;
}
componentSet.sourceApiVersion = await this.getSourceApiVersion();
if (this.getFlag<string>('apiversion')) {
componentSet.apiVersion = this.getFlag<string>('apiversion');
}

const mdapiRetrieve = await componentSet.retrieve({
usernameOrConnection: this.org.getUsername(),
merge: true,
output: this.project.getDefaultPackage().path,
});

this.ux.setSpinnerStatus('Retrieving metadata from the org');

// assume: remote deletes that get deleted locally don't fire hooks?
await this.lifecycle.emit('preretrieve', componentSet.toArray());
this.retrieveResult = await mdapiRetrieve.pollStatus(1000, this.getFlag<Duration>('wait').seconds);

// Assume: remote deletes that get deleted locally don't fire hooks.
await this.lifecycle.emit('postretrieve', this.retrieveResult.getFileResponses());
}

protected resolveSuccess(): void {
// there might not be a retrieveResult if we don't have anything to retrieve
if (this.retrieveResult && this.retrieveResult.response.status !== RequestStatus.Succeeded) {
this.setExitCode(1);
}
}

protected formatResult(): PullResponse[] {
const formatterOptions = {
verbose: this.getFlag<boolean>('verbose', false),
};

const formatter = new PullResultFormatter(
this.logger,
this.ux,
formatterOptions,
this.retrieveResult,
this.deleteFileResponses
);

// Only display results to console when JSON flag is unset.
if (!this.isJsonOutput()) {
formatter.display();
}

return formatter.getJson();
}
}
Loading

0 comments on commit b871774

Please sign in to comment.