UNPKG

@dxatscale/sfprofiles

Version:
568 lines (507 loc) 22.8 kB
/* eslint-disable @typescript-eslint/no-array-constructor */ import MetadataFiles from '@impl/metadata/metadataFiles'; import * as xml2js from 'xml2js'; import * as path from 'path'; import * as fs from 'fs-extra'; import * as rimraf from 'rimraf'; import { SOURCE_EXTENSION_REGEX, MetadataInfo, METADATA_INFO, UNSPLITED_METADATA, PROFILE_PERMISSIONSET_EXTENSION, } from '@impl/metadata/metadataInfo'; import FileUtils from '@utils/fileutils'; import * as _ from 'lodash'; import ProfileDiff from './profileDiff'; import PermsetDiff from './permsetDiff'; import WorkflowDiff from './workflowDiff'; import SharingRuleDiff from './sharingRuleDiff'; import CustomLabelsDiff from './customLabelsDiff'; import DiffUtil, { DiffFile, DiffFileStatus } from './diffUtil'; import { Sfpowerkit } from '@utils/sfpowerkit'; import SFPLogger, {LoggerLevel } from '@dxatscale/sfp-logger'; import { DXProjectManifestUtils } from '@utils/dxProjectManifestUtils'; import simplegit from 'simple-git'; import { Messages } from '@salesforce/core'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('sfpowerkit', 'project_diff'); const deleteNotSupported = ['RecordType']; const git = simplegit(); const unsplitedMetadataExtensions = UNSPLITED_METADATA.map((elem) => { return elem.sourceExtension; }); const permissionExtensions = PROFILE_PERMISSIONSET_EXTENSION.map((elem) => { return elem.sourceExtension; }); const SEP = /\/|\\/; export default class DiffImpl { destructivePackageObjPre: any[]; destructivePackageObjPost: any[]; resultOutput: { action: string; metadataType: string; componentName: string; message: string; path: string; }[]; public constructor( private revisionFrom?: string, private revisionTo?: string, private isDestructive?: boolean, private pathToIgnore?: any[] ) { if (this.revisionTo == null || this.revisionTo.trim() === '') { this.revisionTo = 'HEAD'; } if (this.revisionFrom == null) { this.revisionFrom = ''; } this.destructivePackageObjPost = new Array(); this.destructivePackageObjPre = new Array(); this.resultOutput = []; } public async build(outputFolder: string, packagedirectories: string[], apiversion: string) { rimraf.sync(outputFolder); if (packagedirectories) { Sfpowerkit.setProjectDirectories(packagedirectories); } if (apiversion) { Sfpowerkit.setapiversion(apiversion); } //const sepRegex=/\t| |\n/; const sepRegex = /\n|\r/; let data = ''; //check if same commit const commitFrom = await git.raw(['rev-list', '-n', '1', this.revisionFrom]); const commitTo = await git.raw(['rev-list', '-n', '1', this.revisionTo]); if (commitFrom === commitTo) { throw new Error(messages.getMessage('sameCommitErrorMessage')); } //Make it relative to make the command works from a project created as a subfolder in a repository git.addConfig('core.quotepath','false'); data = await git.diff(["--no-renames", '--raw', this.revisionFrom, this.revisionTo, '--relative']); SFPLogger.log(`Input Param: From: ${this.revisionFrom} To: ${this.revisionTo} `, LoggerLevel.INFO); SFPLogger.log(`SHA Found From: ${commitFrom} To: ${commitTo} `, LoggerLevel.INFO); SFPLogger.log(data, LoggerLevel.TRACE); let content = data.split(sepRegex); let diffFile: DiffFile = await DiffUtil.parseContent(content); await DiffUtil.fetchFileListRevisionTo(this.revisionTo); let filesToCopy = diffFile.addedEdited; let deletedFiles = diffFile.deleted; deletedFiles = deletedFiles.filter((deleted) => { let found = false; let deletedMetadata = MetadataFiles.getFullApiNameWithExtension(deleted.path); for (let i = 0; i < filesToCopy.length; i++) { let addedOrEdited = MetadataFiles.getFullApiNameWithExtension(filesToCopy[i].path); if (deletedMetadata === addedOrEdited) { found = true; break; } } return !found; }); if (fs.existsSync(outputFolder) == false) { fs.mkdirSync(outputFolder); } SFPLogger.log('Files to be copied', LoggerLevel.DEBUG); filesToCopy.forEach((element)=>{ SFPLogger.log(element as any,LoggerLevel.DEBUG) }); if (filesToCopy && filesToCopy.length > 0) { for (let i = 0; i < filesToCopy.length; i++) { let filePath = filesToCopy[i].path; try { if (DiffImpl.checkForIngore(this.pathToIgnore, filePath)) { let matcher = filePath.match(SOURCE_EXTENSION_REGEX); let extension = ''; if (matcher) { extension = matcher[0]; } else { extension = path.parse(filePath).ext; } if (unsplitedMetadataExtensions.includes(extension)) { //handle unsplited files await this.handleUnsplittedMetadata(filesToCopy[i], outputFolder); } else { await DiffUtil.copyFile(filePath, outputFolder); SFPLogger.log(`Copied file ${filePath} to ${outputFolder}`, LoggerLevel.DEBUG); } } } catch (ex) { this.resultOutput.push({ action: 'ERROR', componentName: '', metadataType: '', message: ex.message, path: filePath, }); } } } if (this.isDestructive) { SFPLogger.log('Creating Destructive Manifest..', LoggerLevel.INFO); await this.createDestructiveChanges(deletedFiles, outputFolder); } SFPLogger.log(`Generating output summary`, LoggerLevel.INFO); this.buildOutput(outputFolder); if (this.resultOutput.length > 0) { try { await DiffUtil.copyFile('.forceignore', outputFolder); } catch (e) { SFPLogger.log(`.forceignore not found, skipping..`, LoggerLevel.INFO); } try { //check if package path is provided if (packagedirectories) { let sourceApiVersion = await Sfpowerkit.getApiVersion(); let packageDirectorieslist = []; packagedirectories.forEach((path) => { packageDirectorieslist.push({ path: path, }); }); packageDirectorieslist[0].default = true; let sfdx_project = { packageDirectories: packageDirectorieslist, namespace: '', sourceApiVersion: sourceApiVersion, }; fs.outputFileSync(`${outputFolder}/sfdx-project.json`, JSON.stringify(sfdx_project)); } else { //Copy project manifest await DiffUtil.copyFile('sfdx-project.json', outputFolder); } //Remove Project Directories that doesnt have any components in ths diff Fix #178 let dxProjectManifestUtils: DXProjectManifestUtils = new DXProjectManifestUtils(outputFolder); dxProjectManifestUtils.removePackagesNotInDirectory(); } catch (e) { SFPLogger.log(`sfdx-project.json not found, skipping..`, LoggerLevel.INFO); } } return this.resultOutput; } private static checkForIngore(pathToIgnore: any[], filePath: string) { pathToIgnore = pathToIgnore || []; if (pathToIgnore.length === 0) { return true; } let returnVal = true; pathToIgnore.forEach((ignore) => { if ( path.resolve(ignore) === path.resolve(filePath) || path.resolve(filePath).includes(path.resolve(ignore)) ) { returnVal = false; } }); return returnVal; } private buildOutput(outputFolder) { let metadataFiles = new MetadataFiles(); metadataFiles.loadComponents(outputFolder, false); let keys = Object.keys(METADATA_INFO); let excludedFiles = _.difference(unsplitedMetadataExtensions, permissionExtensions); keys.forEach((key) => { if (METADATA_INFO[key].files && METADATA_INFO[key].files.length > 0) { METADATA_INFO[key].files.forEach((filePath) => { let matcher = filePath.match(SOURCE_EXTENSION_REGEX); let extension = ''; if (matcher) { extension = matcher[0]; } else { extension = path.parse(filePath).ext; } if (!excludedFiles.includes(extension)) { let name = FileUtils.getFileNameWithoutExtension(filePath, METADATA_INFO[key].sourceExtension); if (METADATA_INFO[key].isChildComponent) { let fileParts = filePath.split(SEP); let parentName = fileParts[fileParts.length - 3]; name = parentName + '.' + name; } this.resultOutput.push({ action: 'Deploy', metadataType: METADATA_INFO[key].xmlName, componentName: name, message: '', path: filePath, }); } }); } }); return this.resultOutput; } private async handleUnsplittedMetadata(diffFile: DiffFileStatus, outputFolder: string) { let content1 = ''; let content2 = ''; try { if (diffFile.revisionFrom !== '0000000') { content1 = await git.show(['--raw', diffFile.revisionFrom]); } } catch (e) {} try { if (diffFile.revisionTo !== '0000000') { content2 = await git.show(['--raw', diffFile.revisionTo]); } } catch (e) {} FileUtils.mkDirByPathSync(path.join(outputFolder, path.parse(diffFile.path).dir)); if (diffFile.path.endsWith(METADATA_INFO.Workflow.sourceExtension)) { //Workflow let baseName = path.parse(diffFile.path).base; let objectName = baseName.split('.')[0]; await WorkflowDiff.generateWorkflowXml( content1, content2, path.join(outputFolder, diffFile.path), objectName, this.destructivePackageObjPost, this.resultOutput, this.isDestructive ); } if (diffFile.path.endsWith(METADATA_INFO.SharingRules.sourceExtension)) { let baseName = path.parse(diffFile.path).base; let objectName = baseName.split('.')[0]; await SharingRuleDiff.generateSharingRulesXml( content1, content2, path.join(outputFolder, diffFile.path), objectName, this.destructivePackageObjPost, this.resultOutput, this.isDestructive ); } if (diffFile.path.endsWith(METADATA_INFO.CustomLabels.sourceExtension)) { await CustomLabelsDiff.generateCustomLabelsXml( content1, content2, path.join(outputFolder, diffFile.path), this.destructivePackageObjPost, this.resultOutput, this.isDestructive ); } if (diffFile.path.endsWith(METADATA_INFO.Profile.sourceExtension)) { //Deploy only what changed if (content1 === '') { await DiffUtil.copyFile(diffFile.path, outputFolder); SFPLogger.log(`Copied file ${diffFile.path} to ${outputFolder}`, LoggerLevel.DEBUG); } else if (content2 === '') { //The profile is deleted or marked as renamed. //Delete the renamed one let profileType: any = _.find(this.destructivePackageObjPost, function (metaType: any) { return metaType.name === METADATA_INFO.Profile.xmlName; }); if (profileType === undefined) { profileType = { name: METADATA_INFO.Profile.xmlName, members: [], }; this.destructivePackageObjPost.push(profileType); } let baseName = path.parse(diffFile.path).base; let profileName = baseName.split('.')[0]; profileType.members.push(profileName); } else { await ProfileDiff.generateProfileXml(content1, content2, path.join(outputFolder, diffFile.path)); } } if (diffFile.path.endsWith(METADATA_INFO.PermissionSet.sourceExtension)) { let sourceApiVersion = await Sfpowerkit.getApiVersion(); if (content1 === '') { await DiffUtil.copyFile(diffFile.path, outputFolder); SFPLogger.log(`Copied file ${diffFile.path} to ${outputFolder}`, LoggerLevel.DEBUG); } else if (sourceApiVersion <= 39.0) { // in API 39 and erliar PermissionSet deployment are merged. deploy only what changed if (content2 === '') { //Deleted permissionSet let permsetType: any = _.find(this.destructivePackageObjPost, function (metaType: any) { return metaType.name === METADATA_INFO.PermissionSet.xmlName; }); if (permsetType === undefined) { permsetType = { name: METADATA_INFO.PermissionSet.xmlName, members: [], }; this.destructivePackageObjPost.push(permsetType); } let baseName = path.parse(diffFile.path).base; let permsetName = baseName.split('.')[0]; permsetType.members.push(permsetName); } else { await PermsetDiff.generatePermissionsetXml( content1, content2, path.join(outputFolder, diffFile.path) ); } } else { //PermissionSet deployment override in the target org //So deploy the whole file await DiffUtil.copyFile(diffFile.path, outputFolder); SFPLogger.log(`Copied file ${diffFile.path} to ${outputFolder}`, LoggerLevel.DEBUG); } } } private async createDestructiveChanges(filePaths: DiffFileStatus[], outputFolder: string) { if (_.isNil(this.destructivePackageObjPost)) { this.destructivePackageObjPost = new Array(); } else { this.destructivePackageObjPost = this.destructivePackageObjPost.filter((metaType) => { return !_.isNil(metaType.members) && metaType.members.length > 0; }); } this.destructivePackageObjPre = new Array(); //returns root, dir, base and name for (let i = 0; i < filePaths.length; i++) { let filePath = filePaths[i].path; try { let matcher = filePath.match(SOURCE_EXTENSION_REGEX); let extension = ''; if (matcher) { extension = matcher[0]; } else { extension = path.parse(filePath).ext; } if (unsplitedMetadataExtensions.includes(extension)) { //handle unsplited files await this.handleUnsplittedMetadata(filePaths[i], outputFolder); continue; } let parsedPath = path.parse(filePath); let filename = parsedPath.base; let name = MetadataInfo.getMetadataName(filePath); if (name) { if (!MetadataFiles.isCustomMetadata(filePath, name)) { // avoid to generate destructive for Standard Components //Support on Custom Fields and Custom Objects for now this.resultOutput.push({ action: 'Skip', componentName: MetadataFiles.getMemberNameFromFilepath(filePath, name), metadataType: 'StandardField/CustomMetadata', message: '', path: '--', }); continue; } let member = MetadataFiles.getMemberNameFromFilepath(filePath, name); if (name === METADATA_INFO.CustomField.xmlName) { let isFormular = await DiffUtil.isFormulaField(filePaths[i]); if (isFormular) { this.destructivePackageObjPre = this.buildDestructiveTypeObj( this.destructivePackageObjPre, name, member ); SFPLogger.log( `${filePath} ${MetadataFiles.isCustomMetadata(filePath, name)}`, LoggerLevel.DEBUG ); this.resultOutput.push({ action: 'Delete', componentName: member, metadataType: name, message: '', path: 'Manual Intervention Required', }); } else { this.destructivePackageObjPost = this.buildDestructiveTypeObj( this.destructivePackageObjPost, name, member ); } SFPLogger.log( `${filePath} ${MetadataFiles.isCustomMetadata(filePath, name)}`, LoggerLevel.DEBUG ); this.resultOutput.push({ action: 'Delete', componentName: member, metadataType: name, message: '', path: 'destructiveChanges.xml', }); } else { if (!deleteNotSupported.includes(name)) { this.destructivePackageObjPost = this.buildDestructiveTypeObj( this.destructivePackageObjPost, name, member ); this.resultOutput.push({ action: 'Delete', componentName: member, metadataType: name, message: '', path: 'destructiveChanges.xml', }); } else { //add the component in the manual action list // TODO } } } } catch (ex) { this.resultOutput.push({ action: 'ERROR', componentName: '', metadataType: '', message: ex.message, path: filePath, }); } } // this.writeDestructivechanges( // this.destructivePackageObjPre, // outputFolder, // "destructiveChangesPre.xml" // ); this.writeDestructivechanges(this.destructivePackageObjPost, outputFolder, 'destructiveChanges.xml'); } private writeDestructivechanges(destrucObj: Array<any>, outputFolder: string, fileName: string) { //ensure unique component per type for (let i = 0; i < destrucObj.length; i++) { destrucObj[i].members = _.uniq(destrucObj[i].members); } destrucObj = destrucObj.filter((metaType) => { return metaType.members && metaType.members.length > 0; }); if (destrucObj.length > 0) { let dest = { Package: { $: { xmlns: 'htt@impl/metadata', }, types: destrucObj, }, }; let destructivePackageName = fileName; let filepath = path.join(outputFolder, destructivePackageName); let builder = new xml2js.Builder(); let xml = builder.buildObject(dest); fs.writeFileSync(filepath, xml); } } private buildDestructiveTypeObj(destructiveObj, name, member) { let typeIsPresent = false; for (let i = 0; i < destructiveObj.length; i++) { if (destructiveObj[i].name === name) { typeIsPresent = true; destructiveObj[i].members.push(member); break; } } let typeNode: any; if (typeIsPresent === false) { typeNode = { name: name, members: [member], }; destructiveObj.push(typeNode); } return destructiveObj; } }