mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 21:55:38 +00:00
#100346 prepare for manual sync
This commit is contained in:
parent
ccd33c15d0
commit
c70ac014c3
|
@ -9,7 +9,8 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
|||
import { URI } from 'vs/base/common/uri';
|
||||
import {
|
||||
SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
|
||||
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview, IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME
|
||||
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview,
|
||||
IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
|
||||
|
@ -55,6 +56,13 @@ function isSyncData(thing: any): thing is ISyncData {
|
|||
export interface ISyncResourcePreview extends IBaseSyncResourcePreview {
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly resourcePreviews: IResourcePreview[];
|
||||
}
|
||||
|
||||
export interface IResourcePreview extends IBaseResourcePreview {
|
||||
readonly remoteContent: string | null;
|
||||
readonly localContent: string | null;
|
||||
readonly previewContent: string | null;
|
||||
}
|
||||
|
||||
export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
@ -127,7 +135,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData, CancellationToken.None)).hasRemoteChanged : true;
|
||||
const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true;
|
||||
if (hasRemoteChanged) {
|
||||
this._onDidChangeLocal.fire();
|
||||
}
|
||||
|
@ -153,7 +161,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
protected setConflicts(conflicts: Conflict[]) {
|
||||
private setConflicts(conflicts: Conflict[]) {
|
||||
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.local, b.local) && isEqual(a.remote, b.remote))) {
|
||||
this._conflicts = conflicts;
|
||||
this._onDidChangeConflicts.fire(this._conflicts);
|
||||
|
@ -176,7 +184,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
await this.applyPreview(preview, false);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
|
@ -199,7 +207,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
await this.applyPreview(preview, true);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, true);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`);
|
||||
|
||||
} finally {
|
||||
|
@ -267,7 +275,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
|
||||
const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData);
|
||||
await this.applyPreview(preview, false);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
|
@ -294,11 +302,11 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
return this.getRemoteUserData(lastSyncUserData);
|
||||
}
|
||||
|
||||
async generateSyncPreview(): Promise<ISyncResourcePreview | null> {
|
||||
async generateSyncResourcePreview(): Promise<ISyncResourcePreview | null> {
|
||||
if (this.isEnabled()) {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
return this.generatePreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
return this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -343,16 +351,16 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
try {
|
||||
// generate or use existing preview
|
||||
if (!this.syncPreviewPromise) {
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, token));
|
||||
}
|
||||
const preview = await this.syncPreviewPromise;
|
||||
|
||||
if (preview.hasConflicts) {
|
||||
if (preview.resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
|
||||
return SyncStatus.HasConflicts;
|
||||
}
|
||||
|
||||
// apply preview
|
||||
await this.applyPreview(preview, false);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview.resourcePreviews, false);
|
||||
|
||||
// reset preview
|
||||
this.syncPreviewPromise = null;
|
||||
|
@ -367,32 +375,80 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
protected async getSyncPreviewInProgress(): Promise<ISyncResourcePreview | null> {
|
||||
return this.syncPreviewPromise ? this.syncPreviewPromise : null;
|
||||
}
|
||||
|
||||
async acceptConflict(conflictUri: URI, conflictContent: string): Promise<void> {
|
||||
let preview = await this.getSyncPreviewInProgress();
|
||||
let preview = this.syncPreviewPromise ? await this.syncPreviewPromise : null;
|
||||
|
||||
if (!preview || !preview.hasConflicts) {
|
||||
if (!preview || !preview.resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.updatePreviewWithConflict(preview!, conflictUri, conflictContent, token));
|
||||
this.syncPreviewPromise = createCancelablePromise(async token => {
|
||||
const newPreview = await this.updateSyncResourcePreviewWithConflict(preview!, conflictUri, conflictContent, token);
|
||||
await this.updateConflicts(newPreview.resourcePreviews, token);
|
||||
return newPreview;
|
||||
});
|
||||
preview = await this.syncPreviewPromise;
|
||||
|
||||
if (!preview.hasConflicts) {
|
||||
|
||||
if (!preview.resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
|
||||
// apply preview
|
||||
await this.applyPreview(preview, false);
|
||||
await this.applyPreview(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews, false);
|
||||
|
||||
// reset preview
|
||||
this.syncPreviewPromise = null;
|
||||
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateSyncResourcePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, previewContent: string, token: CancellationToken): Promise<ISyncResourcePreview> {
|
||||
const conflict = this.conflicts.find(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource));
|
||||
if (!conflict) {
|
||||
return preview;
|
||||
}
|
||||
const index = preview.resourcePreviews.findIndex(({ previewResource }) => previewResource && isEqual(previewResource, conflict.local));
|
||||
if (index === -1) {
|
||||
return preview;
|
||||
}
|
||||
const resourcePreviews = [...preview.resourcePreviews];
|
||||
const resourcePreview = await this.updateResourcePreviewContent(resourcePreviews[index], conflictResource, previewContent, token);
|
||||
resourcePreviews[index] = resourcePreview;
|
||||
return {
|
||||
...preview,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
protected async updateResourcePreviewContent(resourcePreview: IResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IResourcePreview> {
|
||||
return {
|
||||
...resourcePreview,
|
||||
previewContent,
|
||||
hasConflicts: false,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
private async updateConflicts(resourcePreviews: IResourcePreview[], token: CancellationToken): Promise<void> {
|
||||
const conflicts: Conflict[] = [];
|
||||
for (const resourcePreview of resourcePreviews) {
|
||||
if (resourcePreview.hasConflicts) {
|
||||
conflicts.push({ local: resourcePreview.previewResource!, remote: resourcePreview.remoteResource! });
|
||||
}
|
||||
}
|
||||
|
||||
for (const conflict of this.conflicts) {
|
||||
// clear obsolete conflicts
|
||||
if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) {
|
||||
try {
|
||||
await this.fileService.del(conflict.local);
|
||||
} catch (error) {
|
||||
// Ignore & log
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setConflicts(conflicts);
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
|
@ -400,11 +456,6 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
return !!lastSyncData;
|
||||
}
|
||||
|
||||
protected async isLastSyncFromCurrentMachine(remoteUserData: IRemoteUserData): Promise<boolean> {
|
||||
const machineId = await this.currentMachineIdPromise;
|
||||
return !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
|
||||
}
|
||||
|
||||
async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
|
||||
const handles = await this.userDataSyncStoreService.getAllRefs(this.resource);
|
||||
return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) }));
|
||||
|
@ -447,12 +498,41 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
return null;
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(uri: URI): Promise<string | null> {
|
||||
const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null;
|
||||
if (syncPreview) {
|
||||
for (const resourcePreview of syncPreview.resourcePreviews) {
|
||||
if (resourcePreview.previewResource && isEqual(resourcePreview.previewResource, uri)) {
|
||||
return resourcePreview.previewContent || '';
|
||||
}
|
||||
if (resourcePreview.remoteResource && isEqual(resourcePreview.remoteResource, uri)) {
|
||||
return resourcePreview.remoteContent || '';
|
||||
}
|
||||
if (resourcePreview.localResource && isEqual(resourcePreview.localResource, uri)) {
|
||||
return resourcePreview.localContent || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.lastSyncResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview> {
|
||||
const machineId = await this.currentMachineIdPromise;
|
||||
const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
|
||||
|
||||
// For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine
|
||||
const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData;
|
||||
const resourcePreviews = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
|
||||
await this.updateConflicts(resourcePreviews, token);
|
||||
return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine };
|
||||
}
|
||||
|
||||
async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncResource);
|
||||
|
@ -531,21 +611,30 @@ export abstract class AbstractSynchroniser extends Disposable {
|
|||
this.syncPreviewPromise.cancel();
|
||||
this.syncPreviewPromise = null;
|
||||
}
|
||||
if (this.conflicts.length) {
|
||||
await Promise.all(this.conflicts.map(async ({ local }) => {
|
||||
try {
|
||||
this.fileService.del(local);
|
||||
} catch (error) {
|
||||
// Ignore & log
|
||||
this.logService.error(error);
|
||||
}
|
||||
}));
|
||||
this.setConflicts([]);
|
||||
}
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
protected abstract readonly version: number;
|
||||
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncResourcePreview>;
|
||||
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract applyPreview(preview: ISyncResourcePreview, forcePush: boolean): Promise<void>;
|
||||
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IResourcePreview[]>;
|
||||
protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IResourcePreview[], forcePush: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IFileSyncPreview extends ISyncResourcePreview {
|
||||
export interface IFileResourcePreview extends IResourcePreview {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly content: string | null;
|
||||
}
|
||||
|
||||
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
|
@ -568,28 +657,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
|||
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await super.stop();
|
||||
try {
|
||||
await this.fileService.del(this.localPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) {
|
||||
const syncPreview = await this.getSyncPreviewInProgress();
|
||||
if (syncPreview) {
|
||||
if (isEqual(this.remotePreviewResource, conflictResource)) {
|
||||
return syncPreview.remoteUserData && syncPreview.remoteUserData.syncData ? syncPreview.remoteUserData.syncData.content : null;
|
||||
}
|
||||
if (isEqual(this.localPreviewResource, conflictResource)) {
|
||||
return (syncPreview as IFileSyncPreview).fileContent ? (syncPreview as IFileSyncPreview).fileContent!.value.toString() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async getLocalFileContent(): Promise<IFileContent | null> {
|
||||
try {
|
||||
return await this.fileService.readFile(this.file);
|
||||
|
@ -624,8 +691,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
|||
this.triggerLocalChange();
|
||||
}
|
||||
|
||||
protected abstract readonly localPreviewResource: URI;
|
||||
protected abstract readonly remotePreviewResource: URI;
|
||||
}
|
||||
|
||||
export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import {
|
||||
IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService,
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
|
@ -14,8 +14,8 @@ import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/comm
|
|||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { merge, getIgnoredExtensions, IMergeResult } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources';
|
||||
|
@ -25,9 +25,8 @@ import { compare } from 'vs/base/common/strings';
|
|||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface IExtensionsSyncPreview extends ISyncResourcePreview {
|
||||
export interface IExtensionResourcePreview extends IResourcePreview {
|
||||
readonly localExtensions: ISyncExtension[];
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
readonly added: ISyncExtension[];
|
||||
readonly removed: IExtensionIdentifier[];
|
||||
readonly updated: ISyncExtension[];
|
||||
|
@ -74,84 +73,111 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
|||
() => undefined, 500)(() => this.triggerLocalChange()));
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionsSyncPreview> {
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionResourcePreview[]> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const localResource = ExtensionsSynchroniser.EXTENSIONS_DATA_URI;
|
||||
const localContent = this.format(localExtensions);
|
||||
const remoteResource = this.remotePreviewResource;
|
||||
const previewResource = this.localPreviewResource;
|
||||
const previewContent = null;
|
||||
const resourcePreviews: IExtensionResourcePreview[] = [];
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData);
|
||||
const mergeResult = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
added, removed, updated, remote, localExtensions, skippedExtensions: [],
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
resourcePreviews.push({
|
||||
localResource,
|
||||
localContent,
|
||||
remoteResource,
|
||||
remoteContent: this.format(remoteExtensions),
|
||||
previewResource,
|
||||
previewContent,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localExtensions,
|
||||
skippedExtensions: [],
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
resourcePreviews.push({
|
||||
localResource,
|
||||
localContent,
|
||||
remoteResource,
|
||||
remoteContent: null,
|
||||
previewResource,
|
||||
previewContent,
|
||||
added: [], removed: [], updated: [], remote: null, localExtensions, skippedExtensions: [],
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionsSyncPreview> {
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionResourcePreview[]> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const mergeResult = merge(localExtensions, null, null, [], ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
return [{
|
||||
localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
|
||||
localContent: this.format(localExtensions),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localExtensions,
|
||||
skippedExtensions: [],
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
|
||||
const syncExtensions = await this.parseAndMigrateExtensions(syncData);
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const mergeResult = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
return [{
|
||||
localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
|
||||
localContent: this.format(localExtensions),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote: syncExtensions,
|
||||
localExtensions,
|
||||
skippedExtensions: [],
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
|
||||
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
let lastSyncExtensions: ISyncExtension[] | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSyncExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData!);
|
||||
}
|
||||
} else {
|
||||
lastSyncExtensions = await this.parseAndMigrateExtensions(lastSyncUserData.syncData!);
|
||||
}
|
||||
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null;
|
||||
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
|
@ -165,36 +191,34 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
|||
|
||||
const mergeResult = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
|
||||
localContent: this.format(localExtensions),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
skippedExtensions,
|
||||
remoteUserData,
|
||||
localExtensions,
|
||||
lastSyncUserData,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
isLastSyncFromCurrentMachine,
|
||||
skippedExtensions,
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IExtensionsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<IExtensionsSyncPreview> {
|
||||
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
|
||||
}
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IExtensionResourcePreview[], forcePush: boolean): Promise<void> {
|
||||
let { added, removed, updated, remote, skippedExtensions, localExtensions, localChange, remoteChange } = resourcePreviews[0];
|
||||
|
||||
protected async applyPreview({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreview, forcePush: boolean): Promise<void> {
|
||||
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (localChange !== Change.None) {
|
||||
await this.backupLocal(JSON.stringify(localExtensions));
|
||||
skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
|
||||
}
|
||||
|
@ -215,18 +239,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
|||
}
|
||||
}
|
||||
|
||||
private getResourcePreviews({ added, removed, updated, remote }: IMergeResult): IResourcePreview[] {
|
||||
const hasLocalChanged = added.length > 0 || removed.length > 0 || updated.length > 0;
|
||||
const hasRemoteChanged = remote !== null;
|
||||
return [{
|
||||
hasLocalChanged,
|
||||
hasConflicts: false,
|
||||
hasRemoteChanged,
|
||||
localResouce: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
|
||||
remoteResource: this.remotePreviewResource
|
||||
}];
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }];
|
||||
}
|
||||
|
@ -238,6 +250,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
|||
return this.format(localExtensions);
|
||||
}
|
||||
|
||||
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import {
|
||||
IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService,
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
@ -14,9 +14,9 @@ import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources';
|
|||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { merge, IMergeResult } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
@ -30,12 +30,11 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
|||
const argvStoragePrefx = 'globalState.argv.';
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
interface IGlobalStateSyncPreview extends ISyncResourcePreview {
|
||||
export interface IGlobalStateResourcePreview extends IResourcePreview {
|
||||
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
readonly remote: IStringDictionary<IStorageValue> | null;
|
||||
readonly skippedStorageKeys: string[];
|
||||
readonly localUserData: IGlobalState;
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
}
|
||||
|
||||
interface ILastSyncUserData extends IRemoteUserData {
|
||||
|
@ -75,76 +74,99 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
|||
);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
|
||||
const localGlobalState = await this.getLocalGlobalState();
|
||||
const resourcePreviews: IGlobalStateResourcePreview[] = [];
|
||||
const localResource = GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI;
|
||||
const localContent = this.format(localGlobalState);
|
||||
const remoteResource = this.remotePreviewResource;
|
||||
const previewResource = this.localPreviewResource;
|
||||
const previewContent = null;
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
|
||||
const mergeResult = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, remote, skipped } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
local, remote, localUserData: localGlobalState, skippedStorageKeys: skipped,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
resourcePreviews.push({
|
||||
localResource,
|
||||
localContent,
|
||||
remoteResource,
|
||||
remoteContent: this.format(remoteGlobalState),
|
||||
previewResource,
|
||||
previewContent,
|
||||
local,
|
||||
remote,
|
||||
localUserData: localGlobalState,
|
||||
skippedStorageKeys: skipped,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
local: { added: {}, removed: [], updated: {} }, remote: null, localUserData: localGlobalState, skippedStorageKeys: [],
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: false,
|
||||
resourcePreviews.push({
|
||||
localResource,
|
||||
localContent,
|
||||
remoteResource,
|
||||
remoteContent: null,
|
||||
previewResource,
|
||||
previewContent,
|
||||
local: { added: {}, removed: [], updated: {} },
|
||||
remote: null,
|
||||
localUserData: localGlobalState,
|
||||
skippedStorageKeys: [],
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: []
|
||||
};
|
||||
});
|
||||
}
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
return {
|
||||
local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData,
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
return [{
|
||||
localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
|
||||
localContent: this.format(localUserData),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
local: { added: {}, removed: [], updated: {} },
|
||||
remote: localUserData.storage,
|
||||
localUserData,
|
||||
skippedStorageKeys: [],
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: false,
|
||||
resourcePreviews: this.getResourcePreviews({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, skipped: [] })
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalStateSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalStateResourcePreview[]> {
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const mergeResult = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, skipped } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData,
|
||||
return [{
|
||||
localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
|
||||
localContent: this.format(localUserData),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
local,
|
||||
remote: syncGlobalState.storage,
|
||||
localUserData,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: false,
|
||||
resourcePreviews: [],
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
let lastSyncGlobalState: IGlobalState | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSyncGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
}
|
||||
} else {
|
||||
lastSyncGlobalState = lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
}
|
||||
const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
const localGloablState = await this.getLocalGlobalState();
|
||||
|
||||
|
@ -156,30 +178,32 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
|||
|
||||
const mergeResult = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, remote, skipped } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
|
||||
return {
|
||||
local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData,
|
||||
return [{
|
||||
localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
|
||||
localContent: this.format(localGloablState),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
local,
|
||||
remote,
|
||||
localUserData: localGloablState,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
isLastSyncFromCurrentMachine,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IGlobalStateSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
|
||||
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
|
||||
}
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IGlobalStateResourcePreview[], forcePush: boolean): Promise<void> {
|
||||
let { local, remote, localUserData, localChange, remoteChange, skippedStorageKeys } = resourcePreviews[0];
|
||||
|
||||
protected async applyPreview({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalStateSyncPreview, forcePush: boolean): Promise<void> {
|
||||
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (localChange !== Change.None) {
|
||||
// update local
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`);
|
||||
await this.backupLocal(JSON.stringify(localUserData));
|
||||
|
@ -187,7 +211,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
|||
this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`);
|
||||
}
|
||||
|
||||
if (hasRemoteChanged) {
|
||||
if (remoteChange !== Change.None) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`);
|
||||
const content = JSON.stringify(<IGlobalState>{ storage: remote });
|
||||
|
@ -203,18 +227,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
|||
}
|
||||
}
|
||||
|
||||
private getResourcePreviews({ local, remote }: IMergeResult): IResourcePreview[] {
|
||||
const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0;
|
||||
const hasRemoteChanged = remote !== null;
|
||||
return [{
|
||||
hasLocalChanged,
|
||||
hasConflicts: false,
|
||||
hasRemoteChanged,
|
||||
localResouce: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
|
||||
remoteResource: this.remotePreviewResource
|
||||
}];
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }];
|
||||
}
|
||||
|
@ -225,6 +237,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
|||
return this.format(localGlobalState);
|
||||
}
|
||||
|
||||
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
|
|
|
@ -7,7 +7,7 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo
|
|||
import {
|
||||
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource,
|
||||
IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle,
|
||||
IRemoteUserData, ISyncData, IResourcePreview
|
||||
IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
@ -19,7 +19,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
|||
import { OS, OperatingSystem } from 'vs/base/common/platform';
|
||||
import { isUndefined } from 'vs/base/common/types';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
|
||||
|
@ -53,105 +53,69 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
|||
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const content = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = false;
|
||||
const hasConflicts = false;
|
||||
const previewContent = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: previewContent,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const content: string | null = fileContent ? fileContent.value.toString() : null;
|
||||
const hasLocalChanged = false;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
const previewContent: string | null = fileContent ? fileContent.value.toString() : null;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const content = this.getKeybindingsContentFromSyncContent(syncData.content);
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
const previewContent = this.getKeybindingsContentFromSyncContent(syncData.content);
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
let lastSyncContent: string | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSyncContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
}
|
||||
} else {
|
||||
lastSyncContent = lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
|
||||
}
|
||||
const lastSyncContent: string | null = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
// Get file content last to get the latest
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
|
@ -170,7 +134,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
|||
const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
|
||||
// Sync only if there are changes
|
||||
if (result.hasChanges) {
|
||||
content = result.mergeContent;
|
||||
previewContent = result.mergeContent;
|
||||
hasConflicts = result.hasConflicts;
|
||||
hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
|
||||
hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
|
||||
|
@ -181,43 +145,37 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
|||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`);
|
||||
content = fileContent.value.toString();
|
||||
previewContent = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
}
|
||||
|
||||
if (content && !token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
|
||||
if (previewContent && !token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
|
||||
}
|
||||
|
||||
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
previewResource: this.localPreviewResource
|
||||
remoteContent,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
hasConflicts,
|
||||
localChange: hasLocalChanged ? Change.Modified : Change.None,
|
||||
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
|
||||
}];
|
||||
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews };
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) {
|
||||
preview = { ...preview, content: conflictContent, hasConflicts: false };
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise<void> {
|
||||
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview;
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], forcePush: boolean): Promise<void> {
|
||||
let { fileContent, previewContent: content, localChange, remoteChange } = resourcePreviews[0];
|
||||
|
||||
if (content !== null) {
|
||||
if (this.hasErrors(content)) {
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
|
||||
|
@ -226,7 +184,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
|||
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
|
||||
}
|
||||
|
||||
if (hasRemoteChanged) {
|
||||
if (remoteChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`);
|
||||
const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null);
|
||||
remoteUserData = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref);
|
||||
|
@ -272,7 +230,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
|||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, uri)) {
|
||||
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
let content = await super.resolveContent(uri);
|
||||
|
@ -292,11 +250,6 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
|||
return null;
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
const content = await super.resolvePreviewContent(conflictResource);
|
||||
return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null;
|
||||
}
|
||||
|
||||
getKeybindingsContentFromSyncContent(syncContent: string): string | null {
|
||||
try {
|
||||
const parsed = <ISyncContent>JSON.parse(syncContent);
|
||||
|
|
|
@ -7,7 +7,7 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo
|
|||
import {
|
||||
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY,
|
||||
SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IUserDataSynchroniser,
|
||||
IRemoteUserData, ISyncData, IResourcePreview
|
||||
IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { localize } from 'vs/nls';
|
||||
|
@ -17,7 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
|||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
|
@ -58,133 +58,94 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
|||
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
if (remoteSettingsSyncContent !== null) {
|
||||
// Update ignored settings from local file content
|
||||
content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
previewContent = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = false;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
if (fileContent !== null) {
|
||||
// Remove ignored settings
|
||||
content = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
|
||||
previewContent = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
const hasLocalChanged = false;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
|
||||
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
if (settingsSyncContent) {
|
||||
content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
previewContent = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
resourcePreviews,
|
||||
isLastSyncFromCurrentMachine: false
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
let lastSettingsSyncContent: ISettingsSyncContent | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
}
|
||||
} else {
|
||||
lastSettingsSyncContent = this.getSettingsSyncContent(lastSyncUserData);
|
||||
}
|
||||
const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null;
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
|
@ -195,7 +156,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
|||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, [], formattingOptions);
|
||||
content = result.localContent || result.remoteContent;
|
||||
previewContent = result.localContent || result.remoteContent;
|
||||
hasLocalChanged = result.localContent !== null;
|
||||
hasRemoteChanged = result.remoteContent !== null;
|
||||
hasConflicts = result.hasConflicts;
|
||||
|
@ -204,49 +165,47 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
|||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`);
|
||||
content = fileContent.value.toString();
|
||||
previewContent = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
}
|
||||
|
||||
if (content && !token.isCancellationRequested) {
|
||||
if (previewContent && !token.isCancellationRequested) {
|
||||
// Remove the ignored settings from the preview.
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions);
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
|
||||
const content = updateIgnoredSettings(previewContent, '{}', ignoredSettings, formattingOptions);
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
|
||||
}
|
||||
|
||||
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
previewResource: this.localPreviewResource
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: hasLocalChanged ? Change.Modified : Change.None,
|
||||
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
|
||||
hasConflicts,
|
||||
}];
|
||||
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews };
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Add ignored settings from local file content
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const content = updateIgnoredSettings(conflictContent, preview.fileContent ? preview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
preview = { ...preview, content, hasConflicts: false };
|
||||
}
|
||||
return preview;
|
||||
protected async updateResourcePreviewContent(resourcePreview: IFileResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IFileResourcePreview> {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Add ignored settings from local file content
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
previewContent = updateIgnoredSettings(previewContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
return super.updateResourcePreviewContent(resourcePreview, resource, previewContent, token) as Promise<IFileResourcePreview>;
|
||||
}
|
||||
|
||||
protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise<void> {
|
||||
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview;
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], forcePush: boolean): Promise<void> {
|
||||
let { fileContent, previewContent: content, localChange, remoteChange } = resourcePreviews[0];
|
||||
|
||||
if (content !== null) {
|
||||
|
||||
this.validateContent(content);
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
|
||||
|
@ -254,7 +213,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
|||
await this.updateLocalFileContent(content, fileContent);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
|
||||
}
|
||||
if (hasRemoteChanged) {
|
||||
if (remoteChange !== Change.None) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Update ignored settings from remote
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
|
@ -302,7 +261,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
|||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, uri)) {
|
||||
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
let content = await super.resolveContent(uri);
|
||||
|
@ -327,13 +286,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
|||
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
let content = await super.resolvePreviewContent(conflictResource);
|
||||
if (content !== null) {
|
||||
const settingsSyncContent = this.parseSettingsSyncContent(content);
|
||||
content = settingsSyncContent ? settingsSyncContent.settings : null;
|
||||
}
|
||||
if (content !== null) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// remove ignored settings from the remote content for preview
|
||||
// remove ignored settings from the preview content
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
content = updateIgnoredSettings(content, '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
|
|
@ -5,28 +5,31 @@
|
|||
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
|
||||
export interface IMergeResult {
|
||||
added: IStringDictionary<string>;
|
||||
updated: IStringDictionary<string>;
|
||||
removed: string[];
|
||||
local: {
|
||||
added: IStringDictionary<string>;
|
||||
updated: IStringDictionary<string>;
|
||||
removed: string[];
|
||||
};
|
||||
remote: {
|
||||
added: IStringDictionary<string>;
|
||||
updated: IStringDictionary<string>;
|
||||
removed: string[];
|
||||
};
|
||||
conflicts: string[];
|
||||
remote: IStringDictionary<string> | null;
|
||||
}
|
||||
|
||||
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null, resolvedConflicts: IStringDictionary<string | null> = {}): IMergeResult {
|
||||
const added: IStringDictionary<string> = {};
|
||||
const updated: IStringDictionary<string> = {};
|
||||
const removed: Set<string> = new Set<string>();
|
||||
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null): IMergeResult {
|
||||
const localAdded: IStringDictionary<string> = {};
|
||||
const localUpdated: IStringDictionary<string> = {};
|
||||
const localRemoved: Set<string> = new Set<string>();
|
||||
|
||||
if (!remote) {
|
||||
return {
|
||||
added,
|
||||
removed: values(removed),
|
||||
updated,
|
||||
conflicts: [],
|
||||
remote: Object.keys(local).length > 0 ? local : null
|
||||
local: { added: localAdded, updated: localUpdated, removed: values(localRemoved) },
|
||||
remote: { added: local, updated: {}, removed: [] },
|
||||
conflicts: []
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -34,145 +37,118 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar
|
|||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return {
|
||||
added,
|
||||
removed: values(removed),
|
||||
updated,
|
||||
conflicts: [],
|
||||
remote: null
|
||||
local: { added: localAdded, updated: localUpdated, removed: values(localRemoved) },
|
||||
remote: { added: {}, updated: {}, removed: [] },
|
||||
conflicts: []
|
||||
};
|
||||
}
|
||||
|
||||
const baseToLocal = compare(base, local);
|
||||
const baseToRemote = compare(base, remote);
|
||||
const remoteContent: IStringDictionary<string> = deepClone(remote);
|
||||
|
||||
const remoteAdded: IStringDictionary<string> = {};
|
||||
const remoteUpdated: IStringDictionary<string> = {};
|
||||
const remoteRemoved: Set<string> = new Set<string>();
|
||||
|
||||
const conflicts: Set<string> = new Set<string>();
|
||||
const handledConflicts: Set<string> = new Set<string>();
|
||||
const handleConflict = (key: string): void => {
|
||||
if (handledConflicts.has(key)) {
|
||||
return;
|
||||
}
|
||||
handledConflicts.add(key);
|
||||
const conflictContent = resolvedConflicts[key];
|
||||
|
||||
// add to conflicts
|
||||
if (conflictContent === undefined) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
|
||||
// remove the snippet
|
||||
else if (conflictContent === null) {
|
||||
delete remote[key];
|
||||
if (local[key]) {
|
||||
removed.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// add/update the snippet
|
||||
else {
|
||||
if (local[key]) {
|
||||
if (local[key] !== conflictContent) {
|
||||
updated[key] = conflictContent;
|
||||
}
|
||||
} else {
|
||||
added[key] = conflictContent;
|
||||
}
|
||||
remoteContent[key] = conflictContent;
|
||||
}
|
||||
};
|
||||
|
||||
// Removed snippets in Local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
// Conflict - Got updated in remote.
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Add to local
|
||||
added[key] = remote[key];
|
||||
localAdded[key] = remote[key];
|
||||
}
|
||||
// Remove it in remote
|
||||
else {
|
||||
delete remoteContent[key];
|
||||
remoteRemoved.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed snippets in Remote
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Conflict - Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
// Also remove in Local
|
||||
else {
|
||||
removed.add(key);
|
||||
localRemoved.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Updated snippets in Local
|
||||
for (const key of values(baseToLocal.updated)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent[key] = local[key];
|
||||
remoteUpdated[key] = local[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Updated snippets in Remote
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else if (local[key] !== undefined) {
|
||||
updated[key] = remote[key];
|
||||
localUpdated[key] = remote[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Added snippets in Local
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in remote
|
||||
if (baseToRemote.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent[key] = local[key];
|
||||
remoteAdded[key] = local[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Added snippets in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
added[key] = remote[key];
|
||||
localAdded[key] = remote[key];
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed: values(removed), updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent };
|
||||
return {
|
||||
local: { added: localAdded, removed: values(localRemoved), updated: localUpdated },
|
||||
remote: { added: remoteAdded, removed: values(remoteRemoved), updated: remoteUpdated },
|
||||
conflicts: values(conflicts),
|
||||
};
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<string> | null, to: IStringDictionary<string> | null): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
|
@ -196,7 +172,7 @@ function compare(from: IStringDictionary<string> | null, to: IStringDictionary<s
|
|||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
function areSame(a: IStringDictionary<string>, b: IStringDictionary<string>): boolean {
|
||||
export function areSame(a: IStringDictionary<string>, b: IStringDictionary<string>): boolean {
|
||||
const { added, removed, updated } = compare(a, b);
|
||||
return added.size === 0 && removed.size === 0 && updated.size === 0;
|
||||
}
|
||||
|
|
|
@ -5,30 +5,23 @@
|
|||
|
||||
import {
|
||||
IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService,
|
||||
Conflict, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, IResourcePreview
|
||||
USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, UserDataSyncError, UserDataSyncErrorCode, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename, dirname } from 'vs/base/common/resources';
|
||||
import { joinPath, extname, relativePath, isEqualOrParent, basename, dirname } from 'vs/base/common/resources';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { merge, IMergeResult } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { merge, IMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
interface ISinppetsSyncPreview extends ISyncResourcePreview {
|
||||
readonly local: IStringDictionary<IFileContent>;
|
||||
readonly added: IStringDictionary<string>;
|
||||
readonly updated: IStringDictionary<string>;
|
||||
readonly removed: string[];
|
||||
readonly conflicts: Conflict[];
|
||||
readonly resolvedConflicts: IStringDictionary<string | null>;
|
||||
readonly remote: IStringDictionary<string> | null;
|
||||
}
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import { localize } from 'vs/nls';
|
||||
import { values } from 'vs/base/common/map';
|
||||
|
||||
export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
|
@ -60,83 +53,40 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
|||
this.triggerLocalChange();
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISinppetsSyncPreview> {
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const resourcePreviews: IFileResourcePreview[] = [];
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, localSnippets);
|
||||
const { added, updated, remote, removed } = mergeResult;
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
added, removed, updated, remote, local,
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: this.getResourcePreviews(mergeResult)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
added: {}, removed: [], updated: {}, remote: null, local: {},
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: false,
|
||||
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: []
|
||||
};
|
||||
resourcePreviews.push(...this.getResourcePreviews(mergeResult, local, remoteSnippets));
|
||||
}
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISinppetsSyncPreview> {
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const mergeResult = merge(localSnippets, null, null);
|
||||
const { added, updated, remote, removed } = mergeResult;
|
||||
return {
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
hasConflicts: false,
|
||||
resourcePreviews: this.getResourcePreviews(mergeResult)
|
||||
};
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult, local, {});
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const snippets = this.parseSnippets(syncData);
|
||||
const mergeResult = merge(localSnippets, snippets, localSnippets);
|
||||
const { added, updated, removed } = mergeResult;
|
||||
return {
|
||||
added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, hasConflicts: false,
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: this.getResourcePreviews(mergeResult)
|
||||
};
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult, local, snippets);
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
return this.doGeneratePreview(local, remoteUserData, lastSyncUserData, {}, token);
|
||||
}
|
||||
|
||||
private async doGeneratePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null> = {}, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreview> {
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
|
||||
let lastSyncSnippets: IStringDictionary<string> | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSyncSnippets = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
|
||||
}
|
||||
} else {
|
||||
lastSyncSnippets = lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
|
||||
}
|
||||
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
|
||||
|
||||
if (remoteSnippets) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);
|
||||
|
@ -144,79 +94,47 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
|||
this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`);
|
||||
}
|
||||
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts);
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult);
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets);
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult, local, remoteSnippets || {});
|
||||
|
||||
const conflicts: Conflict[] = [];
|
||||
for (const key of mergeResult.conflicts) {
|
||||
const localPreview = joinPath(this.syncPreviewFolder, key);
|
||||
conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) });
|
||||
const content = local[key];
|
||||
if (!token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(localPreview, content ? content.value : VSBuffer.fromString(''));
|
||||
}
|
||||
}
|
||||
|
||||
for (const conflict of this.conflicts) {
|
||||
// clear obsolete conflicts
|
||||
if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) {
|
||||
try {
|
||||
await this.fileService.del(conflict.local);
|
||||
} catch (error) {
|
||||
// Ignore & log
|
||||
this.logService.error(error);
|
||||
for (const resourcePreview of resourcePreviews) {
|
||||
if (resourcePreview.hasConflicts) {
|
||||
if (!token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(resourcePreview.previewResource!, VSBuffer.fromString(resourcePreview.previewContent || ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setConflicts(conflicts);
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async updateResourcePreviewContent(resourcePreview: IFileResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IFileResourcePreview> {
|
||||
return {
|
||||
remoteUserData, local,
|
||||
lastSyncUserData,
|
||||
added: mergeResult.added,
|
||||
removed: mergeResult.removed,
|
||||
updated: mergeResult.updated,
|
||||
conflicts,
|
||||
hasConflicts: conflicts.length > 0,
|
||||
remote: mergeResult.remote,
|
||||
resolvedConflicts,
|
||||
hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0,
|
||||
hasRemoteChanged: mergeResult.remote !== null,
|
||||
isLastSyncFromCurrentMachine,
|
||||
resourcePreviews
|
||||
...resourcePreview,
|
||||
previewContent: previewContent || null,
|
||||
hasConflicts: false,
|
||||
localChange: previewContent ? Change.Modified : Change.Deleted,
|
||||
remoteChange: previewContent ? Change.Modified : Change.Deleted,
|
||||
};
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: ISinppetsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISinppetsSyncPreview> {
|
||||
const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0];
|
||||
if (conflict) {
|
||||
const key = relativePath(this.syncPreviewFolder, conflict.local)!;
|
||||
preview.resolvedConflicts[key] = content || null;
|
||||
preview = await this.doGeneratePreview(preview.local, preview.remoteUserData, preview.lastSyncUserData, preview.resolvedConflicts, token);
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], forcePush: boolean): Promise<void> {
|
||||
if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
|
||||
throw new UserDataSyncError(localize('unresolved conflicts', "Error while syncing {0}. Please resolve conflicts first.", this.syncResourceLogLabel), UserDataSyncErrorCode.UnresolvedConflicts, this.resource);
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
protected async applyPreview(preview: ISinppetsSyncPreview, forcePush: boolean): Promise<void> {
|
||||
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = preview;
|
||||
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
if (resourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (resourcePreviews.some(({ localChange }) => localChange !== Change.None)) {
|
||||
// back up all snippets
|
||||
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
|
||||
await this.updateLocalSnippets(added, removed, updated, local);
|
||||
await this.updateLocalBackup(resourcePreviews);
|
||||
await this.updateLocalSnippets(resourcePreviews);
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
|
||||
const content = JSON.stringify(remote);
|
||||
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
|
||||
if (resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) {
|
||||
remoteUserData = await this.updateRemoteSnippets(resourcePreviews, remoteUserData, forcePush);
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
|
@ -226,52 +144,149 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
|||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`);
|
||||
}
|
||||
|
||||
for (const { previewResource } of resourcePreviews) {
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(previewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private getResourcePreviews(mergeResult: IMergeResult): IResourcePreview[] {
|
||||
const resourcePreviews: IResourcePreview[] = [];
|
||||
for (const key of Object.keys(mergeResult.added)) {
|
||||
resourcePreviews.push({
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false
|
||||
});
|
||||
}
|
||||
for (const key of Object.keys(mergeResult.updated)) {
|
||||
resourcePreviews.push({
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
localResouce: joinPath(this.snippetsFolder, key),
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: true
|
||||
});
|
||||
}
|
||||
for (const key of mergeResult.removed) {
|
||||
resourcePreviews.push({
|
||||
localResouce: joinPath(this.snippetsFolder, key),
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false
|
||||
});
|
||||
}
|
||||
for (const key of mergeResult.conflicts) {
|
||||
resourcePreviews.push({
|
||||
localResouce: joinPath(this.snippetsFolder, key),
|
||||
private getResourcePreviews(mergeResult: IMergeResult, localFileContent: IStringDictionary<IFileContent>, remoteSnippets: IStringDictionary<string>): IFileResourcePreview[] {
|
||||
const resourcePreviews: Map<string, IFileResourcePreview> = new Map<string, IFileResourcePreview>();
|
||||
|
||||
/* Snippets added remotely -> add locally */
|
||||
for (const key of Object.keys(mergeResult.local.added)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: null,
|
||||
localContent: null,
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
hasConflicts: true,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: true
|
||||
previewContent: mergeResult.local.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Added,
|
||||
remoteChange: Change.None
|
||||
});
|
||||
}
|
||||
|
||||
return resourcePreviews;
|
||||
}
|
||||
/* Snippets updated remotely -> update locally */
|
||||
for (const key of Object.keys(mergeResult.local.updated)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.local.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.None
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.clearConflicts();
|
||||
return super.stop();
|
||||
/* Snippets removed remotely -> remove locally */
|
||||
for (const key of mergeResult.local.removed) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.Deleted,
|
||||
remoteChange: Change.None
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets added locally -> add remotely */
|
||||
for (const key of Object.keys(mergeResult.remote.added)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.remote.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Added
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets updated locally -> update remotely */
|
||||
for (const key of Object.keys(mergeResult.remote.updated)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.remote.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets removed locally -> remove remotely */
|
||||
for (const key of mergeResult.remote.removed) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: null,
|
||||
localContent: null,
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Deleted
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets with conflicts */
|
||||
for (const key of mergeResult.conflicts) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key] || null,
|
||||
localContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key] || null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: true,
|
||||
localChange: localFileContent[key] ? Change.Modified : Change.Added,
|
||||
remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added
|
||||
});
|
||||
}
|
||||
|
||||
/* Unmodified Snippets */
|
||||
for (const key of Object.keys(localFileContent)) {
|
||||
if (!resourcePreviews.has(key)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key] || null,
|
||||
localContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key] || null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return values(resourcePreviews);
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
|
@ -294,13 +309,16 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
|||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqualOrParent(uri.with({ scheme: this.syncFolder.scheme }), this.syncPreviewFolder)) {
|
||||
if (isEqualOrParent(uri.with({ scheme: this.syncPreviewFolder.scheme }), this.syncPreviewFolder)
|
||||
|| isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME }))) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
content = await super.resolveContent(dirname(uri));
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
|
@ -309,20 +327,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
|||
return snippets[basename(uri)] || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
const syncPreview = await this.getSyncPreviewInProgress();
|
||||
if (syncPreview) {
|
||||
const key = relativePath(this.syncPreviewFolder, conflictResource.with({ scheme: this.syncPreviewFolder.scheme }))!;
|
||||
if (conflictResource.scheme === this.syncPreviewFolder.scheme) {
|
||||
return (syncPreview as ISinppetsSyncPreview).local[key] ? (syncPreview as ISinppetsSyncPreview).local[key].value.toString() : null;
|
||||
} else if (syncPreview.remoteUserData && syncPreview.remoteUserData.syncData) {
|
||||
const snippets = this.parseSnippets(syncPreview.remoteUserData.syncData);
|
||||
return snippets[key] || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -338,34 +343,78 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
|||
return false;
|
||||
}
|
||||
|
||||
private async clearConflicts(): Promise<void> {
|
||||
if (this.conflicts.length) {
|
||||
await Promise.all(this.conflicts.map(({ local }) => this.fileService.del(local)));
|
||||
this.setConflicts([]);
|
||||
private async updateLocalBackup(resourcePreviews: IFileResourcePreview[]): Promise<void> {
|
||||
const local: IStringDictionary<IFileContent> = {};
|
||||
for (const resourcePreview of resourcePreviews) {
|
||||
if (resourcePreview.fileContent) {
|
||||
local[basename(resourcePreview.localResource!)] = resourcePreview.fileContent;
|
||||
}
|
||||
}
|
||||
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
|
||||
}
|
||||
|
||||
private async updateLocalSnippets(resourcePreviews: IFileResourcePreview[]): Promise<void> {
|
||||
if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
|
||||
// Do not update if there are conflicts
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { fileContent, previewContent: content, localResource, remoteResource, localChange } of resourcePreviews) {
|
||||
if (localChange !== Change.None) {
|
||||
const key = remoteResource ? basename(remoteResource) : basename(localResource!);
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
|
||||
// Removed
|
||||
if (localChange === Change.Deleted) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource));
|
||||
await this.fileService.del(resource);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource));
|
||||
}
|
||||
|
||||
// Added
|
||||
else if (localChange === Change.Added) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource));
|
||||
await this.fileService.createFile(resource, VSBuffer.fromString(content!), { overwrite: false });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource));
|
||||
}
|
||||
|
||||
// Updated
|
||||
else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource));
|
||||
await this.fileService.writeFile(resource, VSBuffer.fromString(content!), fileContent!);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateLocalSnippets(added: IStringDictionary<string>, removed: string[], updated: IStringDictionary<string>, local: IStringDictionary<IFileContent>): Promise<void> {
|
||||
for (const key of removed) {
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource));
|
||||
await this.fileService.del(resource);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource));
|
||||
private async updateRemoteSnippets(resourcePreviews: IFileResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise<IRemoteUserData> {
|
||||
if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
|
||||
// Do not update if there are conflicts
|
||||
return remoteUserData;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(added)) {
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource));
|
||||
await this.fileService.createFile(resource, VSBuffer.fromString(added[key]), { overwrite: false });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource));
|
||||
const currentSnippets: IStringDictionary<string> = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : {};
|
||||
const newSnippets: IStringDictionary<string> = deepClone(currentSnippets);
|
||||
|
||||
for (const { previewContent: content, localResource, remoteResource, remoteChange } of resourcePreviews) {
|
||||
if (remoteChange !== Change.None) {
|
||||
const key = localResource ? basename(localResource) : basename(remoteResource!);
|
||||
if (remoteChange === Change.Deleted) {
|
||||
delete newSnippets[key];
|
||||
} else {
|
||||
newSnippets[key] = content!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(updated)) {
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource));
|
||||
await this.fileService.writeFile(resource, VSBuffer.fromString(updated[key]), local[key]);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource));
|
||||
if (!areSame(currentSnippets, newSnippets)) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
|
||||
remoteUserData = await this.updateRemoteUserData(JSON.stringify(newSnippets), forcePush ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
|
||||
}
|
||||
return remoteUserData;
|
||||
}
|
||||
|
||||
private parseSnippets(syncData: ISyncData): IStringDictionary<string> {
|
||||
|
|
|
@ -212,6 +212,7 @@ export enum UserDataSyncErrorCode {
|
|||
LocalInvalidContent = 'LocalInvalidContent',
|
||||
LocalError = 'LocalError',
|
||||
Incompatible = 'Incompatible',
|
||||
UnresolvedConflicts = 'UnresolvedConflicts',
|
||||
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
@ -293,20 +294,24 @@ export interface ISyncData {
|
|||
content: string;
|
||||
}
|
||||
|
||||
export const enum Change {
|
||||
None,
|
||||
Added,
|
||||
Modified,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
export interface IResourcePreview {
|
||||
readonly remoteResource?: URI;
|
||||
readonly localResouce?: URI;
|
||||
readonly previewResource?: URI;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly remoteResource: URI;
|
||||
readonly localResource: URI;
|
||||
readonly previewResource: URI;
|
||||
readonly localChange: Change;
|
||||
readonly remoteChange: Change;
|
||||
readonly hasConflicts: boolean;
|
||||
}
|
||||
|
||||
export interface ISyncResourcePreview {
|
||||
readonly isLastSyncFromCurrentMachine: boolean;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
readonly resourcePreviews: IResourcePreview[];
|
||||
}
|
||||
|
||||
|
@ -325,7 +330,7 @@ export interface IUserDataSynchroniser {
|
|||
replace(uri: URI): Promise<boolean>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
generateSyncPreview(): Promise<ISyncResourcePreview | null>
|
||||
generateSyncResourcePreview(): Promise<ISyncResourcePreview | null>
|
||||
hasPreviouslySynced(): Promise<boolean>
|
||||
hasLocalData(): Promise<boolean>;
|
||||
resetLocal(): Promise<void>;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask, Change } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
|
@ -297,8 +297,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
|||
}
|
||||
|
||||
for (const synchroniser of synchronizers) {
|
||||
const preview = await synchroniser.generateSyncPreview();
|
||||
if (preview && !preview.isLastSyncFromCurrentMachine && (preview.hasLocalChanged || preview.hasRemoteChanged)) {
|
||||
const preview = await synchroniser.generateSyncResourcePreview();
|
||||
if (preview && !preview.isLastSyncFromCurrentMachine
|
||||
&& (preview.resourcePreviews.some(({ localChange }) => localChange !== Change.None) || preview.resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,11 +116,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries', async () => {
|
||||
|
@ -129,11 +131,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries in different order', async () => {
|
||||
|
@ -142,11 +146,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with different base content', async () => {
|
||||
|
@ -156,11 +162,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote', async () => {
|
||||
|
@ -169,11 +177,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to remote', async () => {
|
||||
|
@ -182,11 +192,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, remote);
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, remote);
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when new entry is added to remote from base and local has not changed', async () => {
|
||||
|
@ -195,11 +207,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from remote from base and local has not changed', async () => {
|
||||
|
@ -208,11 +222,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when all entries are removed from base and local has not changed', async () => {
|
||||
|
@ -221,11 +237,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in remote from base and local has not changed', async () => {
|
||||
|
@ -234,11 +252,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => {
|
||||
|
@ -247,11 +267,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.local.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.local.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when a new entries are added to local', async () => {
|
||||
|
@ -260,11 +282,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
|
||||
|
@ -273,11 +297,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1, 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.added, { 'html.json': htmlSnippet1, 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from local from base and remote has not changed', async () => {
|
||||
|
@ -286,11 +312,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, ['typescript.json']);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in local from base and remote has not changed', async () => {
|
||||
|
@ -299,11 +327,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => {
|
||||
|
@ -312,11 +342,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.remote.removed, ['typescript.json']);
|
||||
});
|
||||
|
||||
test('merge when local and remote with one entry but different value', async () => {
|
||||
|
@ -325,11 +357,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
|
||||
|
@ -339,11 +373,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge with single entry and local is empty', async () => {
|
||||
|
@ -353,11 +389,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with conflicts', async () => {
|
||||
|
@ -367,41 +405,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet });
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with resolved conflicts - update', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet2 };
|
||||
const resolvedConflicts = { 'html.json': htmlSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, base, resolvedConflicts);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'html.json': htmlSnippet2, 'c.json': cSnippet });
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with resolved conflicts - remove', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet2 };
|
||||
const resolvedConflicts = { 'html.json': null };
|
||||
|
||||
const actual = merge(local, remote, base, resolvedConflicts);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, ['html.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with multiple conflicts', async () => {
|
||||
|
@ -411,26 +421,13 @@ suite('SnippetsMerge', () => {
|
|||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with multiple conflicts and resolving one conflict', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'c.json': cSnippet };
|
||||
const resolvedConflicts = { 'html.json': htmlSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, base, resolvedConflicts);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet1 });
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['typescript.json']);
|
||||
assert.deepEqual(actual.remote, { 'c.json': cSnippet, 'html.json': htmlSnippet1 });
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -4,19 +4,21 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData, Change, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
interface ITestSyncPreview extends ISyncResourcePreview {
|
||||
interface ITestResourcePreview extends IResourcePreview {
|
||||
ref?: string;
|
||||
}
|
||||
|
||||
const resource = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'testResource', path: `/current.json` });
|
||||
|
||||
class TestSynchroniser extends AbstractSynchroniser {
|
||||
|
||||
syncBarrier: Barrier = new Barrier();
|
||||
|
@ -40,32 +42,32 @@ class TestSynchroniser extends AbstractSynchroniser {
|
|||
return super.doSync(remoteUserData, lastSyncUserData);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestResourcePreview[]> {
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: null, previewResource: resource, localChange: Change.None, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestResourcePreview[]> {
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: null, previewResource: resource, localChange: Change.None, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ITestSyncPreview> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ITestResourcePreview[]> {
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: null, previewResource: resource, localChange: Change.None, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestResourcePreview[]> {
|
||||
if (this.syncResult.hasError) {
|
||||
throw new Error('failed');
|
||||
}
|
||||
return { ref: remoteUserData.ref, hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: null, previewResource: resource, localChange: Change.None, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts, ref: remoteUserData.ref }];
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, conflictContent: string): Promise<ISyncResourcePreview> {
|
||||
return preview;
|
||||
}
|
||||
|
||||
protected async applyPreview({ ref }: ITestSyncPreview, forcePush: boolean): Promise<void> {
|
||||
if (ref) {
|
||||
await this.apply(ref);
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, preview: ITestResourcePreview[], forcePush: boolean): Promise<void> {
|
||||
if (preview[0]?.ref) {
|
||||
await this.apply(preview[0].ref);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -402,7 +402,7 @@ abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProv
|
|||
handle,
|
||||
collapsibleState: TreeItemCollapsibleState.None,
|
||||
resourceUri: resource,
|
||||
command: { id: `workbench.actions.sync.commpareWithLocal`, title: '', arguments: [<TreeViewItemHandleArg>{ $treeViewId: '', $treeItemHandle: handle }] },
|
||||
command: { id: `workbench.actions.sync.compareWithLocal`, title: '', arguments: [<TreeViewItemHandleArg>{ $treeViewId: '', $treeItemHandle: handle }] },
|
||||
contextValue: `sync-associatedResource-${(<SyncResourceHandleTreeItem>element).syncResourceHandle.syncResource}`
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue