Skip to content

Commit

Permalink
Merge pull request #15 from ChuckJonas/ralphcallaway-destructive-changes
Browse files Browse the repository at this point in the history
Ralphcallaway destructive changes
  • Loading branch information
ChuckJonas authored Oct 20, 2019
2 parents 6a92c4c + d636d58 commit c3f930a
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 77 deletions.
3 changes: 1 addition & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
"request": "attach",
"name": "Attach to Remote",
"address": "127.0.0.1",
"port": 9229,
"localRoot": "${workspaceFolder}"
"port": 9229
},
{
"name": "Unit Tests",
Expand Down
3 changes: 2 additions & 1 deletion messages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"fromBranchDescription": "The git ref (branch or commit) which we are deploying from. If left blank, will use working copy",
"force": "Continue even if source is behind target",
"purgeDescription": "Delete output dir if it already exists (without prompt)",
"ignoreWhitespace": "Don't package changes that are whitespace only"
"ignoreWhitespace": "Don't package changes that are whitespace only",
"nodelete": "Don't generate destructiveChanges.xml for deletions"
}
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sfdx-git-packager",
"description": "Generates a package.xml for difference between two branches",
"version": "0.0.1",
"version": "0.0.2",
"author": "Charlie Jonas @ChuckJonas",
"bugs": "https://github.com/ChuckJonas/sfdx-git-diff-to-pkg/issues",
"dependencies": {
Expand All @@ -13,7 +13,6 @@
"diff": "^4.0.1",
"glob-regex": "^0.3.2",
"ignore": "^5.1.4",
"rimraf": "^3.0.0",
"tmp": "^0.1.0",
"tslib": "^1",
"xml2js": "^0.4.22"
Expand All @@ -24,7 +23,6 @@
"@oclif/test": "^1",
"@salesforce/dev-config": "1.4.1",
"@types/chai": "^4",
"@types/rimraf": "^2.0.2",
"@types/diff": "^4.0.2",
"@types/mocha": "^5",
"@types/node": "^10",
Expand Down
100 changes: 73 additions & 27 deletions src/commands/git/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as jsdiff from 'diff';
import { promises as fsPromise } from 'fs';
import { dirname, isAbsolute, join, relative } from 'path';
import * as tmp from 'tmp';
import { resolveMetadata } from '../../metadataResolvers';
import { getResolver, resolveMetadata } from '../../metadataResolvers';
import { copyFileFromRef, getIgnore, purgeFolder, spawnPromise } from '../../util';

interface DiffResults {
Expand Down Expand Up @@ -40,9 +40,10 @@ export default class Package extends SfdxCommand {
sourceref: flags.string({ char: 's', description: messages.getMessage('fromBranchDescription') }),
targetref: flags.string({ char: 't', description: messages.getMessage('toBranchDescription'), default: 'master' }),
outputdir: flags.string({ char: 'd', description: messages.getMessage('outputdirDescription'), required: true }),
ignorewhitespace: flags.boolean({ char: 'w', description: messages.getMessage('ignoreWhitespace')}),
ignorewhitespace: flags.boolean({ char: 'w', description: messages.getMessage('ignoreWhitespace') }),
purge: flags.boolean({ description: messages.getMessage('purgeDescription') }),
force: flags.boolean({ char: 'f', description: messages.getMessage('force')})
nodelete: flags.boolean({ description: messages.getMessage('nodelete') }),
force: flags.boolean({ char: 'f', description: messages.getMessage('force') })
};

// Comment this out if your command does not require an org username
Expand Down Expand Up @@ -73,7 +74,7 @@ export default class Package extends SfdxCommand {

try {
const diffRefs = `${toBranch}...` + (fromBranch ? fromBranch : '');
const aheadBehind = await spawnPromise('git', ['rev-list', '--left-right', '--count', diffRefs], {shell: true});
const aheadBehind = await spawnPromise('git', ['rev-list', '--left-right', '--count', diffRefs], { shell: true });
const behind = Number(aheadBehind.split(/(\s+)/)[0]);
if (behind > 0) {
const behindMessage = `${fromBranch ? fromBranch : '"working tree"'} is ${behind} commit(s) behind ${toBranch}! You probably want to rebase ${toBranch} into ${fromBranch} before deploying!`;
Expand All @@ -87,19 +88,30 @@ export default class Package extends SfdxCommand {
}
}

const diff = await spawnPromise('git', diffArgs, {shell: true});
const changes = await this.getChanged(diff);
if (!changes.changed.length) {
const diff = await spawnPromise('git', diffArgs, { shell: true });
const diffResults = await this.getChanged(diff, fromBranch);

const hasChanges = diffResults.changed.length > 0;
const hasDeletions = diffResults.removed.length > 0 && !this.flags.nodelete;
if (!hasChanges && !hasDeletions) {
this.ux.warn('No changes found!');
this.exit(1);
return;
}

// create a temp project so we can leverage force:source:convert
// create a temp project so we can leverage force:source:convert for destructiveChanges

const tmpProject = await this.setupTmpProject(changes, fromBranch);
const outDir = isAbsolute(this.flags.outputdir) ? this.flags.outputdir : join(this.projectPath, this.flags.outputdir);
let tmpDeleteProj: string;
let tempDeleteProjConverted: string;
if (hasDeletions) {
tmpDeleteProj = await this.setupTmpProject(diffResults.removed, toBranch);
tempDeleteProjConverted = await this.mkTempDir();
await spawnPromise('sfdx', ['force:source:convert', '-d', tempDeleteProjConverted], { shell: true, cwd: tmpDeleteProj });
}

// create a temp project so we can leverage force:source:convert for primary deploy
const tmpProject = await this.setupTmpProject(diffResults.changed, fromBranch);
const outDir = isAbsolute(this.flags.outputdir) ? this.flags.outputdir : join(this.projectPath, this.flags.outputdir);
try {
const stat = await fs.stat(outDir);
if (stat.isDirectory()) {
Expand All @@ -120,17 +132,23 @@ export default class Package extends SfdxCommand {
try {
await purgeFolder(outDir);
} catch (e) {
this.ux.error('Failed to remove files');
this.ux.error(e);
this.exit(1);
return;
}

}
}
} catch (e) {}
} catch (e) { }

await spawnPromise('sfdx', ['force:source:convert', '-d', outDir], {shell: true, cwd: tmpProject});
await fs.mkdirp(outDir);

if (hasChanges) {
await spawnPromise('sfdx', ['force:source:convert', '-d', outDir], { shell: true, cwd: tmpProject });
}
if (hasDeletions) {
await fsPromise.copyFile(`${tempDeleteProjConverted}/package.xml`, `${outDir}/destructiveChanges.xml`);
}
} catch (e) {
this.ux.error(e);
this.exit(1);
Expand All @@ -139,8 +157,7 @@ export default class Package extends SfdxCommand {
return {};
}

private async setupTmpProject(diff: DiffResults, targetRef: string) {

private async mkTempDir() {
const tempDir = await new Promise<string>((resolve, reject) => {
tmp.dir((err, path) => {
if (err) {
Expand All @@ -149,16 +166,21 @@ export default class Package extends SfdxCommand {
resolve(path);
});
});
// const tempDir = join(this.projectPath, TEMP);
await fs.mkdirp(tempDir);
return tempDir;
}

private async setupTmpProject(changed: string[], targetRef: string | undefined) {
const tempDir = await this.mkTempDir();

for (const sourcePath of this.sourcePaths) {
await fs.mkdirp(join(tempDir, sourcePath));
}

await fsPromise.copyFile(join(this.projectPath, 'sfdx-project.json'), join(tempDir, 'sfdx-project.json'));

for (const path of diff.changed) {
const metadataPaths = await resolveMetadata(path);
for (const path of changed) {
const metadataPaths = await resolveMetadata(path, targetRef);

if (!metadataPaths) {
this.ux.warn(`Could not resolve metadata for ${path}`);
Expand All @@ -173,28 +195,31 @@ export default class Package extends SfdxCommand {

const newPath = join(tempDir, mdPath);
await fs.mkdirp(dirname(newPath));

if (targetRef) {
await copyFileFromRef(mdPath, targetRef, newPath);
} else {
await fsPromise.copyFile(mdPath, newPath);
}
}

}

return tempDir;
}

private async getChanged(diffOutput: string): Promise<DiffResults> {
private async getChanged(diffOutput: string, targetRef: string): Promise<DiffResults> {
const ignore = await getIgnore(this.projectPath);
const lines = diffOutput.split(/\r?\n/);
// tuple of additions, deletions
const changed = [];
const removed = [];
let removed = [];
for (const line of lines) {
const parts = line.split('\t');
const parts = line.split('\t');
const status = parts[0];
const path = parts[1];

if (!path || path.startsWith('.') || ignore.ignores(path) ) {
if (!path || path.startsWith('.') || ignore.ignores(path)) {
continue;
}

Expand All @@ -221,14 +246,35 @@ export default class Package extends SfdxCommand {
continue;
}

// [TODO] build a "distructivechanges.xml"
if (status === 'D') {
removed.push(path);
} else {
changed.push(path);
}

}

// check for directory style resources that are full deletions (and are actually changes)
const notFullyRemoved = [];
for (const path of removed) {
const resolver = getResolver(path);

if (resolver.getIsDirectory()) {
const metadataPaths = await resolver.getMetadataPaths(path, targetRef);
// current implementation will return meta file regardless of whether it exists in org or not
if (metadataPaths.length > 1) {

notFullyRemoved.push(path);
for (const mdPath of metadataPaths) {
if (!changed.includes(mdPath)) {
changed.push(mdPath);
}
}
}
}
}

removed = removed.filter(path => !notFullyRemoved.includes(path));
return {
changed,
removed
Expand All @@ -239,11 +285,11 @@ export default class Package extends SfdxCommand {

// checks two strings and returns true if they have "non-whitespace" changes (spaces or newlines)
function hasNonWhitespaceChanges(a: string, b: string) {
const diffResults = jsdiff.diffLines(a, b, {ignoreWhitespace: true, newlineIsToken: true});
const diffResults = jsdiff.diffLines(a, b, { ignoreWhitespace: true, newlineIsToken: true });
for (const result of diffResults) {
if (result.added || result.removed) {
return true;
}
if (result.added || result.removed) {
return true;
}
}
return false;
}
46 changes: 29 additions & 17 deletions src/metadataResolvers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { extname } from 'path';
import { getFiles } from './util';
import { getFilesFromRef } from './util';

// these need to be re-witten for windows... maybe use globs instead
const AURA_REGEX = /(.*\/aura\/\w*)\/.*/;
Expand All @@ -9,7 +9,8 @@ const STATIC_RESOURCE_FILE_REGEX = /(.*\/staticresources\/\w*)\.\w*/;

interface MetadataResolver {
match: ((path: string) => boolean) | RegExp;
getMetadataPaths: (path: string) => Promise<string[]>;
getMetadataPaths: (path: string, ref: string) => Promise<string[]>;
getIsDirectory: () => boolean;
}

// order from most selective to least
Expand All @@ -20,52 +21,63 @@ const metadataResolvers: MetadataResolver[] = [
},
getMetadataPaths: async (path: string) => {
return [path, path + '-meta.xml'];
}
},
getIsDirectory: () => false
},
{ // reverse of above rule
match: COMP_META,
getMetadataPaths: async (path: string) => {
getMetadataPaths: async (path: string, ref: string) => {
return [path, path.replace('-meta.xml', '')];
}
},
getIsDirectory: () => false
},
{ // other metadata
match: path => {
return path.endsWith('-meta.xml');
},
getMetadataPaths: async (path: string) => {
getMetadataPaths: async (path: string, ref: string) => {
return [path];
}
},
getIsDirectory: () => false
},
{ // aura bundles
match: AURA_REGEX,
getMetadataPaths: async (path: string) => {
getMetadataPaths: async (path: string, ref: string) => {
const appDir = AURA_REGEX.exec(path)[1];
return await getFiles(appDir);
}
return await getFilesFromRef(appDir, ref);
},
getIsDirectory: () => true
},
{ // decompressed static resource (folders)
match: STATIC_RESOURCE_FOLDER_REGEX,
getMetadataPaths: async (path: string) => {
getMetadataPaths: async (path: string, ref: string) => {
const appDir = STATIC_RESOURCE_FOLDER_REGEX.exec(path)[1];
return [...await getFiles(appDir), `${appDir}.resource-meta.xml`];
}
return [...await getFilesFromRef(appDir, ref), `${appDir}.resource-meta.xml`];
},
getIsDirectory: () => true
},
{ // static resource (single files)
match: STATIC_RESOURCE_FILE_REGEX,
getMetadataPaths: async (path: string) => {
const baseName = STATIC_RESOURCE_FILE_REGEX.exec(path)[1];
return [path, `${baseName}.resource-meta.xml` ];
}
},
getIsDirectory: () => false
}
];

// given a path, return all other paths that must be included along side for a valid deployment
export function resolveMetadata(path: string) {
export function getResolver(path: string) {
for (const resolver of metadataResolvers) {
const isMatch = resolver.match instanceof RegExp ? resolver.match.test(path) : resolver.match(path);
if (isMatch) {
return resolver.getMetadataPaths(path);
return resolver;
}
}
return null;
}

// given a path, return all other paths that must be included along side for a valid deployment
export function resolveMetadata(path: string, ref: string) {
const resolver = getResolver(path);
return resolver && resolver.getMetadataPaths(path, ref);
}
Loading

0 comments on commit c3f930a

Please sign in to comment.