Skip to content

Commit

Permalink
feat: Add diff command
Browse files Browse the repository at this point in the history
  • Loading branch information
y-lakhdar committed Aug 13, 2019
1 parent 402fd74 commit d3e7bd6
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 34 deletions.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
"main": "index.js",
"license": "MIT",
"dependencies": {
"chalk": "^2.4.2",
"cli-table": "^0.3.1",
"commander": "^3.0.0",
"extract-zip": "^1.6.7",
"fs-extra": "^8.1.0"
"fs-extra": "^8.1.0",
"inquirer": "^6.5.1",
"recursive-readdir": "^2.2.2",
"underscore": "^1.9.1"
},
"devDependencies": {
"@babel/cli": "^7.5.5",
Expand Down
30 changes: 24 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
const path = require('path');
const program = require('commander');
// import { areFilesIdentical } from './tasks/diff';
import { retrieveMetadata } from './tasks/retrieve';

// import { extract } from './tasks/extract';
import { arePackagesIdentical } from './tasks/diff';
import { interactive } from './tasks/interactive';
program
.command('compare <username1> <username2> <retrievetargetdir> <xmlPackage>')
.description('Compare metadata between 2 Orgs based on the XML package')
.option('-d --delete', 'Delete file artifacts after the diff')
.option('-p --buildpackage', 'Build XML package based on thr diff')
.action((username1, username2, retrievetargetdir, xmlPackage) => {
Promise.all([retrieveMetadata(retrievetargetdir, xmlPackage, username1), retrieveMetadata(retrievetargetdir, xmlPackage, username2)])
const folderNames = ['mdapipkg-1', 'mdapipkg-2'];
const pathToPackages = [path.resolve(retrievetargetdir, folderNames[0]), path.resolve(retrievetargetdir, folderNames[1])];

Promise.all([retrieveMetadata(pathToPackages[0], xmlPackage, username1), retrieveMetadata(pathToPackages[1], xmlPackage, username2)])
// TODO: add loading animation here
.then(() => {
// Extracting files
// extract();
// TODO: Read files from directory
// TODO: Extract files
// TODO: Compare files
// TODO: Print summary
arePackagesIdentical(pathToPackages[0], pathToPackages[1], { folderNames, pathToPackages })
.then(diffResult => {
// TODO: Ask which field to diff
if (diffResult.TO_UPDATE) {
interactive(diffResult);
}
// TODO: Delete files after if specified
})
.catch(err => {
console.error(err);
});
// TODO: Group Aura components
})
.catch(err => {
console.log(err);
Expand Down
101 changes: 92 additions & 9 deletions src/tasks/diff.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,97 @@
const fs = require('fs-extra');
import { getFileNameFromPath } from '../utils/fileUtils';

const os = require('os');
const chalk = require('chalk');
const Table = require('cli-table');
const _ = require('underscore');
const { fetchAllFiles, getFileContentSync } = require('./../utils/fileUtils');
const { convertPath, normalizeData } = require('./../utils/stringUtils');
// const logger = require('./logger');

const diff = (fileList1, fileList2, options) => {
const summary = { TO_CREATE: [], TO_UPDATE: [], TO_DELETE: [] };
fileList1.forEach(file1 => {
const file2 = convertPath(file1, options.folderNames[0], options.folderNames[1]);
if (_.contains(fileList2, file2)) {
const content1 = getFileContentSync(file1);
const content2 = getFileContentSync(file2);

if (content2) {
// if (Buffer.compare(content1, content2)) {
if (normalizeData(content1) != normalizeData(content2)) {
summary.TO_UPDATE.push([file1, file2]);
}
fileList2 = _.without(fileList2, file2);
} else {
summary.TO_CREATE.push(file1);
}
} else {
summary.TO_CREATE.push(file1);
}
});

// Add files that are left in the second list. This means that they were not present in the initial list
fileList2.forEach(file2 => {
summary.TO_DELETE.push(file2);
});
return summary;
};

const printSummary = diffResult => {
var table = new Table({
head: [chalk.white('State'), chalk.white('Name')],
colWidths: [10, 60]
});

const addToTable = (list, state, style) => {
_.each(list, file => {
table.push([style(state), style(getFileNameFromPath(file))]);
});
};
addToTable(diffResult.TO_CREATE, 'New', chalk.green);
addToTable(diffResult.TO_DELETE, 'Deleted', chalk.red);
addToTable(_.map(diffResult.TO_UPDATE, file => file[0]), 'Changed', chalk.yellow);

console.log(table.toString() + os.EOL);

if (diffResult.TO_CREATE.length > 0) {
console.log(chalk.bold('New files: ' + chalk.green(diffResult.TO_CREATE.length)));
}
if (diffResult.TO_UPDATE.length > 0) {
console.log(chalk.bold('Updated files: ' + chalk.green(diffResult.TO_UPDATE.length)));
}
if (diffResult.TO_DELETE.length > 0) {
console.log(chalk.bold('Deleted files: ' + chalk.green(diffResult.TO_DELETE.length)));
}
if (diffResult.TO_CREATE.length + diffResult.TO_DELETE.length + diffResult.TO_UPDATE.length == 0) {
console.log(chalk.bold('No changes found'));
}

console.log(os.EOL);
};

/**
* Determine if 2 files are identical
* Determine if 2 dirs are identical
*
* @param {*} file1 path to initial file
* @param {*} file2 path to second file
* @returns "true" if the 2 files are identical. "false" otherwise.
* @param {*} dir1 path to initial dir
* @param {*} dir2 path to second dir
* @param {*} [options={}] diff Options
* @returns "true" if the 2 dirs are identical. "false" otherwise.
*/
export const areFilesIdentical = (file1, file2) => {
const data1 = fs.readFileSync(file1);
const data2 = fs.readFileSync(file2);
return data1 == data2;
export const arePackagesIdentical = (dir1, dir2, options = {}) => {
return new Promise((resolve, reject) => {
Promise.all([fetchAllFiles(dir1), fetchAllFiles(dir2)])
.then(arrays => {
try {
const diffResult = diff(arrays[0], arrays[1], options);
printSummary(diffResult);
resolve(diffResult);
} catch (error) {
reject(error);
}
})
.catch(err => {
reject(err);
});
});
};
22 changes: 18 additions & 4 deletions src/tasks/extract.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
// var extract = require('extract-zip');
const path = require('path');
const extractZip = require('extract-zip');

// extract(zipPath, { dir: target }, function(err) {
// // extraction is complete. make sure to handle the err
// });
/**
* Extracts the content of a zip file.
*
* @param {*} zipPath The absolute path of the zip file
*/
export const extract = zipPath => {
return new Promise((resolve, reject) => {
// TODO: put in a variable "sfdc-org-diff"
return extractZip(zipPath, { dir: path.resolve(__dirname, 'sfdc-org-diff') }, err => {
if (err) {
reject(err);
}
resolve();
});
});
};
41 changes: 41 additions & 0 deletions src/tasks/interactive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getFileNameFromPath } from '../utils/fileUtils';
import { spawn } from 'child_process';

const _ = require('underscore');
const inquirer = require('inquirer');

const openVsCodeDiff = filesToDiff => {
_.each(filesToDiff, group => {
spawn('code', ['--diff', group[0], group[1], '--new-window']).on('exit', function(error) {
if (error) {
console.error(error);
}
});
});
};

export const interactive = diffResult => {
const fileDict = {};

_.each(diffResult.TO_UPDATE, group => {
fileDict[getFileNameFromPath(group[0])] = group;
});

inquirer
.prompt([
{
type: 'checkbox',
message: 'Select the files to diff',

name: 'filesToDiff',
choices: _.keys(fileDict)
}
])
.then(answers => {
const filesToDiff = [];
_.each(answers.filesToDiff, file => {
filesToDiff.push(fileDict[file]);
});
openVsCodeDiff(filesToDiff);
});
};
20 changes: 20 additions & 0 deletions src/tasks/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const os = require('os');
const chalk = require('chalk');
const _ = require('underscore');

const log = (message, options, ...meta) => {
let fullMessage = '';
_.each(meta, m => {
if (m.toString()) {
fullMessage += os.EOL + m.toString();
}
options.style = options.style || (x => x);
console.log(options.style(message + fullMessage));
});
};

export const error = (message, ...meta) => {
// if (this.level <= LoggerSingleton.ERROR) {
log(message, { style: chalk.red }, meta);
// }
};
27 changes: 16 additions & 11 deletions src/tasks/retrieve.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawn } from 'child_process';
// import { spawn } from 'child_process';

/**
* Retrieve metadata from an org using Metadata API
Expand All @@ -7,15 +7,20 @@ import { spawn } from 'child_process';
* @param {*} xmlPackage Path to XML Package
* @param {*} targetusername Username or alias for the target org; Ovverides default target org.
*/
export const retrieveMetadata = (retrievetargetdir, xmlPackage, targetusername) => {
return new Promise((resolve, reject) => {
return spawn('sfdx', ['force:mdapi:retrieve', '-r', retrievetargetdir, '-u', targetusername, '-k', xmlPackage], {
stdio: 'inherit'
}).on('exit', function(error) {
if (error) {
reject(error);
}
resolve();
});
export const retrieveMetadata = () => {
return new Promise(resolve => {
resolve();
});
};
// export const retrieveMetadata = (retrievetargetdir, xmlPackage, targetusername) => {
// return new Promise((resolve, reject) => {
// return spawn('sfdx', ['force:mdapi:retrieve', '-r', retrievetargetdir, '-u', targetusername, '-k', xmlPackage], {
// stdio: 'inherit'
// }).on('exit', function(error) {
// if (error) {
// reject(error);
// }
// resolve();
// });
// });
// };
28 changes: 28 additions & 0 deletions src/utils/fileUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const _ = require('underscore');
const fs = require('fs-extra');
const recursive = require('recursive-readdir');

export const fetchAllFiles = rootDir => {
return new Promise((resolve, reject) => {
const ignore = ['*-meta.xml', '.DS_Store'];
recursive(rootDir, ignore)
.then(data => {
resolve(data);
})
.catch(() => {
// TODO: add logger
reject(`Unable to read package directory "${rootDir}"`);
});
});
};

export const getFileContentSync = filePath => {
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, { encoding: 'utf8' });
}
return null;
};

export const getFileNameFromPath = path => {
return _.last(path.split('/'));
};
9 changes: 9 additions & 0 deletions src/utils/stringUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const os = require('os');

export const convertPath = (text, initial, replacer) => {
return text.replace(initial, replacer);
};

export const normalizeData = data => {
return data.replace(/\r\n/gm, os.EOL).replace(/\n/gm, os.EOL);
};
Loading

0 comments on commit d3e7bd6

Please sign in to comment.