-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
459 additions
and
280 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/* | ||
* 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 { resolve } from 'path'; | ||
import { SfError } from '@salesforce/core'; | ||
import { SourceComponent, ComponentSet, ForceIgnore } from '@salesforce/source-deploy-retrieve'; | ||
import { ConflictResponse, ChangeResult } from './types'; | ||
import { getMetadataKey } from './functions'; | ||
import { populateTypesAndNames } from './populateTypesAndNames'; | ||
export const throwIfConflicts = (conflicts: ConflictResponse[]): void => { | ||
if (conflicts.length > 0) { | ||
const conflictError = new SfError('Conflict detected'); | ||
conflictError.setData(conflicts); | ||
} | ||
}; | ||
|
||
export const findConflictsInComponentSet = ( | ||
components: SourceComponent[], | ||
conflicts: ChangeResult[] | ||
): ConflictResponse[] => { | ||
// map do dedupe by name-type-filename | ||
const conflictMap = new Map<string, ConflictResponse>(); | ||
const cs = new ComponentSet(components); | ||
conflicts | ||
.filter((cr) => cr.name && cr.type && cs.has({ fullName: cr.name, type: cr.type })) | ||
.forEach((c) => { | ||
c.filenames?.forEach((f) => { | ||
conflictMap.set(`${c.name}#${c.type}#${f}`, { | ||
state: 'Conflict', | ||
// the following 2 type assertions are valid because of previous filter statement | ||
// they can be removed once TS is smarter about filtering | ||
fullName: c.name as string, | ||
type: c.type as string, | ||
filePath: resolve(f), | ||
}); | ||
}); | ||
}); | ||
const reformattedConflicts = Array.from(conflictMap.values()); | ||
return reformattedConflicts; | ||
}; | ||
|
||
export const dedupeConflictChangeResults = ({ | ||
localChanges = [], | ||
remoteChanges = [], | ||
projectPath, | ||
forceIgnore, | ||
}: { | ||
localChanges: ChangeResult[]; | ||
remoteChanges: ChangeResult[]; | ||
projectPath: string; | ||
forceIgnore: ForceIgnore; | ||
}): ChangeResult[] => { | ||
// index the remoteChanges by filename | ||
const fileNameIndex = new Map<string, ChangeResult>(); | ||
const metadataKeyIndex = new Map<string, ChangeResult>(); | ||
remoteChanges.map((change) => { | ||
if (change.name && change.type) { | ||
metadataKeyIndex.set(getMetadataKey(change.name, change.type), change); | ||
} | ||
change.filenames?.map((filename) => { | ||
fileNameIndex.set(filename, change); | ||
}); | ||
}); | ||
|
||
const conflicts = new Set<ChangeResult>(); | ||
|
||
populateTypesAndNames({ elements: localChanges, excludeUnresolvable: true, projectPath, forceIgnore }).map( | ||
(change) => { | ||
const metadataKey = getMetadataKey(change.name as string, change.type as string); | ||
// option 1: name and type match | ||
if (metadataKeyIndex.has(metadataKey)) { | ||
conflicts.add({ ...(metadataKeyIndex.get(metadataKey) as ChangeResult) }); | ||
} else { | ||
// option 2: some of the filenames match | ||
change.filenames?.map((filename) => { | ||
if (fileNameIndex.has(filename)) { | ||
conflicts.add({ ...(fileNameIndex.get(filename) as ChangeResult) }); | ||
} | ||
}); | ||
} | ||
} | ||
); | ||
// deeply de-dupe | ||
return Array.from(conflicts); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* | ||
* 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 { EOL } from 'os'; | ||
import { Logger } from '@salesforce/core'; | ||
import { ComponentSet } from '@salesforce/source-deploy-retrieve'; | ||
import { ChangeResult } from './types'; | ||
import { metadataMemberGuard } from './guards'; | ||
import { getKeyFromObject, getMetadataKey } from './functions'; | ||
|
||
const logger = Logger.childFromRoot('SourceTracking.PopulateFilePaths'); | ||
|
||
/** | ||
* Will build a component set, crawling your local directory, to get paths for remote changes | ||
* | ||
* @param elements ChangeResults that may or may not have filepaths in their filenames parameters | ||
* @param packageDirPaths Array of paths from PackageDirectories | ||
* @returns | ||
*/ | ||
export const populateFilePaths = (elements: ChangeResult[], packageDirPaths: string[]): ChangeResult[] => { | ||
if (elements.length === 0) { | ||
return []; | ||
} | ||
|
||
logger.debug('populateFilePaths for change elements', elements); | ||
// component set generated from an array of MetadataMember from all the remote changes | ||
// but exclude the ones that aren't in the registry | ||
const remoteChangesAsMetadataMember = elements | ||
.map((element) => { | ||
if (typeof element.type === 'string' && typeof element.name === 'string') { | ||
return { | ||
type: element.type, | ||
fullName: element.name, | ||
}; | ||
} | ||
}) | ||
.filter(metadataMemberGuard); | ||
|
||
const remoteChangesAsComponentSet = new ComponentSet(remoteChangesAsMetadataMember); | ||
|
||
logger.debug(` the generated component set has ${remoteChangesAsComponentSet.size.toString()} items`); | ||
if (remoteChangesAsComponentSet.size < elements.length) { | ||
// there *could* be something missing | ||
// some types (ex: LWC) show up as multiple files in the remote changes, but only one in the component set | ||
// iterate the elements to see which ones didn't make it into the component set | ||
const missingComponents = elements.filter( | ||
(element) => | ||
!remoteChangesAsComponentSet.has({ type: element?.type as string, fullName: element?.name as string }) | ||
); | ||
// Throw if anything was actually missing | ||
if (missingComponents.length > 0) { | ||
throw new Error( | ||
`unable to generate complete component set for ${elements | ||
.map((element) => `${element.name} (${element.type})`) | ||
.join(EOL)}` | ||
); | ||
} | ||
} | ||
|
||
const matchingLocalSourceComponentsSet = ComponentSet.fromSource({ | ||
fsPaths: packageDirPaths, | ||
include: remoteChangesAsComponentSet, | ||
}); | ||
logger.debug( | ||
` local source-backed component set has ${matchingLocalSourceComponentsSet.size.toString()} items from remote` | ||
); | ||
|
||
// make it simpler to find things later | ||
const elementMap = new Map<string, ChangeResult>(); | ||
elements.map((element) => { | ||
elementMap.set(getKeyFromObject(element), element); | ||
}); | ||
|
||
// iterates the local components and sets their filenames | ||
for (const matchingComponent of matchingLocalSourceComponentsSet.getSourceComponents().toArray()) { | ||
if (matchingComponent.fullName && matchingComponent.type.name) { | ||
logger.debug( | ||
`${matchingComponent.fullName}|${matchingComponent.type.name} matches ${ | ||
matchingComponent.xml | ||
} and maybe ${matchingComponent.walkContent().toString()}` | ||
); | ||
const key = getMetadataKey(matchingComponent.type.name, matchingComponent.fullName); | ||
elementMap.set(key, { | ||
...elementMap.get(key), | ||
modified: true, | ||
origin: 'remote', | ||
filenames: [matchingComponent.xml as string, ...matchingComponent.walkContent()].filter((filename) => filename), | ||
}); | ||
} | ||
} | ||
|
||
return Array.from(elementMap.values()); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* | ||
* 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 { Logger } from '@salesforce/core'; | ||
import { isString } from '@salesforce/ts-types'; | ||
import { MetadataResolver, VirtualTreeContainer, ForceIgnore } from '@salesforce/source-deploy-retrieve'; | ||
import { ChangeResult } from './types'; | ||
import { sourceComponentGuard } from './guards'; | ||
import { ensureRelative } from './functions'; | ||
const logger = Logger.childFromRoot('SourceTracking.PopulateTypesAndNames'); | ||
|
||
/** | ||
* uses SDR to translate remote metadata records into local file paths (which only typically have the filename). | ||
* | ||
* @input elements: ChangeResult[] | ||
* @input projectPath | ||
* @input forceIgnore: ForceIgnore. If provided, result will indicate whether the file is ignored | ||
* @input excludeUnresolvable: boolean Filter out components where you can't get the name and type (that is, it's probably not a valid source component) | ||
* @input resolveDeleted: constructs a virtualTree instead of the actual filesystem--useful when the files no longer exist | ||
*/ | ||
export const populateTypesAndNames = ({ | ||
elements, | ||
projectPath, | ||
forceIgnore, | ||
excludeUnresolvable = false, | ||
resolveDeleted = false, | ||
}: { | ||
elements: ChangeResult[]; | ||
projectPath: string; | ||
forceIgnore?: ForceIgnore; | ||
excludeUnresolvable?: boolean; | ||
resolveDeleted?: boolean; | ||
}): ChangeResult[] => { | ||
if (elements.length === 0) { | ||
return []; | ||
} | ||
|
||
logger.debug(`populateTypesAndNames for ${elements.length} change elements`); | ||
const filenames = elements.flatMap((element) => element.filenames).filter(isString); | ||
|
||
// component set generated from the filenames on all local changes | ||
const resolver = new MetadataResolver( | ||
undefined, | ||
resolveDeleted ? VirtualTreeContainer.fromFilePaths(filenames) : undefined, | ||
!!forceIgnore | ||
); | ||
const sourceComponents = filenames | ||
.flatMap((filename) => { | ||
try { | ||
return resolver.getComponentsFromPath(filename); | ||
} catch (e) { | ||
logger.warn(`unable to resolve ${filename}`); | ||
return undefined; | ||
} | ||
}) | ||
.filter(sourceComponentGuard); | ||
|
||
logger.debug(` matching SourceComponents have ${sourceComponents.length} items from local`); | ||
|
||
// make it simpler to find things later | ||
const elementMap = new Map<string, ChangeResult>(); | ||
elements.map((element) => { | ||
element.filenames?.map((filename) => { | ||
elementMap.set(ensureRelative(filename, projectPath), element); | ||
}); | ||
}); | ||
|
||
// iterates the local components and sets their filenames | ||
sourceComponents.map((matchingComponent) => { | ||
if (matchingComponent?.fullName && matchingComponent?.type.name) { | ||
const filenamesFromMatchingComponent = [matchingComponent.xml, ...matchingComponent.walkContent()]; | ||
const ignored = filenamesFromMatchingComponent | ||
.filter(isString) | ||
.filter((filename) => !filename.includes('__tests__')) | ||
.some((filename) => forceIgnore?.denies(filename)); | ||
filenamesFromMatchingComponent.map((filename) => { | ||
if (filename && elementMap.has(filename)) { | ||
// add the type/name from the componentSet onto the element | ||
elementMap.set(filename, { | ||
origin: 'remote', | ||
...elementMap.get(filename), | ||
type: matchingComponent.type.name, | ||
name: matchingComponent.fullName, | ||
ignored, | ||
}); | ||
} | ||
}); | ||
} | ||
}); | ||
return excludeUnresolvable | ||
? Array.from(new Set(elementMap.values())).filter((changeResult) => changeResult.name && changeResult.type) | ||
: Array.from(new Set(elementMap.values())); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.