From 4a934b0d4e632469825ef4431b01947924d98480 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 2 Apr 2021 14:36:06 +0200 Subject: [PATCH] Add --ignore-destructive parameter (#112) Co-authored-by: Mehdi Cherfaoui <> --- README.md | 27 ++++- __tests__/unit/lib/utils/repoGitDiff.test.js | 66 +++++++++++ bin/cli | 4 + messages/delta.js | 1 + src/commands/sgd/source/delta.ts | 115 ++++++++++++------- src/utils/repoGitDiff.js | 45 +++++--- 6 files changed, 199 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 83acf520..87b7b257 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ To view the full list and description of the sgd options, run `sfdx sgd:source:d -o, --output [dir] source package specific output [./output] (default: "./output") -a, --api-version [version] salesforce API version [50] (default: "50") -i, --ignore specify the ignore file (default: ".forceignore") +-D, --ignore-destructive specify the ignore file (default: ".forceignore") -r, --repo [dir] git repository location [.] (default: ".") -d, --generate-delta generate delta files in [./output] folder -h, --help output usage information @@ -205,7 +206,9 @@ Comparing changes performed in the `develop` branch since its common ancestor wi sfdx sgd:source:delta --to develop --from $(git merge-base develop master) --output . ``` -### Advanced use-case: Generating a folder containing only the added/modified sources +### Advanced use-cases: + +#### Generating a folder containing only the added/modified sources: Using a package.xml file to deploy a subset of the metadata is propably the simpliest approach to delta deployments. But there are some situations where you may want to have the actual source files related to all the components that have been changed recently. @@ -225,6 +228,28 @@ In addition to the `package` and `destructiveChanges` folders, the `sfdx sgd:sou _Content of the output folder when using the --generate-delta option, with the same scenario as above:_ ![delta-source](/img/example_generateDelta.png) +#### Excluding some metadata only from destructiveChanges.xml: + +The `--ignore [-i]` parameter allows you to specify an [ignore file](https://git-scm.com/docs/gitignore) used to filter the +element on the diff to ignore. Every diff line matching the pattern from the ignore file specified in the `--ignore [-i]` will be ignored by SGD, +and will not be used to add member in `package.xml` nor `destructiveChanges.xml` (and will also be ignored when using the `--delta-generate` parameter). + +But, sometimes you may need to have two different ignore policies for generating the `package.xml` and `destructiveChanges.xml` files. This is where the `--ignore-destructive [-D]` option comes handy! + +Use the `--ignore-destructive` parameter to specify a dedicated ignore file to handle deletions (resulting in metadata listed in the `destructiveChanges.xml` output). In orther words, this will override the `--ignore [-i]` parameter for deleted items. + +For example, consider a repository containing multiple sub-folders (force-app/main,force-app/sample, etc) and a commit deleting the Custom\_\_c object from one folder and modifying the Custom\_\_c object from another folder. This event will be treated has a Modification and a Deletion. By default, the Custom\_\_c object would appear in the `package.xml` and in `destructiveChanges.xml`, which could be a little bit inconsistent and can break the CI/CD build. This is a situation where your may want to use the `--ignore-destructive [-D]` parameter! Add the Custom\_\_c object pattern in an ignore file and pass it in the CLI parameter: + +```sh +# destructiveignore +*Custom\_\_c.object-meta.xml + +$ sfdx sgd:source:delta --from commit --ignore-destructive destructiveignore + +``` + +Note that in a situatrion where only the `--ignore [-i]` parameter is specified (and `--ignore-destructive [-D]` is not specified), then the plugin will ignore items matching `--ignore [-i]` parameter in all situations: Addition, Modification and Deletion. + ## Javascript Module ```js diff --git a/__tests__/unit/lib/utils/repoGitDiff.test.js b/__tests__/unit/lib/utils/repoGitDiff.test.js index 14ad7479..66b63a2a 100644 --- a/__tests__/unit/lib/utils/repoGitDiff.test.js +++ b/__tests__/unit/lib/utils/repoGitDiff.test.js @@ -75,6 +75,72 @@ describe(`test if repoGitDiff`, () => { expect(work).toStrictEqual(expected) }) + test('can filter ignored destructive files', () => { + const output = ['D force-app/main/default/lwc/jsconfig.json'] + child_process.spawnSync.mockImplementation(() => ({ + stdout: output[0], + })) + const work = repoGitDiff( + { output: '', repo: '', ignoreDestructive: FORCEIGNORE_MOCK_PATH }, + // eslint-disable-next-line no-undef + globalMetadata + ) + //should be empty + const expected = [] + expect(work).toStrictEqual(expected) + }) + + test('can filter ignored and ignored destructive files', () => { + const output = [ + 'M force-app/main/default/lwc/jsconfig.json', + 'D force-app/main/default/lwc/jsconfig.json', + ] + child_process.spawnSync.mockImplementation(() => ({ + stdout: output[0], + })) + const work = repoGitDiff( + { + output: '', + repo: '', + ignore: FORCEIGNORE_MOCK_PATH, + ignoreDestructive: FORCEIGNORE_MOCK_PATH, + }, + // eslint-disable-next-line no-undef + globalMetadata + ) + //should be empty + const expected = [] + expect(work).toStrictEqual(expected) + }) + + test('can filter deletion if only ignored is specified files', () => { + const output = ['D force-app/main/default/lwc/jsconfig.json'] + child_process.spawnSync.mockImplementation(() => ({ + stdout: output[0], + })) + const work = repoGitDiff( + { output: '', repo: '', ignore: FORCEIGNORE_MOCK_PATH }, + // eslint-disable-next-line no-undef + globalMetadata + ) + //should be empty + const expected = [] + expect(work).toStrictEqual(expected) + }) + + test('cannot filter non deletion if only ignored destructive is specified files', () => { + const output = ['A force-app/main/default/lwc/jsconfig.json'] + child_process.spawnSync.mockImplementation(() => ({ + stdout: output[0], + })) + const work = repoGitDiff( + { output: '', repo: '', ignoreDestructive: FORCEIGNORE_MOCK_PATH }, + // eslint-disable-next-line no-undef + globalMetadata + ) + expect(work).toStrictEqual(output) + }) + test('can filter sub folders', () => { const output = ['M force-app/main/default/pages/Account.page'] child_process.spawnSync.mockImplementation(() => ({ diff --git a/bin/cli b/bin/cli index 516201f7..dbe748ad 100644 --- a/bin/cli +++ b/bin/cli @@ -23,6 +23,10 @@ program './output' ) .option('-i, --ignore [file]', 'ignore file to use [./.forceignore]') + .option( + '-D, --ignore-destructive [file]', + 'ignore file to use [./.forceignore]' + ) .option('-a, --api-version [version]', 'salesforce API version [50]', '50') .option('-r, --repo [dir]', 'git repository location [.]', '.') .option('-d, --generate-delta', 'generate delta files in [./output] folder') diff --git a/messages/delta.js b/messages/delta.js index 7cb6c6df..d95db13b 100644 --- a/messages/delta.js +++ b/messages/delta.js @@ -7,6 +7,7 @@ module.exports = { repoFlag: 'git repository location', outputFlag: 'source package specific output', ignoreFlag: 'ignore file to use', + ignoreDestructiveFlag: 'ignore file to use', apiVersionFlag: 'salesforce API version', deltaFlag: 'generate delta files in [--output] folder', } diff --git a/src/commands/sgd/source/delta.ts b/src/commands/sgd/source/delta.ts index 06cb128a..9dfa1b8e 100644 --- a/src/commands/sgd/source/delta.ts +++ b/src/commands/sgd/source/delta.ts @@ -1,55 +1,84 @@ -import { flags, SfdxCommand } from '@salesforce/command'; -import { Messages } from '@salesforce/core'; -import { AnyJson } from '@salesforce/ts-types'; -import * as sgd from '../../../main.js'; +import { flags, SfdxCommand } from '@salesforce/command' +import { Messages } from '@salesforce/core' +import { AnyJson } from '@salesforce/ts-types' +import * as sgd from '../../../main.js' // Initialize Messages with the current plugin directory -Messages.importMessagesDirectory(__dirname); -const COMMAND_NAME = 'delta'; +Messages.importMessagesDirectory(__dirname) +const COMMAND_NAME = 'delta' // Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core, // or any library that is using the messages framework can also be loaded this way. -const messages = Messages.loadMessages('sfdx-git-delta', COMMAND_NAME); +const messages = Messages.loadMessages('sfdx-git-delta', COMMAND_NAME) export default class SourceDeltaGenerate extends SfdxCommand { + public static description = messages.getMessage('command', []) - public static description = messages.getMessage('command', []); + protected static flagsConfig = { + to: flags.string({ + char: 't', + description: messages.getMessage('toFlag'), + default: 'HEAD', + }), + from: flags.string({ + char: 'f', + description: messages.getMessage('fromFlag'), + required: true, + }), + repo: flags.filepath({ + char: 'r', + description: messages.getMessage('repoFlag'), + default: '.', + }), + ignore: flags.filepath({ + char: 'i', + description: messages.getMessage('ignoreFlag'), + }), + 'ignore-destructive': flags.filepath({ + char: 'D', + description: messages.getMessage('ignoreDestructiveFlag'), + }), + output: flags.filepath({ + char: 'o', + description: messages.getMessage('outputFlag'), + default: './output', + }), + 'api-version': flags.number({ + char: 'a', + description: messages.getMessage('apiVersionFlag'), + default: 50.0, + }), + 'generate-delta': flags.boolean({ + char: 'd', + description: messages.getMessage('deltaFlag'), + }), + } - protected static flagsConfig = { - to: flags.string({ char: 't', description: messages.getMessage('toFlag'), default: 'HEAD' }), - from: flags.string({ char: 'f', description: messages.getMessage('fromFlag'), required: true }), - repo: flags.filepath({ char: 'r', description: messages.getMessage('repoFlag'), default: '.' }), - ignore: flags.filepath({ char: 'i', description: messages.getMessage('ignoreFlag')}), - output: flags.filepath({ char: 'o', description: messages.getMessage('outputFlag'), default: './output' }), - 'api-version': flags.number({ char: 'a', description: messages.getMessage('apiVersionFlag'), default: 50.0 }), - 'generate-delta': flags.boolean({ char: 'd', description: messages.getMessage('deltaFlag')}) - }; - - public async run(): Promise { - - const output = { - error: null, + public async run(): Promise { + const output = { + error: null, + output: this.flags.output, + success: true, + warnings: [], + } + try { + const jobResult = sgd({ + to: this.flags.to, + from: this.flags.from, output: this.flags.output, - success: true, - warnings: [] - }; - try { - const jobResult = sgd({ - to: this.flags.to, - from: this.flags.from, - output: this.flags.output, - ignore: this.flags.ignore, - apiVersion: this.flags['api-version'], - repo: this.flags.repo, - generateDelta: this.flags['generate-delta'] - }); - output.warnings = jobResult?.warnings?.map(warning => warning.message); - } catch (err) { - output.success = false; - output.error = err.message; - process.exitCode = 1; - } - this.ux.log(JSON.stringify(output, null, 2)); - return null; + ignore: this.flags.ignore, + ignoreDestructive: this.flags['ignore-destructive'], + apiVersion: this.flags['api-version'], + repo: this.flags.repo, + generateDelta: this.flags['generate-delta'], + }) + output.warnings = jobResult?.warnings?.map(warning => warning.message) + } catch (err) { + output.success = false + output.error = err.message + process.exitCode = 1 } + this.ux.log(JSON.stringify(output, null, 2)) + return null + } } diff --git a/src/utils/repoGitDiff.js b/src/utils/repoGitDiff.js index ce073928..14f51571 100644 --- a/src/utils/repoGitDiff.js +++ b/src/utils/repoGitDiff.js @@ -8,7 +8,6 @@ const os = require('os') const path = require('path') const fullDiffParams = ['--no-pager', 'diff', '--name-status', '--no-renames'] -const ig = ignore() module.exports = (config, metadata) => { const { stdout: diff } = childProcess.spawnSync( @@ -17,14 +16,10 @@ module.exports = (config, metadata) => { { cwd: config.repo, encoding: gc.UTF8_ENCODING } ) - if (config.ignore && fs.existsSync(config.ignore)) { - ig.add(fs.readFileSync(config.ignore).toString()) - } - - return treatResult(cpUtils.treatDataFromSpawn(diff), metadata) + return treatResult(cpUtils.treatDataFromSpawn(diff), metadata, config) } -const treatResult = (repoDiffResult, metadata) => { +const treatResult = (repoDiffResult, metadata, config) => { const lines = repoDiffResult.split(os.EOL) const linesPerDiffType = lines.reduce( (acc, line) => (acc[line.charAt(0)]?.push(line), acc), @@ -41,13 +36,33 @@ const treatResult = (repoDiffResult, metadata) => { ) ) - return lines.filter( - line => - !!line && - !deletedRenamed.has(line) && - !ig.ignores(line.replace(gc.GIT_DIFF_TYPE_REGEX, '')) && - line - .split(path.sep) - .some(part => Object.prototype.hasOwnProperty.call(metadata, part)) + return lines + .filter( + line => + !!line && + !deletedRenamed.has(line) && + line + .split(path.sep) + .some(part => Object.prototype.hasOwnProperty.call(metadata, part)) + ) + .filter(filterIgnore(config)) +} + +const filterIgnore = config => line => { + const ig = ignore() + const dig = ignore() + ;[ + { ignore: config.ignore, helper: ig }, + { ignore: config.ignoreDestructive, helper: dig }, + ].forEach( + ign => + ign.ignore && + fs.existsSync(ign.ignore) && + ign.helper.add(fs.readFileSync(ign.ignore).toString()) ) + return config.ignoreDestructive + ? line.startsWith(gc.DELETION) + ? !dig.ignores(line.replace(gc.GIT_DIFF_TYPE_REGEX, '')) + : !ig.ignores(line.replace(gc.GIT_DIFF_TYPE_REGEX, '')) + : !ig.ignores(line.replace(gc.GIT_DIFF_TYPE_REGEX, '')) }