src/vs/platform/userDataSync/common/extensionsMerge.ts
428 LOC · 384 covered · 44 uncovered · 137 ranges · 689 concepts · 64 introducers · 421 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
extensionsMerge.ts ×8
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IStringDictionary } from '../../../base/common/collections.js';
import { deepClone, equals } from '../../../base/common/objects.js';
import * as semver from '../../../base/common/semver/semver.js';
import { assertReturnsDefined } from '../../../base/common/types.js';
import { IExtensionIdentifier } from '../../extensions/common/extensions.js';
import { ILocalSyncExtension, IRemoteSyncExtension, ISyncExtension } from './userDataSync.js';
export interface IMergeResult {
readonly local: { added: ISyncExtension[]; removed: IExtensionIdentifier[]; updated: ISyncExtension[] };
readonly remote: { added: ISyncExtension[]; removed: ISyncExtension[]; updated: ISyncExtension[]; all: ISyncExtension[] } | null;
}
export function merge(localExtensions: ILocalSyncExtension[], remoteExtensions: IRemoteSyncExtension[] | null, lastSyncExtensions: IRemoteSyncExtension[] | null, skippedExtensions: ISyncExtension[], ignoredExtensions: string[], lastSyncBuiltinExtensions: IExtensionIdentifier[] | null): IMergeResult {
const removed: IExtensionIdentifier[] = [];
const updated: ISyncExtension[] = [];
if (!remoteExtensions) {
const remote = localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase()));
extensionsMerge.ts ×2
return {
local: {
added,
removed,
updated,
},
remote: remote.length > 0 ? {
updated: [],
removed: [],
all: remote
};
}
localExtensions = localExtensions.map(massageIncomingExtension) as ILocalSyncExtension[];
remoteExtensions = remoteExtensions.map(massageIncomingExtension);
lastSyncExtensions = lastSyncExtensions ? lastSyncExtensions.map(massageIncomingExtension) : null;
extensionsMerge.ts ×8
const uuids: Map<string, string> = new Map<string, string>();
const addUUID = (identifier: IExtensionIdentifier) => { if (identifier.uuid) { uuids.set(identifier.id.toLowerCase(), identifier.uuid); } };
localExtensions.forEach(({ identifier }) => addUUID(identifier));
remoteExtensions.forEach(({ identifier }) => addUUID(identifier));
lastSyncExtensions?.forEach(({ identifier }) => addUUID(identifier));
skippedExtensions?.forEach(({ identifier }) => addUUID(identifier));
lastSyncBuiltinExtensions?.forEach(identifier => addUUID(identifier));
const getKey = (extension: ISyncExtension): string => {
const uuid = extension.identifier.uuid || uuids.get(extension.identifier.id.toLowerCase());
extensionsMerge.ts ×9
return uuid ? `uuid:${uuid}` : `id:${extension.identifier.id.toLowerCase()}`;
};
const addExtensionToMap = (map: Map<string, ISyncExtension>, extension: ISyncExtension) => {
extensionsMerge.ts ×8
return map;
};
const localExtensionsMap: Map<string, ISyncExtension> = localExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
extensionsMerge.ts ×8
const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce((map: Map<string, ISyncExtension>, extension: ISyncExtension) => addExtensionToMap(map, deepClone(extension)), new Map<string, ISyncExtension>());
const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => {
return set.add(uuid ? `uuid:${uuid}` : `id:${id.toLowerCase()}`);
const lastSyncBuiltinExtensionsSet = lastSyncBuiltinExtensions ? lastSyncBuiltinExtensions.reduce((set, { id, uuid }) => {
return set.add(uuid ? `uuid:${uuid}` : `id:${id.toLowerCase()}`);
const localToRemote = compare(localExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet, false);
if (localToRemote.added.size > 0 || localToRemote.removed.size > 0 || localToRemote.updated.size > 0) {
const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet, false);
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet, true);
const merge = (key: string, localExtension: ISyncExtension, remoteExtension: ISyncExtension, preferred: ISyncExtension): ISyncExtension => {
let pinned: boolean | undefined, version: string | undefined, preRelease: boolean | undefined;
extensionsMerge.ts ×10
if (localExtension.installed) {
preRelease = preferred.preRelease;
if (pinned) {
}
preRelease = remoteExtension.preRelease;
if (pinned) {
}
pinned = localExtension.pinned;
if (pinned) {
version = localExtension.version;
}
}
preRelease = localExtension.preRelease;
}
...preferred,
installed: localExtension.installed || remoteExtension.installed,
pinned,
preRelease,
version: version ?? (remoteExtension.version && (!localExtension.installed || semver.gt(remoteExtension.version, localExtension.version)) ? remoteExtension.version : localExtension.version),
state: mergeExtensionState(localExtension, remoteExtension, lastSyncExtensionsMap?.get(key)),
};
};
// Remotely removed extension => exist in base and does not in remote
for (const key of baseToRemote.removed.values()) {
if (!localExtension) {
}
const baseExtension = assertReturnsDefined(lastSyncExtensionsMap?.get(key));
extensionsMerge.ts ×3
const wasAnInstalledExtensionDuringLastSync = lastSyncBuiltinExtensionsSet && !lastSyncBuiltinExtensionsSet.has(key) && baseExtension.installed;
if (localExtension.installed && wasAnInstalledExtensionDuringLastSync /* It is an installed extension now and during last sync */) {
removed.push(localExtension.identifier);
// Add to remote: It is a builtin extenision or got installed after last sync
extensionsMerge.ts ×1
newRemoteExtensionsMap.set(key, localExtension);
}
}
// Remotely added extension => does not exist in base and exist in remote
for (const key of baseToRemote.added.values()) {
const remoteExtension = assertReturnsDefined(remoteExtensionsMap.get(key));
extensionsMerge.ts ×3
const localExtension = localExtensionsMap.get(key);
// Also exist in local
if (localExtension) {
if (localToRemote.updated.has(key)) {
const mergedExtension = merge(key, localExtension, remoteExtension, remoteExtension);
extensionsMerge.ts ×2
// Update locally only when the extension has changes in properties other than installed poperty
if (!areSame(localExtension, remoteExtension, false, false)) {
}
}
if (remoteExtension.installed) {
}
// Remotely updated extension => exist in base and remote
for (const key of baseToRemote.updated.values()) {
const remoteExtension = assertReturnsDefined(remoteExtensionsMap.get(key));
extensionsMerge.ts ×2
const baseExtension = assertReturnsDefined(lastSyncExtensionsMap?.get(key));
const localExtension = localExtensionsMap.get(key);
// Also exist in local
if (localExtension) {
const wasAnInstalledExtensionDuringLastSync = lastSyncBuiltinExtensionsSet && !lastSyncBuiltinExtensionsSet.has(key) && baseExtension.installed;
extensionsMerge.ts ×3
if (wasAnInstalledExtensionDuringLastSync && localExtension.installed && !remoteExtension.installed) {
removed.push(localExtension.identifier);
const mergedExtension = merge(key, localExtension, remoteExtension, remoteExtension);
updated.push(massageOutgoingExtension(mergedExtension, key));
newRemoteExtensionsMap.set(key, mergedExtension);
}
else if (remoteExtension.installed) {
added.push(massageOutgoingExtension(remoteExtension, key));
}
}
// Locally added extension => does not exist in base and exist in local
for (const key of baseToLocal.added.values()) {
if (baseToRemote.added.has(key)) {
}
newRemoteExtensionsMap.set(key, assertReturnsDefined(localExtensionsMap.get(key)));
extensionsMerge.ts ×1
}
// Locally updated extension => exist in base and local
for (const key of baseToLocal.updated.values()) {
if (baseToRemote.removed.has(key)) {
}
if (baseToRemote.updated.has(key)) {
}
const localExtension = assertReturnsDefined(localExtensionsMap.get(key));
extensionsMerge.ts ×1
const remoteExtension = assertReturnsDefined(remoteExtensionsMap.get(key));
// Update remotely
newRemoteExtensionsMap.set(key, merge(key, localExtension, remoteExtension, localExtension));
}
// Locally removed extensions => exist in base and does not exist in local
for (const key of baseToLocal.removed.values()) {
if (baseToRemote.updated.has(key)) {
}
if (baseToRemote.removed.has(key)) {
}
if (skippedExtensionsMap.has(key)) {
}
if (!assertReturnsDefined(remoteExtensionsMap.get(key)).installed) {
}
if (!lastSyncBuiltinExtensionsSet) {
continue;
}
if (lastSyncBuiltinExtensionsSet.has(key) || !assertReturnsDefined(lastSyncExtensionsMap?.get(key)).installed) {
extensionsMerge.ts ×2
}
}
const remote: ISyncExtension[] = [];
const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>(), true);
const hasRemoteChanges = remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0;
extensionsMerge.ts ×8
if (hasRemoteChanges) {
newRemoteExtensionsMap.forEach((value, key) => remote.push(massageOutgoingExtension(value, key)));
extensionsMerge.ts ×2
}
return {
local: { added, removed, updated },
remote: hasRemoteChanges ? {
added: [...remoteChanges.added].map(id => newRemoteExtensionsMap.get(id)!),
extensionsMerge.ts ×2
updated: [...remoteChanges.updated].map(id => newRemoteExtensionsMap.get(id)!),
removed: [...remoteChanges.removed].map(id => remoteExtensionsMap.get(id)!),
all: remote
};
}
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>, checkVersionProperty: boolean): { added: Set<string>; removed: Set<string>; updated: Set<string> } {
extensionsMerge.ts ×9
const fromKeys = from ? [...from.keys()].filter(key => !ignoredExtensions.has(key)) : [];
const toKeys = [...to.keys()].filter(key => !ignoredExtensions.has(key));
const added = toKeys.filter(key => !fromKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => !toKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
for (const key of fromKeys) {
if (removed.has(key)) {
}
const toExtension = to.get(key);
if (!toExtension || !areSame(fromExtension, toExtension, checkVersionProperty, true)) {
extensionsMerge.ts ×9
}
return { added, removed, updated };
}
function areSame(fromExtension: ISyncExtension, toExtension: ISyncExtension, checkVersionProperty: boolean, checkInstalledProperty: boolean): boolean {
extensionsMerge.ts ×6
if (fromExtension.disabled !== toExtension.disabled) {
return false;
}
if (!!fromExtension.isApplicationScoped !== !!toExtension.isApplicationScoped) {
return false;
}
if (checkInstalledProperty && fromExtension.installed !== toExtension.installed) {
return false;
}
if (fromExtension.preRelease !== toExtension.preRelease) {
return false;
}
if (fromExtension.pinned !== toExtension.pinned) {
return false;
}
if (toExtension.pinned && fromExtension.version !== toExtension.version) {
return false;
}
if (!isSameExtensionState(fromExtension.state, toExtension.state)) {
return false;
}
if ((checkVersionProperty && fromExtension.version !== toExtension.version)) {
extensionsMerge.ts ×6
/* extension version changed */
return false;
}
return true;
}
function mergeExtensionState(localExtension: ISyncExtension, remoteExtension: ISyncExtension, lastSyncExtension: ISyncExtension | undefined): IStringDictionary<any> | undefined {
extensionsMerge.ts ×10
const localState = localExtension.state;
const remoteState = remoteExtension.state;
const baseState = lastSyncExtension?.state;
// If remote extension has no version, use local state
if (!remoteExtension.version) {
return localState;
}
// If local state exists and local extension is latest then use local state
if (localState && semver.gt(localExtension.version, remoteExtension.version)) {
return localState;
}
// If remote state exists and remote extension is latest, use remote state
extensionsMerge.ts ×10
if (remoteState && semver.gt(remoteExtension.version, localExtension.version)) {
return remoteState;
}
/* Remote and local are on same version */
// If local state is not yet set, use remote state
if (!localState) {
return remoteState;
}
// If remote state is not yet set, use local state
if (!remoteState) {
return localState;
}
const mergedState: IStringDictionary<any> = deepClone(localState);
const baseToRemote = baseState ? compareExtensionState(baseState, remoteState) : { added: Object.keys(remoteState).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
extensionsMerge.ts ×10
const baseToLocal = baseState ? compareExtensionState(baseState, localState) : { added: Object.keys(localState).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
// Added/Updated in remote
for (const key of [...baseToRemote.added.values(), ...baseToRemote.updated.values()]) {
mergedState[key] = remoteState[key];
}
// Removed in remote
for (const key of baseToRemote.removed.values()) {
// Not updated in local
if (!baseToLocal.updated.has(key)) {
delete mergedState[key];
}
}
return mergedState;
}
function compareExtensionState(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>; removed: Set<string>; updated: Set<string> } {
extensionsMerge.ts ×5
const fromKeys = Object.keys(from);
const toKeys = Object.keys(to);
const added = toKeys.filter(key => !fromKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => !toKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
for (const key of fromKeys) {
if (removed.has(key)) {
continue;
}
const value1 = from[key];
const value2 = to[key];
if (!equals(value1, value2)) {
updated.add(key);
}
}
return { added, removed, updated };
}
function isSameExtensionState(a: IStringDictionary<any> = {}, b: IStringDictionary<any> = {}): boolean {
extensionsMerge.ts ×5
const { added, removed, updated } = compareExtensionState(a, b);
return added.size === 0 && removed.size === 0 && updated.size === 0;
}
// massage incoming extension - add optional properties
function massageIncomingExtension(extension: ISyncExtension): ISyncExtension {
extensionsMerge.ts ×9
return { ...extension, ...{ disabled: !!extension.disabled, installed: !!extension.installed } };
}
// massage outgoing extension - remove optional properties
function massageOutgoingExtension(extension: ISyncExtension, key: string): ISyncExtension {
extensionsMerge.ts ×5
const massagedExtension: ISyncExtension = {
...extension,
identifier: {
id: extension.identifier.id,
uuid: key.startsWith('uuid:') ? key.substring('uuid:'.length) : undefined
},
/* set following always so that to differentiate with older clients */
preRelease: !!extension.preRelease,
pinned: !!extension.pinned,
};
if (!extension.disabled) {
}
}
}
}
}