mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-13 11:38:36 -05:00
Merge from vscode 817eb6b0c720a4ecbc13c020afbbebfed667aa09 (#7356)
This commit is contained in:
329
src/vs/platform/userDataSync/common/extensionsSync.ts
Normal file
329
src/vs/platform/userDataSync/common/extensionsSync.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { keys, values } from 'vs/base/common/map';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { Queue } from 'vs/base/common/async';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export interface ISyncPreviewResult {
|
||||
readonly added: ISyncExtension[];
|
||||
readonly removed: ISyncExtension[];
|
||||
readonly updated: ISyncExtension[];
|
||||
readonly remote: ISyncExtension[] | null;
|
||||
}
|
||||
|
||||
export class ExtensionsSynchroniser extends Disposable implements ISynchroniser {
|
||||
|
||||
private static EXTERNAL_USER_DATA_EXTENSIONS_KEY: string = 'extensions';
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Idle;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
|
||||
|
||||
private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
|
||||
|
||||
private readonly lastSyncExtensionsResource: URI;
|
||||
private readonly replaceQueue: Queue<void>;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
this.replaceQueue = this._register(new Queue());
|
||||
this.lastSyncExtensionsResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncExtensions');
|
||||
this._register(
|
||||
Event.debounce(
|
||||
Event.any(
|
||||
Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
|
||||
Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error))),
|
||||
() => undefined, 500)(() => this._onDidChangeLocal.fire()));
|
||||
}
|
||||
|
||||
private setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
}
|
||||
}
|
||||
|
||||
async sync(): Promise<boolean> {
|
||||
if (!this.configurationService.getValue<boolean>('userConfiguration.syncExtensions')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
try {
|
||||
await this.doSync();
|
||||
} catch (e) {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Failed to Synchronise extensions as there is a new remote version available. Synchronising again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
return true;
|
||||
}
|
||||
|
||||
async getRemoteExtensions(): Promise<ISyncExtension[]> {
|
||||
const remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, null);
|
||||
return remoteData.content ? JSON.parse(remoteData.content) : [];
|
||||
}
|
||||
|
||||
removeExtension(identifier: IExtensionIdentifier): Promise<void> {
|
||||
return this.replaceQueue.queue(async () => {
|
||||
const remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, null);
|
||||
const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : [];
|
||||
const removedExtensions = remoteExtensions.filter(e => areSameExtensions(e.identifier, identifier));
|
||||
if (removedExtensions.length) {
|
||||
for (const removedExtension of removedExtensions) {
|
||||
remoteExtensions.splice(remoteExtensions.indexOf(removedExtension), 1);
|
||||
}
|
||||
await this.writeToRemote(remoteExtensions, remoteData.ref);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async doSync(): Promise<void> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
let remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData);
|
||||
|
||||
const lastSyncExtensions: ISyncExtension[] = lastSyncData ? JSON.parse(lastSyncData.content!) : null;
|
||||
const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : null;
|
||||
const localExtensions = await this.getLocalExtensions();
|
||||
|
||||
const { added, removed, updated, remote } = this.merge(localExtensions, remoteExtensions, lastSyncExtensions);
|
||||
|
||||
// update local
|
||||
await this.updateLocalExtensions(added, removed, updated);
|
||||
|
||||
if (remote) {
|
||||
// update remote
|
||||
remoteData = await this.writeToRemote(remote, remoteData.ref);
|
||||
}
|
||||
|
||||
// update last sync
|
||||
await this.updateLastSyncValue(remoteData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge Strategy:
|
||||
* - If remote does not exist, merge with local (First time sync)
|
||||
* - Overwrite local with remote changes. Removed, Added, Updated.
|
||||
* - Update remote with those local extension which are newly added or updated or removed and untouched in remote.
|
||||
*/
|
||||
private merge(localExtensions: ISyncExtension[], remoteExtensions: ISyncExtension[] | null, lastSyncExtensions: ISyncExtension[] | null): { added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], remote: ISyncExtension[] | null } {
|
||||
|
||||
// First time sync
|
||||
if (!remoteExtensions) {
|
||||
return { added: [], removed: [], updated: [], remote: localExtensions };
|
||||
}
|
||||
|
||||
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));
|
||||
if (lastSyncExtensions) {
|
||||
lastSyncExtensions.forEach(({ identifier }) => addUUID(identifier));
|
||||
}
|
||||
|
||||
const addExtensionToMap = (map: Map<string, ISyncExtension>, extension: ISyncExtension) => {
|
||||
const uuid = extension.identifier.uuid || uuids.get(extension.identifier.id.toLowerCase());
|
||||
const key = uuid ? `uuid:${uuid}` : `id:${extension.identifier.id.toLowerCase()}`;
|
||||
map.set(key, extension);
|
||||
return map;
|
||||
};
|
||||
const localExtensionsMap = localExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
|
||||
const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
|
||||
const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
|
||||
const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
|
||||
|
||||
const localToRemote = this.compare(localExtensionsMap, remoteExtensionsMap);
|
||||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return { added: [], removed: [], updated: [], remote: null };
|
||||
}
|
||||
|
||||
const added: ISyncExtension[] = [];
|
||||
const removed: IExtensionIdentifier[] = [];
|
||||
const updated: ISyncExtension[] = [];
|
||||
|
||||
const baseToLocal = lastSyncExtensionsMap ? this.compare(lastSyncExtensionsMap, localExtensionsMap) : { added: keys(localExtensionsMap).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToRemote = lastSyncExtensionsMap ? this.compare(lastSyncExtensionsMap, remoteExtensionsMap) : { added: keys(remoteExtensionsMap).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
|
||||
const massageSyncExtension = (extension: ISyncExtension, key: string): ISyncExtension => {
|
||||
return {
|
||||
identifier: {
|
||||
id: extension.identifier.id,
|
||||
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
|
||||
},
|
||||
enabled: extension.enabled,
|
||||
version: extension.version
|
||||
};
|
||||
};
|
||||
|
||||
// Remotely removed extension.
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
const e = localExtensionsMap.get(key);
|
||||
if (e) {
|
||||
removed.push(e.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Remotely added extension
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Is different from local to remote
|
||||
if (localToRemote.updated.has(key)) {
|
||||
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
|
||||
}
|
||||
} else {
|
||||
// Add to local
|
||||
added.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
|
||||
}
|
||||
}
|
||||
|
||||
// Remotely updated extensions
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
// If updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
// Is different from local to remote
|
||||
if (localToRemote.updated.has(key)) {
|
||||
// update it in local
|
||||
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Locally added extensions
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
// Not there in remote
|
||||
if (!baseToRemote.added.has(key)) {
|
||||
newRemoteExtensionsMap.set(key, massageSyncExtension(localExtensionsMap.get(key)!, key));
|
||||
}
|
||||
}
|
||||
|
||||
// Locally updated extensions
|
||||
for (const key of values(baseToLocal.updated)) {
|
||||
// If removed in remote
|
||||
if (baseToRemote.removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If not updated in remote
|
||||
if (!baseToRemote.updated.has(key)) {
|
||||
newRemoteExtensionsMap.set(key, massageSyncExtension(localExtensionsMap.get(key)!, key));
|
||||
}
|
||||
}
|
||||
|
||||
// Locally removed extensions
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
// If not updated in remote
|
||||
if (!baseToRemote.updated.has(key)) {
|
||||
newRemoteExtensionsMap.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
const remoteChanges = this.compare(remoteExtensionsMap, newRemoteExtensionsMap);
|
||||
const remote = remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0 ? values(newRemoteExtensionsMap) : null;
|
||||
return { added, removed, updated, remote };
|
||||
}
|
||||
|
||||
private compare(from: Map<string, ISyncExtension>, to: Map<string, ISyncExtension>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = keys(from);
|
||||
const toKeys = keys(to);
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).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 fromExtension = from.get(key)!;
|
||||
const toExtension = to.get(key);
|
||||
if (!toExtension
|
||||
|| fromExtension.enabled !== toExtension.enabled
|
||||
|| fromExtension.version !== toExtension.version
|
||||
) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
private async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[]): Promise<void> {
|
||||
if (removed.length) {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const extensionsToRemove = installedExtensions.filter(({ identifier }) => removed.some(r => areSameExtensions(identifier, r)));
|
||||
await Promise.all(extensionsToRemove.map(e => this.extensionManagementService.uninstall(e)));
|
||||
}
|
||||
|
||||
if (added.length || updated.length) {
|
||||
await Promise.all([...added, ...updated].map(async e => {
|
||||
const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier, e.version);
|
||||
if (extension) {
|
||||
await this.extensionManagementService.installFromGallery(extension);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private async getLocalExtensions(): Promise<ISyncExtension[]> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
return installedExtensions.map(({ identifier }) => ({ identifier, enabled: true }));
|
||||
}
|
||||
|
||||
private async getLastSyncUserData(): Promise<IUserData | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncExtensionsResource);
|
||||
return JSON.parse(content.value.toString());
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async writeToRemote(extensions: ISyncExtension[], ref: string | null): Promise<IUserData> {
|
||||
const content = JSON.stringify(extensions);
|
||||
ref = await this.userDataSyncStoreService.write(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, content, ref);
|
||||
return { content, ref };
|
||||
}
|
||||
|
||||
private async updateLastSyncValue(remoteUserData: IUserData): Promise<void> {
|
||||
await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
@@ -47,6 +49,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@ISettingsMergeService private readonly settingsMergeService: ISettingsMergeService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
this.lastSyncSettingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncSettings.json');
|
||||
@@ -135,7 +138,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
|
||||
|
||||
let { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
|
||||
if (hasRemoteChanged) {
|
||||
const ref = await this.writeToRemote(content, remoteUserData.ref);
|
||||
const remoteContent = remoteUserData.content ? await this.settingsMergeService.computeRemoteContent(content, remoteUserData.content, this.getIgnoredSettings()) : content;
|
||||
const ref = await this.writeToRemote(remoteContent, remoteUserData.ref);
|
||||
remoteUserData = { ref, content };
|
||||
}
|
||||
if (hasLocalChanged) {
|
||||
@@ -176,45 +180,40 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
|
||||
// First time sync to remote
|
||||
if (fileContent && !remoteContent) {
|
||||
this.logService.trace('Settings Sync: Remote contents does not exist. So sync with settings file.');
|
||||
hasRemoteChanged = true;
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(fileContent.value.toString()));
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
// Settings file does not exist, so sync with remote contents.
|
||||
if (remoteContent && !fileContent) {
|
||||
this.logService.trace('Settings Sync: Settings file does not exist. So sync with remote contents');
|
||||
hasLocalChanged = true;
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(remoteContent));
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
if (fileContent && remoteContent) {
|
||||
const localContent: string = fileContent.value.toString();
|
||||
if (remoteContent) {
|
||||
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
|
||||
if (!lastSyncData // First time sync
|
||||
|| lastSyncData.content !== localContent // Local has moved forwarded
|
||||
|| lastSyncData.content !== remoteContent // Remote has moved forwarded
|
||||
) {
|
||||
this.logService.trace('Settings Sync: Merging remote contents with settings file.');
|
||||
const result = await this.settingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null);
|
||||
const result = await this.settingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, this.getIgnoredSettings());
|
||||
// Sync only if there are changes
|
||||
if (result.hasChanges) {
|
||||
hasLocalChanged = result.mergeContent !== localContent;
|
||||
hasRemoteChanged = result.mergeContent !== remoteContent;
|
||||
hasConflicts = result.hasConflicts;
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(result.mergeContent));
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
}
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
// First time sync to remote
|
||||
if (fileContent) {
|
||||
this.logService.trace('Settings Sync: Remote contents does not exist. So sync with settings file.');
|
||||
hasRemoteChanged = true;
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(fileContent.value.toString()));
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
this.logService.trace('Settings Sync: No changes.');
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
private getIgnoredSettings(): IStringDictionary<boolean> {
|
||||
return this.configurationService.getValue<IStringDictionary<boolean>>('userConfiguration.ignoreSettings');
|
||||
}
|
||||
|
||||
private async getLastSyncUserData(): Promise<IUserData | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncSettingsResource);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ISettingsMergeService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
|
||||
export class SettingsMergeChannel implements IServerChannel {
|
||||
|
||||
@@ -17,7 +18,8 @@ export class SettingsMergeChannel implements IServerChannel {
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'merge': return this.service.merge(args[0], args[1], args[2]);
|
||||
case 'merge': return this.service.merge(args[0], args[1], args[2], args[3]);
|
||||
case 'computeRemoteContent': return this.service.computeRemoteContent(args[0], args[1], args[2]);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
@@ -30,8 +32,12 @@ export class SettingsMergeChannelClient implements ISettingsMergeService {
|
||||
constructor(private readonly channel: IChannel) {
|
||||
}
|
||||
|
||||
merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> {
|
||||
return this.channel.call('merge', [localContent, remoteContent, baseContent]);
|
||||
merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: IStringDictionary<boolean>): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> {
|
||||
return this.channel.call('merge', [localContent, remoteContent, baseContent, ignoredSettings]);
|
||||
}
|
||||
|
||||
computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: IStringDictionary<boolean>): Promise<string> {
|
||||
return this.channel.call('computeRemoteContent', [localContent, remoteContent, ignoredSettings]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,54 @@
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export function registerConfiguration() {
|
||||
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
|
||||
.registerConfiguration({
|
||||
id: 'userConfiguration',
|
||||
order: 30,
|
||||
title: localize('userConfiguration', "User Configuration"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'userConfiguration.enableSync': {
|
||||
type: 'boolean',
|
||||
description: localize('userConfiguration.enableSync', "When enabled, synchronises User Configuration: Settings, Keybindings, Extensions & Snippets."),
|
||||
default: true,
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
},
|
||||
'userConfiguration.syncExtensions': {
|
||||
type: 'boolean',
|
||||
description: localize('userConfiguration.syncExtensions', "When enabled extensions are synchronised while synchronising user configuration."),
|
||||
default: true,
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
},
|
||||
'userConfiguration.ignoreSettings': {
|
||||
'type': 'object',
|
||||
description: localize('userConfiguration.ignoreSettings', "Configure settings to be ignored while syncing"),
|
||||
'default': {
|
||||
'userConfiguration.enableSync': true,
|
||||
'userConfiguration.syncExtensions': true,
|
||||
'userConfiguration.ignoreSettings': true
|
||||
},
|
||||
'scope': ConfigurationScope.APPLICATION,
|
||||
'additionalProperties': {
|
||||
'anyOf': [
|
||||
{
|
||||
'type': 'boolean',
|
||||
'description': localize('ignoredSetting', "Id of the stting to be ignored. Set to true or false to enable or disable."),
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export interface IUserData {
|
||||
ref: string;
|
||||
@@ -40,12 +88,18 @@ export interface IUserDataSyncStoreService {
|
||||
write(key: string, content: string, ref: string | null): Promise<string>;
|
||||
}
|
||||
|
||||
export enum SyncSource {
|
||||
export interface ISyncExtension {
|
||||
identifier: IExtensionIdentifier;
|
||||
version?: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export const enum SyncSource {
|
||||
Settings = 1,
|
||||
Extensions
|
||||
}
|
||||
|
||||
export enum SyncStatus {
|
||||
export const enum SyncStatus {
|
||||
Uninitialized = 'uninitialized',
|
||||
Idle = 'idle',
|
||||
Syncing = 'syncing',
|
||||
@@ -66,6 +120,9 @@ export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUser
|
||||
export interface IUserDataSyncService extends ISynchroniser {
|
||||
_serviceBrand: any;
|
||||
readonly conflictsSource: SyncSource | null;
|
||||
|
||||
getRemoteExtensions(): Promise<ISyncExtension[]>;
|
||||
removeExtension(identifier: IExtensionIdentifier): Promise<void>;
|
||||
}
|
||||
|
||||
export const ISettingsMergeService = createDecorator<ISettingsMergeService>('ISettingsMergeService');
|
||||
@@ -74,6 +131,10 @@ export interface ISettingsMergeService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }>;
|
||||
merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: IStringDictionary<boolean>): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }>;
|
||||
|
||||
computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: IStringDictionary<boolean>): Promise<string>;
|
||||
|
||||
}
|
||||
|
||||
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
|
||||
|
||||
@@ -22,7 +22,10 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'sync': return this.service.sync(args[0]);
|
||||
case '_getInitialStatus': return Promise.resolve(this.service.status);
|
||||
case 'getConflictsSource': return Promise.resolve(this.service.conflictsSource);
|
||||
case 'getRemoteExtensions': return this.service.getRemoteExtensions();
|
||||
case 'removeExtension': return this.service.removeExtension(args[0]);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync';
|
||||
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
|
||||
|
||||
@@ -27,14 +29,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
private _conflictsSource: SyncSource | null = null;
|
||||
get conflictsSource(): SyncSource | null { return this._conflictsSource; }
|
||||
|
||||
private readonly settingsSynchroniser: SettingsSynchroniser;
|
||||
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
this.synchronisers = [
|
||||
this.instantiationService.createInstance(SettingsSynchroniser)
|
||||
];
|
||||
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
|
||||
this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser));
|
||||
this.synchronisers = [this.settingsSynchroniser, this.extensionsSynchroniser];
|
||||
this.updateStatus();
|
||||
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
|
||||
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal));
|
||||
@@ -52,6 +57,14 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return true;
|
||||
}
|
||||
|
||||
getRemoteExtensions(): Promise<ISyncExtension[]> {
|
||||
return this.extensionsSynchroniser.getRemoteExtensions();
|
||||
}
|
||||
|
||||
removeExtension(identifier: IExtensionIdentifier): Promise<void> {
|
||||
return this.extensionsSynchroniser.removeExtension(identifier);
|
||||
}
|
||||
|
||||
private updateStatus(): void {
|
||||
this._conflictsSource = this.computeConflictsSource();
|
||||
this.setStatus(this.computeStatus());
|
||||
@@ -121,8 +134,7 @@ export class UserDataAutoSync extends Disposable {
|
||||
}
|
||||
|
||||
private isSyncEnabled(): boolean {
|
||||
const { user: userLocal } = this.configurationService.inspect<boolean>('userConfiguration.enableSync');
|
||||
return userLocal === undefined || userLocal;
|
||||
return this.configurationService.getValue<boolean>('userConfiguration.enableSync');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
return Promise.reject(new Error('No settings sync store url configured.'));
|
||||
}
|
||||
|
||||
const url = joinPath(URI.parse(this.productService.settingsSyncStoreUrl!), key).toString();
|
||||
const url = joinPath(URI.parse(this.productService.settingsSyncStoreUrl!), 'resource', key, 'latest').toString();
|
||||
const headers: IHeaders = {};
|
||||
if (oldValue) {
|
||||
headers['If-None-Match'] = oldValue.ref;
|
||||
@@ -68,7 +68,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
return Promise.reject(new Error('No settings sync store url configured.'));
|
||||
}
|
||||
|
||||
const url = joinPath(URI.parse(this.productService.settingsSyncStoreUrl!), key).toString();
|
||||
const url = joinPath(URI.parse(this.productService.settingsSyncStoreUrl!), 'resource', key).toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'text/plain' };
|
||||
if (ref) {
|
||||
headers['If-Match'] = ref;
|
||||
|
||||
Reference in New Issue
Block a user