Merge from vscode cfbd1999769f4f08dce29629fb92fdc0fac53829

This commit is contained in:
ADS Merger
2020-08-06 07:08:52 +00:00
parent 9c67832880
commit 540046ba00
362 changed files with 7588 additions and 6584 deletions

View File

@@ -53,20 +53,41 @@ function isSyncData(thing: any): thing is ISyncData {
return false;
}
export interface IMergableResourcePreview extends IBaseResourcePreview {
export interface IResourcePreview {
readonly remoteResource: URI;
readonly remoteContent: string | null;
readonly remoteChange: Change;
readonly localResource: URI;
readonly localContent: string | null;
readonly previewContent: string | null;
readonly acceptedContent: string | null;
readonly localChange: Change;
readonly previewResource: URI;
readonly acceptedResource: URI;
}
export interface IAcceptResult {
readonly content: string | null;
readonly localChange: Change;
readonly remoteChange: Change;
}
export interface IMergeResult extends IAcceptResult {
readonly hasConflicts: boolean;
}
export type IResourcePreview = Omit<IMergableResourcePreview, 'mergeState'>;
interface IEditableResourcePreview extends IBaseResourcePreview, IResourcePreview {
localChange: Change;
remoteChange: Change;
mergeState: MergeState;
acceptResult?: IAcceptResult;
}
export interface ISyncResourcePreview extends IBaseSyncResourcePreview {
interface ISyncResourcePreview extends IBaseSyncResourcePreview {
readonly remoteUserData: IRemoteUserData;
readonly lastSyncUserData: IRemoteUserData | null;
readonly resourcePreviews: IMergableResourcePreview[];
readonly resourcePreviews: IEditableResourcePreview[];
}
export abstract class AbstractSynchroniser extends Disposable {
@@ -82,10 +103,10 @@ export abstract class AbstractSynchroniser extends Disposable {
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
private _conflicts: IMergableResourcePreview[] = [];
get conflicts(): IMergableResourcePreview[] { return this._conflicts; }
private _onDidChangeConflicts: Emitter<IMergableResourcePreview[]> = this._register(new Emitter<IMergableResourcePreview[]>());
readonly onDidChangeConflicts: Event<IMergableResourcePreview[]> = this._onDidChangeConflicts.event;
private _conflicts: IBaseResourcePreview[] = [];
get conflicts(): IBaseResourcePreview[] { return this._conflicts; }
private _onDidChangeConflicts: Emitter<IBaseResourcePreview[]> = this._register(new Emitter<IBaseResourcePreview[]>());
readonly onDidChangeConflicts: Event<IBaseResourcePreview[]> = this._onDidChangeConflicts.event;
private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
@@ -99,7 +120,7 @@ export abstract class AbstractSynchroniser extends Disposable {
constructor(
readonly resource: SyncResource,
@IFileService protected readonly fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IEnvironmentService protected readonly environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@@ -162,53 +183,6 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
async pull(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`);
return;
}
await this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pulling ${this.syncResourceLogLabel.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async push(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pushing ${this.syncResourceLogLabel.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
await this.applyPreview(remoteUserData, lastSyncUserData, preview, true);
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<void> {
await this._sync(manifest, true, headers);
}
@@ -292,8 +266,20 @@ export abstract class AbstractSynchroniser extends Disposable {
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData);
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
/* use replace sync data */
const resourcePreviewResults = await this.generateSyncPreview({ ref: remoteUserData.ref, syncData }, lastSyncUserData, CancellationToken.None);
const resourcePreviews: [IResourcePreview, IAcceptResult][] = [];
for (const resourcePreviewResult of resourcePreviewResults) {
/* Accept remote resource */
const acceptResult: IAcceptResult = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.remoteResource, undefined, CancellationToken.None);
/* compute remote change */
const { remoteChange } = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, resourcePreviewResult.remoteContent, CancellationToken.None);
resourcePreviews.push([resourcePreviewResult, { ...acceptResult, remoteChange: remoteChange !== Change.None ? remoteChange : Change.Modified }]);
}
await this.applyResult(remoteUserData, lastSyncUserData, resourcePreviews, false);
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
@@ -384,41 +370,48 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
async accept(resource: URI, content: string | null): Promise<ISyncResourcePreview | null> {
async merge(resource: URI): Promise<ISyncResourcePreview | null> {
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resource, content);
return {
...updatedResourcePreview,
mergeState: MergeState.Accepted
};
const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult?.content || ''));
const acceptResult: IAcceptResult | undefined = mergeResult && !mergeResult.hasConflicts
? await this.getAcceptResult(resourcePreview, resourcePreview.previewResource, undefined, CancellationToken.None)
: undefined;
resourcePreview.acceptResult = acceptResult;
resourcePreview.mergeState = mergeResult.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview;
resourcePreview.localChange = acceptResult ? acceptResult.localChange : mergeResult.localChange;
resourcePreview.remoteChange = acceptResult ? acceptResult.remoteChange : mergeResult.remoteChange;
return resourcePreview;
});
return this.syncPreviewPromise;
}
async merge(resource: URI): Promise<ISyncResourcePreview | null> {
async accept(resource: URI, content?: string | null): Promise<ISyncResourcePreview | null> {
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resourcePreview.previewResource, resourcePreview.previewContent);
return {
...updatedResourcePreview,
mergeState: resourcePreview.hasConflicts ? MergeState.Conflict : MergeState.Accepted
};
const acceptResult = await this.getAcceptResult(resourcePreview, resource, content, CancellationToken.None);
resourcePreview.acceptResult = acceptResult;
resourcePreview.mergeState = MergeState.Accepted;
resourcePreview.localChange = acceptResult.localChange;
resourcePreview.remoteChange = acceptResult.remoteChange;
return resourcePreview;
});
return this.syncPreviewPromise;
}
async discard(resource: URI): Promise<ISyncResourcePreview | null> {
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(resourcePreview.previewContent || ''));
const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resourcePreview.previewResource, resourcePreview.previewContent);
return {
...updatedResourcePreview,
mergeState: MergeState.Preview
};
const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult.content || ''));
resourcePreview.acceptResult = undefined;
resourcePreview.mergeState = MergeState.Preview;
resourcePreview.localChange = mergeResult.localChange;
resourcePreview.remoteChange = mergeResult.remoteChange;
return resourcePreview;
});
return this.syncPreviewPromise;
}
private async updateSyncResourcePreview(resource: URI, updateResourcePreview: (resourcePreview: IMergableResourcePreview) => Promise<IMergableResourcePreview>): Promise<void> {
private async updateSyncResourcePreview(resource: URI, updateResourcePreview: (resourcePreview: IEditableResourcePreview) => Promise<IEditableResourcePreview>): Promise<void> {
if (!this.syncPreviewPromise) {
return;
}
@@ -448,13 +441,6 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
protected async updateResourcePreview(resourcePreview: IResourcePreview, resource: URI, acceptedContent: string | null): Promise<IResourcePreview> {
return {
...resourcePreview,
acceptedContent
};
}
private async doApply(force: boolean): Promise<SyncStatus> {
if (!this.syncPreviewPromise) {
return SyncStatus.Idle;
@@ -473,7 +459,7 @@ export abstract class AbstractSynchroniser extends Disposable {
}
// apply preview
await this.applyPreview(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews, force);
await this.applyResult(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews.map(resourcePreview => ([resourcePreview, resourcePreview.acceptResult!])), force);
// reset preview
this.syncPreviewPromise = null;
@@ -490,8 +476,8 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (error) { /* Ignore */ }
}
private updateConflicts(previews: IMergableResourcePreview[]): void {
const conflicts = previews.filter(p => p.mergeState === MergeState.Conflict);
private updateConflicts(resourcePreviews: IEditableResourcePreview[]): void {
const conflicts = resourcePreviews.filter(({ mergeState }) => mergeState === MergeState.Conflict);
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) {
this._conflicts = conflicts;
this._onDidChangeConflicts.fire(conflicts);
@@ -550,7 +536,7 @@ export abstract class AbstractSynchroniser extends Disposable {
if (syncPreview) {
for (const resourcePreview of syncPreview.resourcePreviews) {
if (isEqual(resourcePreview.acceptedResource, uri)) {
return resourcePreview.acceptedContent;
return resourcePreview.acceptResult ? resourcePreview.acceptResult.content : null;
}
if (isEqual(resourcePreview.remoteResource, uri)) {
return resourcePreview.remoteContent;
@@ -575,22 +561,45 @@ export abstract class AbstractSynchroniser extends Disposable {
// For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine
const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData;
const result = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
const resourcePreviews: IMergableResourcePreview[] = [];
for (const resourcePreview of result) {
if (token.isCancellationRequested) {
break;
const resourcePreviews: IEditableResourcePreview[] = [];
for (const resourcePreviewResult of resourcePreviewResults) {
const acceptedResource = resourcePreviewResult.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
/* No change -> Accept */
if (resourcePreviewResult.localChange === Change.None && resourcePreviewResult.remoteChange === Change.None) {
resourcePreviews.push({
...resourcePreviewResult,
acceptedResource,
acceptResult: { content: null, localChange: Change.None, remoteChange: Change.None },
mergeState: MergeState.Accepted
});
}
if (!apply) {
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(resourcePreview.previewContent || ''));
/* Changed -> Apply ? (Merge ? Conflict | Accept) : Preview */
else {
/* Merge */
const mergeResult = apply ? await this.getMergeResult(resourcePreviewResult, token) : undefined;
if (token.isCancellationRequested) {
break;
}
await this.fileService.writeFile(resourcePreviewResult.previewResource, VSBuffer.fromString(mergeResult?.content || ''));
/* Conflict | Accept */
const acceptResult = mergeResult && !mergeResult.hasConflicts
/* Accept if merged and there are no conflicts */
? await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, undefined, token)
: undefined;
resourcePreviews.push({
...resourcePreviewResult,
acceptResult,
mergeState: mergeResult?.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview,
localChange: acceptResult ? acceptResult.localChange : mergeResult ? mergeResult.localChange : resourcePreviewResult.localChange,
remoteChange: acceptResult ? acceptResult.remoteChange : mergeResult ? mergeResult.remoteChange : resourcePreviewResult.remoteChange
});
}
resourcePreviews.push({
...resourcePreview,
mergeState: resourcePreview.localChange === Change.None && resourcePreview.remoteChange === Change.None ? MergeState.Accepted /* Mark previews with no changes as merged */
: apply ? (resourcePreview.hasConflicts ? MergeState.Conflict : MergeState.Accepted)
: MergeState.Preview
});
}
return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine };
@@ -643,7 +652,7 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (error) {
this.logService.error(error);
}
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource);
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with the current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource);
}
private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> {
@@ -687,11 +696,10 @@ export abstract class AbstractSynchroniser extends Disposable {
}
protected abstract readonly version: number;
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>;
protected abstract getMergeResult(resourcePreview: IResourcePreview, token: CancellationToken): Promise<IMergeResult>;
protected abstract getAcceptResult(resourcePreview: IResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult>;
protected abstract applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, result: [IResourcePreview, IAcceptResult][], force: boolean): Promise<void>;
}
export interface IFileResourcePreview extends IResourcePreview {

View File

@@ -205,7 +205,7 @@ function massageOutgoingExtension(extension: ISyncExtension, key: string): ISync
export function getIgnoredExtensions(installed: ILocalExtension[], configurationService: IConfigurationService): string[] {
const defaultIgnoredExtensions = installed.filter(i => i.isMachineScoped).map(i => i.identifier.id.toLowerCase());
const value = (configurationService.getValue<string[]>('sync.ignoredExtensions') || []).map(id => id.toLowerCase());
const value = getConfiguredIgnoredExtensions(configurationService).map(id => id.toLowerCase());
const added: string[] = [], removed: string[] = [];
if (Array.isArray(value)) {
for (const key of value) {
@@ -218,3 +218,15 @@ export function getIgnoredExtensions(installed: ILocalExtension[], configuration
}
return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1));
}
function getConfiguredIgnoredExtensions(configurationService: IConfigurationService): string[] {
let userValue = configurationService.inspect<string[]>('settingsSync.ignoredExtensions').userValue;
if (userValue !== undefined) {
return userValue;
}
userValue = configurationService.inspect<string[]>('sync.ignoredExtensions').userValue;
if (userValue !== undefined) {
return userValue;
}
return configurationService.getValue<string[]>('settingsSync.ignoredExtensions') || [];
}

View File

@@ -15,7 +15,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens
import { IFileService } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge';
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IAcceptResult, IMergeResult, 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,13 +25,17 @@ import { compare } from 'vs/base/common/strings';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { CancellationToken } from 'vs/base/common/cancellation';
export interface IExtensionResourcePreview extends IResourcePreview {
readonly localExtensions: ISyncExtension[];
interface IExtensionResourceMergeResult extends IAcceptResult {
readonly added: ISyncExtension[];
readonly removed: IExtensionIdentifier[];
readonly updated: ISyncExtension[];
readonly remote: ISyncExtension[] | null;
}
interface IExtensionResourcePreview extends IResourcePreview {
readonly localExtensions: ISyncExtension[];
readonly skippedExtensions: ISyncExtension[];
readonly previewResult: IExtensionResourceMergeResult;
}
interface ILastSyncUserData extends IRemoteUserData {
@@ -41,10 +45,14 @@ interface ILastSyncUserData extends IRemoteUserData {
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` });
/*
Version 3 - Introduce installed property to skip installing built in extensions
protected readonly version: number = 3;
*/
protected readonly version: number = 3;
/* Version 4: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
protected readonly version: number = 4;
protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); }
private readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'extensions.json');
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
@@ -75,47 +83,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
() => undefined, 500)(() => this.triggerLocalChange()));
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionResourcePreview[]> {
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const pullPreview = await this.getPullPreview(remoteExtensions);
return [pullPreview];
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionResourcePreview[]> {
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const pushPreview = await this.getPushPreview(remoteExtensions);
return [pushPreview];
}
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;
return [{
localResource: this.localResource,
localContent: this.format(localExtensions),
remoteResource: this.remoteResource,
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
previewResource: this.previewResource,
previewContent: null,
acceptedResource: this.acceptedResource,
acceptedContent: 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,
}];
}
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 || [] : [];
@@ -131,32 +98,125 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
}
const mergeResult = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
return [{
localResource: this.localResource,
localContent: this.format(localExtensions),
remoteResource: this.remoteResource,
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
previewResource: this.previewResource,
previewContent: null,
acceptedResource: this.acceptedResource,
acceptedContent: null,
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
const previewResult: IExtensionResourceMergeResult = {
added,
removed,
updated,
remote,
localExtensions,
skippedExtensions,
content: this.getPreviewContent(localExtensions, added, updated, removed),
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
remoteChange: remote !== null ? Change.Modified : Change.None,
hasConflicts: false,
};
return [{
skippedExtensions,
localResource: this.localResource,
localContent: this.format(localExtensions),
localExtensions,
remoteResource: this.remoteResource,
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
previewResource: this.previewResource,
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: this.acceptedResource,
}];
}
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IExtensionResourcePreview[], force: boolean): Promise<void> {
let { added, removed, updated, remote, skippedExtensions, localExtensions, localChange, remoteChange } = resourcePreviews[0];
private getPreviewContent(localExtensions: ISyncExtension[], added: ISyncExtension[], updated: ISyncExtension[], removed: IExtensionIdentifier[]): string {
const preview: ISyncExtension[] = [...added, ...updated];
const idsOrUUIDs: Set<string> = new Set<string>();
const addIdentifier = (identifier: IExtensionIdentifier) => {
idsOrUUIDs.add(identifier.id.toLowerCase());
if (identifier.uuid) {
idsOrUUIDs.add(identifier.uuid);
}
};
preview.forEach(({ identifier }) => addIdentifier(identifier));
removed.forEach(addIdentifier);
for (const localExtension of localExtensions) {
if (idsOrUUIDs.has(localExtension.identifier.id.toLowerCase()) || (localExtension.identifier.uuid && idsOrUUIDs.has(localExtension.identifier.uuid))) {
// skip
continue;
}
preview.push(localExtension);
}
return this.format(preview);
}
protected async getMergeResult(resourcePreview: IExtensionResourcePreview, token: CancellationToken): Promise<IMergeResult> {
return { ...resourcePreview.previewResult, hasConflicts: false };
}
protected async getAcceptResult(resourcePreview: IExtensionResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IExtensionResourceMergeResult> {
/* Accept local resource */
if (isEqual(resource, this.localResource)) {
return this.acceptLocal(resourcePreview);
}
/* Accept remote resource */
if (isEqual(resource, this.remoteResource)) {
return this.acceptRemote(resourcePreview);
}
/* Accept preview resource */
if (isEqual(resource, this.previewResource)) {
return resourcePreview.previewResult;
}
throw new Error(`Invalid Resource: ${resource.toString()}`);
}
private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
return {
content: resourcePreview.localContent,
added,
removed,
updated,
remote,
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
remoteChange: remote !== null ? Change.Modified : Change.None,
};
}
private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
if (remoteExtensions !== null) {
const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
return {
content: resourcePreview.remoteContent,
added,
removed,
updated,
remote,
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
remoteChange: remote !== null ? Change.Modified : Change.None,
};
} else {
return {
content: resourcePreview.remoteContent,
added: [], removed: [], updated: [], remote: null,
localChange: Change.None,
remoteChange: Change.None,
};
}
}
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IExtensionResourcePreview, IExtensionResourceMergeResult][], force: boolean): Promise<void> {
let { skippedExtensions, localExtensions } = resourcePreviews[0][0];
let { added, removed, updated, remote, localChange, remoteChange } = resourcePreviews[0][1];
if (localChange === Change.None && remoteChange === Change.None) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
@@ -183,102 +243,18 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
}
}
protected async updateResourcePreview(resourcePreview: IExtensionResourcePreview, resource: URI, acceptedContent: string | null): Promise<IExtensionResourcePreview> {
if (isEqual(resource, this.localResource)) {
const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
return this.getPushPreview(remoteExtensions);
}
return {
...resourcePreview,
acceptedContent,
hasConflicts: false,
localChange: Change.Modified,
remoteChange: Change.Modified,
};
}
private async getPullPreview(remoteExtensions: ISyncExtension[] | null): Promise<IExtensionResourcePreview> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const localResource = this.localResource;
const localContent = this.format(localExtensions);
const remoteResource = this.remoteResource;
const previewResource = this.previewResource;
const acceptedResource = this.acceptedResource;
const previewContent = null;
if (remoteExtensions !== null) {
const mergeResult = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
return {
localResource,
localContent,
remoteResource,
remoteContent: this.format(remoteExtensions),
previewResource,
previewContent,
acceptedResource,
acceptedContent: 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,
};
} else {
return {
localResource,
localContent,
remoteResource,
remoteContent: null,
previewResource,
previewContent,
acceptedResource,
acceptedContent: previewContent,
added: [], removed: [], updated: [], remote: null, localExtensions, skippedExtensions: [],
localChange: Change.None,
remoteChange: Change.None,
hasConflicts: false,
};
}
}
private async getPushPreview(remoteExtensions: ISyncExtension[] | null): Promise<IExtensionResourcePreview> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const mergeResult = merge(localExtensions, null, null, [], ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
return {
localResource: this.localResource,
localContent: this.format(localExtensions),
remoteResource: this.remoteResource,
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
previewResource: this.previewResource,
previewContent: null,
acceptedResource: this.acceptedResource,
acceptedContent: 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,
};
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }];
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
const installedExtensions = await this.extensionManagementService.getInstalled();
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier)));
return this.format(localExtensions);
}
if (isEqual(this.remoteResource, uri) || isEqual(this.localResource, uri) || isEqual(this.acceptedResource, uri)) {
return this.resolvePreviewContent(uri);
}

View File

@@ -5,7 +5,7 @@
import {
IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService,
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, Change
} from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { Event } from 'vs/base/common/event';
@@ -16,7 +16,7 @@ import { IStringDictionary } from 'vs/base/common/collections';
import { edit } from 'vs/platform/userDataSync/common/content';
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
import { parse } from 'vs/base/common/json';
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IAcceptResult, IMergeResult, 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,11 +30,15 @@ import { CancellationToken } from 'vs/base/common/cancellation';
const argvStoragePrefx = 'globalState.argv.';
const argvProperties: string[] = ['locale'];
export interface IGlobalStateResourcePreview extends IResourcePreview {
interface IGlobalStateResourceMergeResult extends IAcceptResult {
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
readonly remote: IStringDictionary<IStorageValue> | null;
}
export interface IGlobalStateResourcePreview extends IResourcePreview {
readonly skippedStorageKeys: string[];
readonly localUserData: IGlobalState;
readonly previewResult: IGlobalStateResourceMergeResult;
}
interface ILastSyncUserData extends IRemoteUserData {
@@ -55,7 +59,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IEnvironmentService readonly environmentService: IEnvironmentService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IConfigurationService configurationService: IConfigurationService,
@@ -76,43 +80,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
);
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
const remoteContent = remoteUserData.syncData !== null ? remoteUserData.syncData.content : null;
const pullPreview = await this.getPullPreview(remoteContent, lastSyncUserData?.skippedStorageKeys || []);
return [pullPreview];
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
const remoteContent = remoteUserData.syncData !== null ? remoteUserData.syncData.content : null;
const pushPreview = await this.getPushPreview(remoteContent);
return [pushPreview];
}
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;
return [{
localResource: this.localResource,
localContent: this.format(localUserData),
remoteResource: this.remoteResource,
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
previewResource: this.previewResource,
previewContent: null,
acceptedResource: this.acceptedResource,
acceptedContent: null,
local,
remote: syncGlobalState.storage,
localUserData,
skippedStorageKeys: skipped,
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,
}];
}
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
@@ -125,30 +92,89 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`);
}
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 { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
const previewResult: IGlobalStateResourceMergeResult = {
content: null,
local,
remote,
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,
};
return [{
skippedStorageKeys: skipped,
localResource: this.localResource,
localContent: this.format(localGloablState),
localUserData: localGloablState,
remoteResource: this.remoteResource,
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
previewResource: this.previewResource,
previewContent: null,
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: this.acceptedResource,
acceptedContent: null,
local,
remote,
localUserData: localGloablState,
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,
}];
}
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IGlobalStateResourcePreview[], force: boolean): Promise<void> {
let { local, remote, localUserData, localChange, remoteChange, skippedStorageKeys } = resourcePreviews[0];
protected async getMergeResult(resourcePreview: IGlobalStateResourcePreview, token: CancellationToken): Promise<IMergeResult> {
return { ...resourcePreview.previewResult, hasConflicts: false };
}
protected async getAcceptResult(resourcePreview: IGlobalStateResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IGlobalStateResourceMergeResult> {
/* Accept local resource */
if (isEqual(resource, this.localResource)) {
return this.acceptLocal(resourcePreview);
}
/* Accept remote resource */
if (isEqual(resource, this.remoteResource)) {
return this.acceptRemote(resourcePreview);
}
/* Accept preview resource */
if (isEqual(resource, this.previewResource)) {
return resourcePreview.previewResult;
}
throw new Error(`Invalid Resource: ${resource.toString()}`);
}
private async acceptLocal(resourcePreview: IGlobalStateResourcePreview): Promise<IGlobalStateResourceMergeResult> {
return {
content: resourcePreview.localContent,
local: { added: {}, removed: [], updated: {} },
remote: resourcePreview.localUserData.storage,
localChange: Change.None,
remoteChange: Change.Modified,
};
}
private async acceptRemote(resourcePreview: IGlobalStateResourcePreview): Promise<IGlobalStateResourceMergeResult> {
if (resourcePreview.remoteContent !== null) {
const remoteGlobalState: IGlobalState = JSON.parse(resourcePreview.remoteContent);
const { local, remote } = merge(resourcePreview.localUserData.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), resourcePreview.skippedStorageKeys, this.logService);
return {
content: resourcePreview.remoteContent,
local,
remote,
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,
};
} else {
return {
content: resourcePreview.remoteContent,
local: { added: {}, removed: [], updated: {} },
remote: null,
localChange: Change.None,
remoteChange: Change.None,
};
}
}
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: [IGlobalStateResourcePreview, IGlobalStateResourceMergeResult][], force: boolean): Promise<void> {
let { localUserData, skippedStorageKeys } = resourcePreviews[0][0];
let { local, remote, localChange, remoteChange } = resourcePreviews[0][1];
if (localChange === Change.None && remoteChange === Change.None) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
@@ -178,93 +204,16 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
}
}
protected async updateResourcePreview(resourcePreview: IGlobalStateResourcePreview, resource: URI, acceptedContent: string | null): Promise<IGlobalStateResourcePreview> {
if (isEqual(this.localResource, resource)) {
return this.getPushPreview(resourcePreview.remoteContent);
}
if (isEqual(this.remoteResource, resource)) {
return this.getPullPreview(resourcePreview.remoteContent, resourcePreview.skippedStorageKeys);
}
return resourcePreview;
}
private async getPullPreview(remoteContent: string | null, skippedStorageKeys: string[]): Promise<IGlobalStateResourcePreview> {
const localGlobalState = await this.getLocalGlobalState();
const localResource = this.localResource;
const localContent = this.format(localGlobalState);
const remoteResource = this.remoteResource;
const previewResource = this.previewResource;
const acceptedResource = this.acceptedResource;
const previewContent = null;
if (remoteContent !== null) {
const remoteGlobalState: IGlobalState = JSON.parse(remoteContent);
const mergeResult = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), skippedStorageKeys, this.logService);
const { local, remote, skipped } = mergeResult;
return {
localResource,
localContent,
remoteResource,
remoteContent: this.format(remoteGlobalState),
previewResource,
previewContent,
acceptedResource,
acceptedContent: 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,
};
} else {
return {
localResource,
localContent,
remoteResource,
remoteContent: null,
previewResource,
previewContent,
acceptedResource,
acceptedContent: previewContent,
local: { added: {}, removed: [], updated: {} },
remote: null,
localUserData: localGlobalState,
skippedStorageKeys: [],
localChange: Change.None,
remoteChange: Change.None,
hasConflicts: false,
};
}
}
private async getPushPreview(remoteContent: string | null): Promise<IGlobalStateResourcePreview> {
const localUserData = await this.getLocalGlobalState();
const remoteGlobalState: IGlobalState = remoteContent ? JSON.parse(remoteContent) : null;
return {
localResource: this.localResource,
localContent: this.format(localUserData),
remoteResource: this.remoteResource,
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
previewResource: this.previewResource,
previewContent: null,
acceptedResource: this.acceptedResource,
acceptedContent: null,
local: { added: {}, removed: [], updated: {} },
remote: localUserData.storage,
localUserData,
skippedStorageKeys: [],
localChange: Change.None,
remoteChange: Change.Modified,
hasConflicts: false,
};
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }];
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) {
const localGlobalState = await this.getLocalGlobalState();
return this.format(localGlobalState);
}
if (isEqual(this.remoteResource, uri) || isEqual(this.localResource, uri) || isEqual(this.acceptedResource, uri)) {
return this.resolvePreviewContent(uri);
}

View File

@@ -7,10 +7,9 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo
import {
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource,
IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle,
IRemoteUserData, ISyncData, Change
IRemoteUserData, Change
} from 'vs/platform/userDataSync/common/userDataSync';
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse } from 'vs/base/common/json';
import { localize } from 'vs/nls';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -19,7 +18,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 { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } 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';
@@ -32,9 +31,14 @@ interface ISyncContent {
all?: string;
}
interface IKeybindingsResourcePreview extends IFileResourcePreview {
previewResult: IMergeResult;
}
export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 1;
/* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
protected readonly version: number = 2;
private readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'keybindings.json');
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
@@ -55,67 +59,7 @@ 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<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const previewContent = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
return [{
localResource: this.localResource,
fileContent,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remoteResource,
remoteContent: previewContent,
previewResource: this.previewResource,
previewContent,
acceptedResource: this.acceptedResource,
acceptedContent: previewContent,
localChange: previewContent !== null ? Change.Modified : Change.None,
remoteChange: Change.None,
hasConflicts: false,
}];
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const previewContent: string | null = fileContent ? fileContent.value.toString() : null;
return [{
localResource: this.localResource,
fileContent,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remoteResource,
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
previewResource: this.previewResource,
previewContent,
acceptedResource: this.acceptedResource,
acceptedContent: previewContent,
localChange: Change.None,
remoteChange: previewContent !== null ? Change.Modified : Change.None,
hasConflicts: false,
}];
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const previewContent = this.getKeybindingsContentFromSyncContent(syncData.content);
return [{
localResource: this.localResource,
fileContent,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remoteResource,
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
previewResource: this.previewResource,
previewContent,
acceptedResource: this.acceptedResource,
acceptedContent: previewContent,
localChange: previewContent !== null ? Change.Modified : Change.None,
remoteChange: previewContent !== null ? Change.Modified : Change.None,
hasConflicts: false,
}];
}
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IKeybindingsResourcePreview[]> {
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
const lastSyncContent: string | null = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
@@ -123,14 +67,15 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
const fileContent = await this.getLocalFileContent();
const formattingOptions = await this.getFormattingOptions();
let previewContent: string | null = null;
let mergedContent: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
if (remoteContent) {
const localContent: string = fileContent ? fileContent.value.toString() : '[]';
if (!localContent.trim() || this.hasErrors(localContent)) {
let localContent: string = fileContent ? fileContent.value.toString() : '[]';
localContent = localContent || '[]';
if (this.hasErrors(localContent)) {
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);
}
@@ -142,7 +87,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) {
previewContent = result.mergeContent;
mergedContent = result.mergeContent;
hasConflicts = result.hasConflicts;
hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
@@ -153,66 +98,126 @@ 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.`);
previewContent = fileContent.value.toString();
mergedContent = fileContent.value.toString();
hasRemoteChanged = true;
}
if (previewContent && !token.isCancellationRequested) {
await this.fileService.writeFile(this.previewResource, VSBuffer.fromString(previewContent));
}
return [{
localResource: this.localResource,
fileContent,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remoteResource,
remoteContent,
previewResource: this.previewResource,
previewContent,
acceptedResource: this.acceptedResource,
acceptedContent: previewContent,
hasConflicts,
const previewResult: IMergeResult = {
content: mergedContent,
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
hasConflicts
};
return [{
fileContent,
localResource: this.localResource,
localContent: fileContent ? fileContent.value.toString() : null,
localChange: previewResult.localChange,
remoteResource: this.remoteResource,
remoteContent,
remoteChange: previewResult.remoteChange,
previewResource: this.previewResource,
previewResult,
acceptedResource: this.acceptedResource,
}];
}
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
let { fileContent, acceptedContent: content, localChange, remoteChange } = resourcePreviews[0];
protected async getMergeResult(resourcePreview: IKeybindingsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
return resourcePreview.previewResult;
}
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);
protected async getAcceptResult(resourcePreview: IKeybindingsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
/* Accept local resource */
if (isEqual(resource, this.localResource)) {
return {
content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
localChange: Change.None,
remoteChange: Change.Modified,
};
}
/* Accept remote resource */
if (isEqual(resource, this.remoteResource)) {
return {
content: resourcePreview.remoteContent,
localChange: Change.Modified,
remoteChange: Change.None,
};
}
/* Accept preview resource */
if (isEqual(resource, this.previewResource)) {
if (content === undefined) {
return {
content: resourcePreview.previewResult.content,
localChange: resourcePreview.previewResult.localChange,
remoteChange: resourcePreview.previewResult.remoteChange,
};
} else {
return {
content,
localChange: Change.Modified,
remoteChange: Change.Modified,
};
}
}
if (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
if (fileContent) {
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
}
await this.updateLocalFileContent(content, fileContent, force);
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
}
throw new Error(`Invalid Resource: ${resource.toString()}`);
}
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, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
}
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IKeybindingsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
const { fileContent } = resourcePreviews[0][0];
let { content, localChange, remoteChange } = resourcePreviews[0][1];
// Delete the preview
try {
await this.fileService.del(this.previewResource);
} catch (e) { /* ignore */ }
} else {
if (localChange === Change.None && remoteChange === Change.None) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`);
}
if (content !== null) {
content = content.trim();
content = content || '[]';
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 (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
if (fileContent) {
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
}
await this.updateLocalFileContent(content || '[]', fileContent, force);
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
}
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, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
}
// Delete the preview
try {
await this.fileService.del(this.previewResource);
} catch (e) { /* ignore */ }
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`);
const lastSyncContent = content !== null || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null;
await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, machineId: remoteUserData.syncData!.machineId, content: lastSyncContent } : null });
const lastSyncContent = content !== null ? this.toSyncContent(content, null) : null;
await this.updateLastSyncUserData({
ref: remoteUserData.ref,
syncData: lastSyncContent ? {
version: remoteUserData.syncData ? remoteUserData.syncData.version : this.version,
machineId: remoteUserData.syncData!.machineId,
content: lastSyncContent
} : null
});
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`);
}
@@ -235,8 +240,9 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
return false;
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return [{ resource: joinPath(uri, 'keybindings.json'), comparableResource: this.file }];
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource;
return [{ resource: joinPath(uri, 'keybindings.json'), comparableResource }];
}
async resolveContent(uri: URI): Promise<string | null> {
@@ -263,7 +269,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
getKeybindingsContentFromSyncContent(syncContent: string): string | null {
try {
const parsed = <ISyncContent>JSON.parse(syncContent);
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {
if (!this.syncKeybindingsPerPlatform()) {
return isUndefined(parsed.all) ? null : parsed.all;
}
switch (OS) {
@@ -287,7 +293,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
} catch (e) {
this.logService.error(e);
}
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {
if (!this.syncKeybindingsPerPlatform()) {
parsed.all = keybindingsContent;
} else {
delete parsed.all;
@@ -306,4 +312,16 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
return JSON.stringify(parsed);
}
private syncKeybindingsPerPlatform(): boolean {
let userValue = this.configurationService.inspect<boolean>('settingsSync.keybindingsPerPlatform').userValue;
if (userValue !== undefined) {
return userValue;
}
userValue = this.configurationService.inspect<boolean>('sync.keybindingsPerPlatform').userValue;
if (userValue !== undefined) {
return userValue;
}
return this.configurationService.getValue<boolean>('settingsSync.keybindingsPerPlatform');
}
}

View File

@@ -23,12 +23,9 @@ export interface IMergeResult {
export function getIgnoredSettings(defaultIgnoredSettings: string[], configurationService: IConfigurationService, settingsContent?: string): string[] {
let value: string[] = [];
if (settingsContent) {
const setting = parse(settingsContent);
if (setting) {
value = setting['sync.ignoredSettings'];
}
value = getIgnoredSettingsFromContent(settingsContent);
} else {
value = configurationService.getValue<string[]>('sync.ignoredSettings');
value = getIgnoredSettingsFromConfig(configurationService);
}
const added: string[] = [], removed: string[] = [...getDisallowedIgnoredSettings()];
if (Array.isArray(value)) {
@@ -43,6 +40,22 @@ export function getIgnoredSettings(defaultIgnoredSettings: string[], configurati
return distinct([...defaultIgnoredSettings, ...added,].filter(setting => removed.indexOf(setting) === -1));
}
function getIgnoredSettingsFromConfig(configurationService: IConfigurationService): string[] {
let userValue = configurationService.inspect<string[]>('settingsSync.ignoredSettings').userValue;
if (userValue !== undefined) {
return userValue;
}
userValue = configurationService.inspect<string[]>('sync.ignoredSettings').userValue;
if (userValue !== undefined) {
return userValue;
}
return configurationService.getValue<string[]>('settingsSync.ignoredSettings') || [];
}
function getIgnoredSettingsFromContent(settingsContent: string): string[] {
const parsed = parse(settingsContent);
return parsed ? parsed['settingsSync.ignoredSettings'] || parsed['sync.ignoredSettings'] || [] : [];
}
export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string {
if (ignoredSettings.length) {

View File

@@ -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 { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } 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';
@@ -26,6 +26,10 @@ import { IStorageService } from 'vs/platform/storage/common/storage';
import { Edit } from 'vs/base/common/jsonFormatter';
import { setProperty, applyEdits } from 'vs/base/common/jsonEdit';
interface ISettingsResourcePreview extends IFileResourcePreview {
previewResult: IMergeResult;
}
export interface ISettingsSyncContent {
settings: string;
}
@@ -38,11 +42,12 @@ function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent {
export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 1;
private readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'settings.json');
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
/* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
protected readonly version: number = 2;
readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'settings.json');
readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
constructor(
@IFileService fileService: IFileService,
@@ -60,112 +65,25 @@ 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<IFileResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
let previewContent: string | null = null;
if (remoteSettingsSyncContent) {
// Update ignored settings from local file content
previewContent = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
}
return [{
localResource: this.localResource,
fileContent,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remoteResource,
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
previewResource: this.previewResource,
previewContent,
acceptedResource: this.acceptedResource,
acceptedContent: previewContent,
localChange: previewContent !== null ? Change.Modified : Change.None,
remoteChange: Change.None,
hasConflicts: false,
}];
}
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 previewContent: string | null = fileContent?.value.toString() || null;
if (previewContent) {
// Remove ignored settings
previewContent = updateIgnoredSettings(previewContent, '{}', ignoredSettings, formatUtils);
}
return [{
localResource: this.localResource,
fileContent,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remoteResource,
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
previewResource: this.previewResource,
previewContent,
acceptedResource: this.acceptedResource,
acceptedContent: previewContent,
localChange: Change.None,
remoteChange: previewContent !== null ? Change.Modified : Change.None,
hasConflicts: false,
}];
}
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 previewContent: string | null = null;
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
if (settingsSyncContent) {
previewContent = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
}
return [{
localResource: this.localResource,
fileContent,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.remoteResource,
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
previewResource: this.previewResource,
previewContent,
acceptedResource: this.acceptedResource,
acceptedContent: previewContent,
localChange: previewContent !== null ? Change.Modified : Change.None,
remoteChange: previewContent !== null ? Change.Modified : Change.None,
hasConflicts: false,
}];
}
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISettingsResourcePreview[]> {
const fileContent = await this.getLocalFileContent();
const formattingOptions = await this.getFormattingOptions();
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null;
const ignoredSettings = await this.getIgnoredSettings();
let acceptedContent: string | null = null;
let previewContent: string | null = null;
let mergedContent: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
if (remoteSettingsSyncContent) {
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
let localContent: string = fileContent ? fileContent.value.toString().trim() : '{}';
localContent = localContent || '{}';
this.validateContent(localContent);
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, [], formattingOptions);
acceptedContent = result.localContent || result.remoteContent;
mergedContent = result.localContent || result.remoteContent;
hasLocalChanged = result.localContent !== null;
hasRemoteChanged = result.remoteContent !== null;
hasConflicts = result.hasConflicts;
@@ -174,75 +92,127 @@ 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.`);
acceptedContent = fileContent.value.toString();
mergedContent = fileContent.value.toString();
hasRemoteChanged = true;
}
if (acceptedContent && !token.isCancellationRequested) {
// Remove the ignored settings from the preview.
previewContent = updateIgnoredSettings(acceptedContent, '{}', ignoredSettings, formattingOptions);
}
const previewResult = {
content: mergedContent,
localChange: hasLocalChanged ? Change.Modified : Change.None,
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
hasConflicts
};
return [{
localResource: this.localResource,
fileContent,
localResource: this.localResource,
localContent: fileContent ? fileContent.value.toString() : null,
localChange: previewResult.localChange,
remoteResource: this.remoteResource,
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
remoteChange: previewResult.remoteChange,
previewResource: this.previewResource,
previewContent,
previewResult,
acceptedResource: this.acceptedResource,
acceptedContent,
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
hasConflicts,
}];
}
protected async updateResourcePreview(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Promise<IFileResourcePreview> {
if (acceptedContent && (isEqual(resource, this.previewResource) || isEqual(resource, this.remoteResource))) {
const formatUtils = await this.getFormattingOptions();
// Add ignored settings from local file content
const ignoredSettings = await this.getIgnoredSettings();
acceptedContent = updateIgnoredSettings(acceptedContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
}
return super.updateResourcePreview(resourcePreview, resource, acceptedContent) as Promise<IFileResourcePreview>;
protected async getMergeResult(resourcePreview: ISettingsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
return {
...resourcePreview.previewResult,
// remove ignored settings from the preview content
content: resourcePreview.previewResult.content ? updateIgnoredSettings(resourcePreview.previewResult.content, '{}', ignoredSettings, formatUtils) : null
};
}
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
let { fileContent, acceptedContent: content, localChange, remoteChange } = resourcePreviews[0];
protected async getAcceptResult(resourcePreview: ISettingsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
if (content !== null) {
const formattingOptions = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
this.validateContent(content);
/* Accept local resource */
if (isEqual(resource, this.localResource)) {
return {
/* Remove ignored settings */
content: resourcePreview.fileContent ? updateIgnoredSettings(resourcePreview.fileContent.value.toString(), '{}', ignoredSettings, formattingOptions) : null,
localChange: Change.None,
remoteChange: Change.Modified,
};
}
if (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
if (fileContent) {
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
}
await this.updateLocalFileContent(content, fileContent, force);
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
}
if (remoteChange !== Change.None) {
const formatUtils = await this.getFormattingOptions();
// Update ignored settings from remote
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const ignoredSettings = await this.getIgnoredSettings(content);
content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils);
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`);
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
/* Accept remote resource */
if (isEqual(resource, this.remoteResource)) {
return {
/* Update ignored settings from local file content */
content: resourcePreview.remoteContent !== null ? updateIgnoredSettings(resourcePreview.remoteContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formattingOptions) : null,
localChange: Change.Modified,
remoteChange: Change.None,
};
}
/* Accept preview resource */
if (isEqual(resource, this.previewResource)) {
if (content === undefined) {
return {
content: resourcePreview.previewResult.content,
localChange: resourcePreview.previewResult.localChange,
remoteChange: resourcePreview.previewResult.remoteChange,
};
} else {
return {
/* Add ignored settings from local file content */
content: content !== null ? updateIgnoredSettings(content, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formattingOptions) : null,
localChange: Change.Modified,
remoteChange: Change.Modified,
};
}
}
// Delete the preview
try {
await this.fileService.del(this.previewResource);
} catch (e) { /* ignore */ }
} else {
throw new Error(`Invalid Resource: ${resource.toString()}`);
}
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ISettingsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
const { fileContent } = resourcePreviews[0][0];
let { content, localChange, remoteChange } = resourcePreviews[0][1];
if (localChange === Change.None && remoteChange === Change.None) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`);
}
content = content ? content.trim() : '{}';
content = content || '{}';
this.validateContent(content);
if (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
if (fileContent) {
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
}
await this.updateLocalFileContent(content, fileContent, force);
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
}
if (remoteChange !== Change.None) {
const formatUtils = await this.getFormattingOptions();
// Update ignored settings from remote
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const ignoredSettings = await this.getIgnoredSettings(content);
content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils);
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`);
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
}
// Delete the preview
try {
await this.fileService.del(this.previewResource);
} catch (e) { /* ignore */ }
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized settings...`);
await this.updateLastSyncUserData(remoteUserData);
@@ -267,8 +237,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
return false;
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return [{ resource: joinPath(uri, 'settings.json'), comparableResource: this.file }];
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource;
return [{ resource: joinPath(uri, 'settings.json'), comparableResource }];
}
async resolveContent(uri: URI): Promise<string | null> {

View File

@@ -10,17 +10,25 @@ import {
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, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } 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, basename, dirname } from 'vs/base/common/resources';
import { VSBuffer } from 'vs/base/common/buffer';
import { merge, IMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge';
import { merge, IMergeResult as ISnippetsMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { deepClone } from 'vs/base/common/objects';
interface ISnippetsResourcePreview extends IFileResourcePreview {
previewResult: IMergeResult;
}
interface ISnippetsAcceptedResourcePreview extends IFileResourcePreview {
acceptResult: IAcceptResult;
}
export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 1;
@@ -51,36 +59,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
this.triggerLocalChange();
}
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);
resourcePreviews.push(...this.getResourcePreviews(mergeResult, local, remoteSnippets));
}
return resourcePreviews;
}
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 resourcePreviews = this.getResourcePreviews(mergeResult, local, {});
return resourcePreviews;
}
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 resourcePreviews = this.getResourcePreviews(mergeResult, local, snippets);
return resourcePreviews;
}
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISnippetsResourcePreview[]> {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
@@ -96,102 +75,72 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
return this.getResourcePreviews(mergeResult, local, remoteSnippets || {});
}
protected async updateResourcePreview(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Promise<IFileResourcePreview> {
return {
...resourcePreview,
acceptedContent,
localChange: this.computeLocalChange(resourcePreview, resource, acceptedContent),
remoteChange: this.computeRemoteChange(resourcePreview, resource, acceptedContent),
};
protected async getMergeResult(resourcePreview: ISnippetsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
return resourcePreview.previewResult;
}
private computeLocalChange(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Change {
const isRemoteResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }));
const isPreviewResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder);
protected async getAcceptResult(resourcePreview: ISnippetsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
const previewExists = acceptedContent !== null;
const remoteExists = resourcePreview.remoteContent !== null;
const localExists = resourcePreview.fileContent !== null;
if (isRemoteResourceAccepted) {
if (remoteExists && localExists) {
return Change.Modified;
}
if (remoteExists && !localExists) {
return Change.Added;
}
if (!remoteExists && localExists) {
return Change.Deleted;
}
return Change.None;
/* Accept local resource */
if (isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }))) {
return {
content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
localChange: Change.None,
remoteChange: resourcePreview.fileContent
? resourcePreview.remoteContent !== null ? Change.Modified : Change.Added
: Change.Deleted
};
}
if (isPreviewResourceAccepted) {
if (previewExists && localExists) {
return Change.Modified;
}
if (previewExists && !localExists) {
return Change.Added;
}
if (!previewExists && localExists) {
return Change.Deleted;
}
return Change.None;
/* Accept remote resource */
if (isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }))) {
return {
content: resourcePreview.remoteContent,
localChange: resourcePreview.remoteContent !== null
? resourcePreview.fileContent ? Change.Modified : Change.Added
: Change.Deleted,
remoteChange: Change.None,
};
}
return Change.None;
/* Accept preview resource */
if (isEqualOrParent(resource, this.syncPreviewFolder)) {
if (content === undefined) {
return {
content: resourcePreview.previewResult.content,
localChange: resourcePreview.previewResult.localChange,
remoteChange: resourcePreview.previewResult.remoteChange,
};
} else {
return {
content,
localChange: content === null
? resourcePreview.fileContent !== null ? Change.Deleted : Change.None
: Change.Modified,
remoteChange: content === null
? resourcePreview.remoteContent !== null ? Change.Deleted : Change.None
: Change.Modified
};
}
}
throw new Error(`Invalid Resource: ${resource.toString()}`);
}
private computeRemoteChange(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Change {
const isLocalResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }));
const isPreviewResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder);
const previewExists = acceptedContent !== null;
const remoteExists = resourcePreview.remoteContent !== null;
const localExists = resourcePreview.fileContent !== null;
if (isLocalResourceAccepted) {
if (remoteExists && localExists) {
return Change.Modified;
}
if (remoteExists && !localExists) {
return Change.Deleted;
}
if (!remoteExists && localExists) {
return Change.Added;
}
return Change.None;
}
if (isPreviewResourceAccepted) {
if (previewExists && remoteExists) {
return Change.Modified;
}
if (previewExists && !remoteExists) {
return Change.Added;
}
if (!previewExists && remoteExists) {
return Change.Deleted;
}
return Change.None;
}
return Change.None;
}
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
if (resourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) {
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ISnippetsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
const accptedResourcePreviews: ISnippetsAcceptedResourcePreview[] = resourcePreviews.map(([resourcePreview, acceptResult]) => ({ ...resourcePreview, acceptResult }));
if (accptedResourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
}
if (resourcePreviews.some(({ localChange }) => localChange !== Change.None)) {
if (accptedResourcePreviews.some(({ localChange }) => localChange !== Change.None)) {
// back up all snippets
await this.updateLocalBackup(resourcePreviews);
await this.updateLocalSnippets(resourcePreviews, force);
await this.updateLocalBackup(accptedResourcePreviews);
await this.updateLocalSnippets(accptedResourcePreviews, force);
}
if (resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) {
remoteUserData = await this.updateRemoteSnippets(resourcePreviews, remoteUserData, force);
if (accptedResourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) {
remoteUserData = await this.updateRemoteSnippets(accptedResourcePreviews, remoteUserData, force);
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
@@ -201,7 +150,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`);
}
for (const { previewResource } of resourcePreviews) {
for (const { previewResource } of accptedResourcePreviews) {
// Delete the preview
try {
await this.fileService.del(previewResource);
@@ -210,29 +159,39 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
private getResourcePreviews(mergeResult: IMergeResult, localFileContent: IStringDictionary<IFileContent>, remoteSnippets: IStringDictionary<string>): IFileResourcePreview[] {
const resourcePreviews: Map<string, IFileResourcePreview> = new Map<string, IFileResourcePreview>();
private getResourcePreviews(snippetsMergeResult: ISnippetsMergeResult, localFileContent: IStringDictionary<IFileContent>, remoteSnippets: IStringDictionary<string>): ISnippetsResourcePreview[] {
const resourcePreviews: Map<string, ISnippetsResourcePreview> = new Map<string, ISnippetsResourcePreview>();
/* Snippets added remotely -> add locally */
for (const key of Object.keys(mergeResult.local.added)) {
for (const key of Object.keys(snippetsMergeResult.local.added)) {
const previewResult: IMergeResult = {
content: snippetsMergeResult.local.added[key],
hasConflicts: false,
localChange: Change.Added,
remoteChange: Change.None,
};
resourcePreviews.set(key, {
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
fileContent: null,
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
localContent: null,
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
remoteContent: remoteSnippets[key],
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: mergeResult.local.added[key],
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
acceptedContent: mergeResult.local.added[key],
hasConflicts: false,
localChange: Change.Added,
remoteChange: Change.None
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
});
}
/* Snippets updated remotely -> update locally */
for (const key of Object.keys(mergeResult.local.updated)) {
for (const key of Object.keys(snippetsMergeResult.local.updated)) {
const previewResult: IMergeResult = {
content: snippetsMergeResult.local.updated[key],
hasConflicts: false,
localChange: Change.Modified,
remoteChange: Change.None,
};
resourcePreviews.set(key, {
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
fileContent: localFileContent[key],
@@ -240,17 +199,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
remoteContent: remoteSnippets[key],
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: mergeResult.local.updated[key],
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
acceptedContent: mergeResult.local.updated[key],
hasConflicts: false,
localChange: Change.Modified,
remoteChange: Change.None
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
});
}
/* Snippets removed remotely -> remove locally */
for (const key of mergeResult.local.removed) {
for (const key of snippetsMergeResult.local.removed) {
const previewResult: IMergeResult = {
content: null,
hasConflicts: false,
localChange: Change.Deleted,
remoteChange: Change.None,
};
resourcePreviews.set(key, {
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
fileContent: localFileContent[key],
@@ -258,17 +221,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
remoteContent: null,
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: null,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
acceptedContent: null,
hasConflicts: false,
localChange: Change.Deleted,
remoteChange: Change.None
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
});
}
/* Snippets added locally -> add remotely */
for (const key of Object.keys(mergeResult.remote.added)) {
for (const key of Object.keys(snippetsMergeResult.remote.added)) {
const previewResult: IMergeResult = {
content: snippetsMergeResult.remote.added[key],
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.Added,
};
resourcePreviews.set(key, {
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
fileContent: localFileContent[key],
@@ -276,17 +243,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
remoteContent: null,
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: mergeResult.remote.added[key],
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
acceptedContent: mergeResult.remote.added[key],
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.Added
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
});
}
/* Snippets updated locally -> update remotely */
for (const key of Object.keys(mergeResult.remote.updated)) {
for (const key of Object.keys(snippetsMergeResult.remote.updated)) {
const previewResult: IMergeResult = {
content: snippetsMergeResult.remote.updated[key],
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.Modified,
};
resourcePreviews.set(key, {
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
fileContent: localFileContent[key],
@@ -294,17 +265,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
remoteContent: remoteSnippets[key],
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: mergeResult.remote.updated[key],
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
acceptedContent: mergeResult.remote.updated[key],
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.Modified
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
});
}
/* Snippets removed locally -> remove remotely */
for (const key of mergeResult.remote.removed) {
for (const key of snippetsMergeResult.remote.removed) {
const previewResult: IMergeResult = {
content: null,
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.Deleted,
};
resourcePreviews.set(key, {
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
fileContent: null,
@@ -312,17 +287,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
remoteContent: remoteSnippets[key],
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: null,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
acceptedContent: null,
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.Deleted
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
});
}
/* Snippets with conflicts */
for (const key of mergeResult.conflicts) {
for (const key of snippetsMergeResult.conflicts) {
const previewResult: IMergeResult = {
content: localFileContent[key] ? localFileContent[key].value.toString() : null,
hasConflicts: true,
localChange: localFileContent[key] ? Change.Modified : Change.Added,
remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added
};
resourcePreviews.set(key, {
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
fileContent: localFileContent[key] || null,
@@ -330,18 +309,22 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
remoteContent: remoteSnippets[key] || null,
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
acceptedContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
hasConflicts: true,
localChange: localFileContent[key] ? Change.Modified : Change.Added,
remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
});
}
/* Unmodified Snippets */
for (const key of Object.keys(localFileContent)) {
if (!resourcePreviews.has(key)) {
const previewResult: IMergeResult = {
content: localFileContent[key] ? localFileContent[key].value.toString() : null,
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.None
};
resourcePreviews.set(key, {
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
fileContent: localFileContent[key] || null,
@@ -349,12 +332,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
remoteContent: remoteSnippets[key] || null,
previewResource: joinPath(this.syncPreviewFolder, key),
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
acceptedContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
hasConflicts: false,
localChange: Change.None,
remoteChange: Change.None
previewResult,
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
});
}
}
@@ -362,7 +343,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
return [...resourcePreviews.values()];
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
let content = await super.resolveContent(uri);
if (content) {
const syncData = this.parseSyncData(content);
@@ -373,7 +354,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
const resource = joinPath(uri, snippet);
const comparableResource = joinPath(this.snippetsFolder, snippet);
const exists = await this.fileService.exists(comparableResource);
result.push({ resource, comparableResource: exists ? comparableResource : undefined });
result.push({ resource, comparableResource: exists ? comparableResource : joinPath(this.syncPreviewFolder, snippet).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }) });
}
return result;
}
@@ -427,8 +408,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
}
private async updateLocalSnippets(resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
for (const { fileContent, acceptedContent: content, localResource, remoteResource, localChange } of resourcePreviews) {
private async updateLocalSnippets(resourcePreviews: ISnippetsAcceptedResourcePreview[], force: boolean): Promise<void> {
for (const { fileContent, acceptResult, localResource, remoteResource, localChange } of resourcePreviews) {
if (localChange !== Change.None) {
const key = remoteResource ? basename(remoteResource) : basename(localResource!);
const resource = joinPath(this.snippetsFolder, key);
@@ -443,31 +424,31 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
// Added
else if (localChange === Change.Added) {
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource));
await this.fileService.createFile(resource, VSBuffer.fromString(content!), { overwrite: force });
await this.fileService.createFile(resource, VSBuffer.fromString(acceptResult.content!), { overwrite: force });
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!), force ? undefined : fileContent!);
await this.fileService.writeFile(resource, VSBuffer.fromString(acceptResult.content!), force ? undefined : fileContent!);
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource));
}
}
}
}
private async updateRemoteSnippets(resourcePreviews: IFileResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise<IRemoteUserData> {
private async updateRemoteSnippets(resourcePreviews: ISnippetsAcceptedResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise<IRemoteUserData> {
const currentSnippets: IStringDictionary<string> = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : {};
const newSnippets: IStringDictionary<string> = deepClone(currentSnippets);
for (const { acceptedContent: content, localResource, remoteResource, remoteChange } of resourcePreviews) {
for (const { acceptResult, 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!;
newSnippets[key] = acceptResult.content!;
}
}
}

View File

@@ -6,7 +6,7 @@
import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isPromiseCanceledError } from 'vs/base/common/errors';
@@ -16,6 +16,8 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { localize } from 'vs/nls';
import { toLocalISOString } from 'vs/base/common/date';
import { URI } from 'vs/base/common/uri';
import { isEqual } from 'vs/base/common/resources';
type AutoSyncClassification = {
sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -27,6 +29,7 @@ type AutoSyncEnablementClassification = {
type AutoSyncErrorClassification = {
code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
service: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
const enablementKey = 'sync.enable';
@@ -41,7 +44,8 @@ export class UserDataAutoSyncEnablementService extends Disposable {
constructor(
@IStorageService protected readonly storageService: IStorageService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserDataSyncStoreManagementService protected readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService
) {
super();
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
@@ -58,7 +62,7 @@ export class UserDataAutoSyncEnablementService extends Disposable {
}
canToggleEnablement(): boolean {
return this.environmentService.sync === undefined;
return this.userDataSyncStoreManagementService.userDataSyncStore !== undefined && this.environmentService.sync === undefined;
}
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
@@ -83,7 +87,21 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
private readonly _onError: Emitter<UserDataSyncError> = this._register(new Emitter<UserDataSyncError>());
readonly onError: Event<UserDataSyncError> = this._onError.event;
private lastSyncUrl: URI | undefined;
private get syncUrl(): URI | undefined {
const value = this.storageService.get(storeUrlKey, StorageScope.GLOBAL);
return value ? URI.parse(value) : undefined;
}
private set syncUrl(syncUrl: URI | undefined) {
if (syncUrl) {
this.storageService.store(storeUrlKey, syncUrl.toString(), StorageScope.GLOBAL);
} else {
this.storageService.remove(storeUrlKey, StorageScope.GLOBAL);
}
}
constructor(
@IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@@ -94,13 +112,12 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
@IStorageService storageService: IStorageService,
@IEnvironmentService environmentService: IEnvironmentService
) {
super(storageService, environmentService);
super(storageService, environmentService, userDataSyncStoreManagementService);
this.syncTriggerDelayer = this._register(new Delayer<void>(0));
this.lastSyncUrl = this.syncUrl;
this.syncUrl = userDataSyncStoreManagementService.userDataSyncStore?.url;
if (userDataSyncStoreService.userDataSyncStore) {
storageService.store(storeUrlKey, userDataSyncStoreService.userDataSyncStore.url.toString(), StorageScope.GLOBAL);
if (userDataSyncStoreManagementService.userDataSyncStore) {
if (this.isEnabled()) {
this.logService.info('Auto Sync is enabled.');
} else {
@@ -123,7 +140,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
const { enabled, message } = this.isAutoSyncEnabled();
if (enabled) {
if (this.autoSync.value === undefined) {
this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService);
this.autoSync.value = new AutoSync(this.lastSyncUrl, 1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreManagementService, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService);
this.autoSync.value.register(this.autoSync.value.onDidStartSync(() => this.lastSyncTriggerTime = new Date().getTime()));
this.autoSync.value.register(this.autoSync.value.onDidFinishSync(e => this.onDidFinishSync(e)));
if (this.startAutoSync()) {
@@ -164,6 +181,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
async turnOn(): Promise<void> {
this.stopDisableMachineEventually();
this.lastSyncUrl = this.syncUrl;
this.setEnablement(true);
}
@@ -218,7 +236,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
// Log to telemetry
if (userDataSyncError instanceof UserDataAutoSyncError) {
this.telemetryService.publicLog2<{ code: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code });
this.telemetryService.publicLog2<{ code: string, service: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
}
// Session got expired
@@ -228,7 +246,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
}
// Turned off from another device
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) {
else if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
}
@@ -252,13 +270,20 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
// Incompatible Local Content
else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleLocalContent) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because server has {0} content with newer version than of client. Requires client upgrade.', userDataSyncError.resource);
this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with newer version than of client. Requires client upgrade.`);
}
// Incompatible Remote Content
else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleRemoteContent) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because server has {0} content with older version than of client. Requires server reset.', userDataSyncError.resource);
this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with older version than of client. Requires server reset.`);
}
// Service changed
else if (userDataSyncError.code === UserDataSyncErrorCode.ServiceChanged || userDataSyncError.code === UserDataSyncErrorCode.DefaultServiceChanged) {
await this.turnOff(false, true /* force soft turnoff on error */, true /* do not disable machine */);
await this.turnOn();
this.logService.info('Auto Sync: Sync Service changed. Turned off auto sync, reset local state and turned on auto sync.');
}
else {
@@ -342,7 +367,9 @@ class AutoSync extends Disposable {
private syncPromise: CancelablePromise<void> | undefined;
constructor(
private readonly lastSyncUrl: URI | undefined,
private readonly interval: number /* in milliseconds */,
private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
private readonly userDataSyncStoreService: IUserDataSyncStoreService,
private readonly userDataSyncService: IUserDataSyncService,
private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
@@ -357,7 +384,7 @@ class AutoSync extends Disposable {
this._register(toDisposable(() => {
if (this.syncPromise) {
this.syncPromise.cancel();
this.logService.info('Auto sync: Canelled sync that is in progress');
this.logService.info('Auto sync: Cancelled sync that is in progress');
this.syncPromise = undefined;
}
if (this.syncTask) {
@@ -394,6 +421,20 @@ class AutoSync extends Disposable {
return this.syncPromise;
}
private hasSyncServiceChanged(): boolean {
return this.lastSyncUrl !== undefined && !isEqual(this.lastSyncUrl, this.userDataSyncStoreManagementService.userDataSyncStore?.url);
}
private async hasDefaultServiceChanged(): Promise<boolean> {
const previous = await this.userDataSyncStoreManagementService.getPreviousUserDataSyncStore();
const current = this.userDataSyncStoreManagementService.userDataSyncStore;
// check if defaults changed
return !!current && !!previous &&
(!isEqual(current.defaultUrl, previous.defaultUrl) ||
!isEqual(current.insidersUrl, previous.insidersUrl) ||
!isEqual(current.stableUrl, previous.stableUrl));
}
private async doSync(reason: string, token: CancellationToken): Promise<void> {
this.logService.info(`Auto Sync: Triggered by ${reason}`);
this._onDidStartSync.fire();
@@ -407,14 +448,30 @@ class AutoSync extends Disposable {
// Server has no data but this machine was synced before
if (manifest === null && await this.userDataSyncService.hasPreviouslySynced()) {
// Sync was turned off in the cloud
throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
if (this.hasSyncServiceChanged()) {
if (await this.hasDefaultServiceChanged()) {
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
} else {
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
}
} else {
// Sync was turned off in the cloud
throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
}
}
const sessionId = this.storageService.get(sessionIdKey, StorageScope.GLOBAL);
// Server session is different from client session
if (sessionId && manifest && sessionId !== manifest.session) {
throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
if (this.hasSyncServiceChanged()) {
if (await this.hasDefaultServiceChanged()) {
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
} else {
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
}
} else {
throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
}
}
const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined);

View File

@@ -13,13 +13,11 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { ILogService } from 'vs/platform/log/common/log';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { URI } from 'vs/base/common/uri';
import { joinPath, isEqualOrParent } from 'vs/base/common/resources';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
import { distinct } from 'vs/base/common/arrays';
import { isArray, isString, isObject } from 'vs/base/common/types';
import { IHeaders } from 'vs/base/parts/request/common/request';
@@ -43,21 +41,25 @@ export function registerConfiguration(): IDisposable {
const ignoredExtensionsSchemaId = 'vscode://schemas/ignoredExtensions';
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
id: 'sync',
id: 'settingsSync',
order: 30,
title: localize('sync', "Sync"),
title: localize('settings sync', "Settings Sync"),
type: 'object',
properties: {
'sync.keybindingsPerPlatform': {
'settingsSync.keybindingsPerPlatform': {
type: 'boolean',
description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."),
description: localize('settingsSync.keybindingsPerPlatform', "Synchronize keybindings for each platform."),
default: true,
scope: ConfigurationScope.APPLICATION,
tags: ['sync', 'usesOnlineServices']
},
'sync.ignoredExtensions': {
'sync.keybindingsPerPlatform': {
type: 'boolean',
deprecationMessage: localize('sync.keybindingsPerPlatform.deprecated', "Deprecated, use settingsSync.keybindingsPerPlatform instead"),
},
'settingsSync.ignoredExtensions': {
'type': 'array',
'description': localize('sync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."),
markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."),
$ref: ignoredExtensionsSchemaId,
'default': [],
'scope': ConfigurationScope.APPLICATION,
@@ -65,9 +67,13 @@ export function registerConfiguration(): IDisposable {
disallowSyncIgnore: true,
tags: ['sync', 'usesOnlineServices']
},
'sync.ignoredSettings': {
'sync.ignoredExtensions': {
'type': 'array',
description: localize('sync.ignoredSettings', "Configure settings to be ignored while synchronizing."),
deprecationMessage: localize('sync.ignoredExtensions.deprecated', "Deprecated, use settingsSync.ignoredExtensions instead"),
},
'settingsSync.ignoredSettings': {
'type': 'array',
description: localize('settingsSync.ignoredSettings', "Configure settings to be ignored while synchronizing."),
'default': [],
'scope': ConfigurationScope.APPLICATION,
$ref: ignoredSettingsSchemaId,
@@ -75,6 +81,10 @@ export function registerConfiguration(): IDisposable {
uniqueItems: true,
disallowSyncIgnore: true,
tags: ['sync', 'usesOnlineServices']
},
'sync.ignoredSettings': {
'type': 'array',
deprecationMessage: localize('sync.ignoredSettings.deprecated', "Deprecated, use settingsSync.ignoredSettings instead"),
}
}
});
@@ -110,8 +120,11 @@ export interface IUserData {
export type IAuthenticationProvider = { id: string, scopes: string[] };
export interface IUserDataSyncStore {
url: URI;
authenticationProviders: IAuthenticationProvider[];
readonly url: URI;
readonly defaultUrl: URI;
readonly stableUrl: URI | undefined;
readonly insidersUrl: URI | undefined;
readonly authenticationProviders: IAuthenticationProvider[];
}
export function isAuthenticationProvider(thing: any): thing is IAuthenticationProvider {
@@ -121,27 +134,6 @@ export function isAuthenticationProvider(thing: any): thing is IAuthenticationPr
&& isArray(thing.scopes);
}
export function getUserDataSyncStore(productService: IProductService, configurationService: IConfigurationService): IUserDataSyncStore | undefined {
const value = {
...(productService[CONFIGURATION_SYNC_STORE_KEY] || {}),
...(configurationService.getValue<ConfigurationSyncStore>(CONFIGURATION_SYNC_STORE_KEY) || {})
};
if (value
&& isString((value as any).url) // {{SQL CARBON EDIT}} strict-nulls
&& isObject((value as any).authenticationProviders) // {{SQL CARBON EDIT}} strict-nulls
&& Object.keys((value as any).authenticationProviders).every(authenticationProviderId => isArray((value as any).authenticationProviders[authenticationProviderId].scopes)) // {{SQL CARBON EDIT}} strict-nulls
) {
return {
url: joinPath(URI.parse((value as any).url), 'v1'), // {{SQL CARBON EDIT}} strict-nulls
authenticationProviders: Object.keys((value as any).authenticationProviders).reduce<IAuthenticationProvider[]>((result, id) => { // {{SQL CARBON EDIT}} strict-nulls
result.push({ id, scopes: (value as any).authenticationProviders[id].scopes }); // {{SQL CARBON EDIT}} strict-nulls
return result;
}, [])
};
}
return undefined;
}
export const enum SyncResource {
Settings = 'settings',
Keybindings = 'keybindings',
@@ -161,11 +153,20 @@ export interface IResourceRefHandle {
created: number;
}
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
export type ServerResource = SyncResource | 'machines';
export interface IUserDataSyncStoreService {
export type UserDataSyncStoreType = 'insiders' | 'stable';
export const IUserDataSyncStoreManagementService = createDecorator<IUserDataSyncStoreManagementService>('IUserDataSyncStoreManagementService');
export interface IUserDataSyncStoreManagementService {
readonly _serviceBrand: undefined;
readonly userDataSyncStore: IUserDataSyncStore | undefined;
switch(type: UserDataSyncStoreType): Promise<void>;
getPreviousUserDataSyncStore(): Promise<IUserDataSyncStore | undefined>;
}
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
export interface IUserDataSyncStoreService {
readonly _serviceBrand: undefined;
readonly onDidChangeDonotMakeRequestsUntil: Event<void>;
readonly donotMakeRequestsUntil: Date | undefined;
@@ -220,6 +221,8 @@ export enum UserDataSyncErrorCode {
NoRef = 'NoRef',
TurnedOff = 'TurnedOff',
SessionExpired = 'SessionExpired',
ServiceChanged = 'ServiceChanged',
DefaultServiceChanged = 'DefaultServiceChanged',
LocalTooManyRequests = 'LocalTooManyRequests',
LocalPreconditionFailed = 'LocalPreconditionFailed',
LocalInvalidContent = 'LocalInvalidContent',
@@ -356,14 +359,12 @@ export interface IUserDataSynchroniser {
readonly onDidChangeLocal: Event<void>;
pull(): Promise<void>;
push(): Promise<void>;
sync(manifest: IUserDataManifest | null, headers: IHeaders): Promise<void>;
replace(uri: URI): Promise<boolean>;
stop(): Promise<void>;
preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise<ISyncResourcePreview | null>;
accept(resource: URI, content: string | null): Promise<ISyncResourcePreview | null>;
accept(resource: URI, content?: string | null): Promise<ISyncResourcePreview | null>;
merge(resource: URI): Promise<ISyncResourcePreview | null>;
discard(resource: URI): Promise<ISyncResourcePreview | null>;
apply(force: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null>;
@@ -375,7 +376,7 @@ export interface IUserDataSynchroniser {
resolveContent(resource: URI): Promise<string | null>;
getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>;
getMachineId(syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
}
@@ -400,12 +401,14 @@ export interface ISyncTask {
export interface IManualSyncTask extends IDisposable {
readonly id: string;
readonly status: SyncStatus;
readonly manifest: IUserDataManifest | null;
readonly onSynchronizeResources: Event<[SyncResource, URI[]][]>;
preview(): Promise<[SyncResource, ISyncResourcePreview][]>;
accept(resource: URI, content: string | null): Promise<[SyncResource, ISyncResourcePreview][]>;
merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]>;
merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]>;
apply(): Promise<[SyncResource, ISyncResourcePreview][]>;
pull(): Promise<void>;
push(): Promise<void>;
@@ -428,10 +431,12 @@ export interface IUserDataSyncService {
readonly lastSyncTime: number | undefined;
readonly onDidChangeLastSyncTime: Event<number>;
readonly onDidResetRemote: Event<void>;
readonly onDidResetLocal: Event<void>;
createSyncTask(): Promise<ISyncTask>;
createManualSyncTask(): Promise<IManualSyncTask>;
pull(): Promise<void>;
replace(uri: URI): Promise<void>;
reset(): Promise<void>;
resetRemote(): Promise<void>;
@@ -440,11 +445,11 @@ export interface IUserDataSyncService {
hasLocalData(): Promise<boolean>;
hasPreviouslySynced(): Promise<boolean>;
resolveContent(resource: URI): Promise<string | null>;
accept(resource: SyncResource, conflictResource: URI, content: string | null, apply: boolean): Promise<void>;
accept(resource: SyncResource, conflictResource: URI, content: string | null | undefined, apply: boolean): Promise<void>;
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>;
getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
}

View File

@@ -5,7 +5,7 @@
import { IServerChannel, IChannel, IPCServer } from 'vs/base/parts/ipc/common/ipc';
import { Event, Emitter } from 'vs/base/common/event';
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest, IUserDataSyncStoreManagementService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
import { URI } from 'vs/base/common/uri';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
@@ -26,6 +26,8 @@ export class UserDataSyncChannel implements IServerChannel {
case 'onDidChangeLocal': return this.service.onDidChangeLocal;
case 'onDidChangeLastSyncTime': return this.service.onDidChangeLastSyncTime;
case 'onSyncErrors': return this.service.onSyncErrors;
case 'onDidResetLocal': return this.service.onDidResetLocal;
case 'onDidResetRemote': return this.service.onDidResetRemote;
}
throw new Error(`Event not found: ${event}`);
}
@@ -46,7 +48,6 @@ export class UserDataSyncChannel implements IServerChannel {
case 'createManualSyncTask': return this.createManualSyncTask();
case 'pull': return this.service.pull();
case 'replace': return this.service.replace(URI.revive(args[0]));
case 'reset': return this.service.reset();
case 'resetRemote': return this.service.resetRemote();
@@ -63,11 +64,11 @@ export class UserDataSyncChannel implements IServerChannel {
throw new Error('Invalid call');
}
private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null }> {
private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null, status: SyncStatus }> {
const manualSyncTask = await this.service.createManualSyncTask();
const manualSyncTaskChannel = new ManualSyncTaskChannel(manualSyncTask, this.logService);
this.server.registerChannel(`manualSyncTask-${manualSyncTask.id}`, manualSyncTaskChannel);
return { id: manualSyncTask.id, manifest: manualSyncTask.manifest };
return { id: manualSyncTask.id, manifest: manualSyncTask.manifest, status: manualSyncTask.status };
}
}
@@ -101,10 +102,12 @@ class ManualSyncTaskChannel implements IServerChannel {
case 'accept': return this.manualSyncTask.accept(URI.revive(args[0]), args[1]);
case 'merge': return this.manualSyncTask.merge(URI.revive(args[0]));
case 'discard': return this.manualSyncTask.discard(URI.revive(args[0]));
case 'discardConflicts': return this.manualSyncTask.discardConflicts();
case 'apply': return this.manualSyncTask.apply();
case 'pull': return this.manualSyncTask.pull();
case 'push': return this.manualSyncTask.push();
case 'stop': return this.manualSyncTask.stop();
case '_getStatus': return this.manualSyncTask.status;
case 'dispose': return this.manualSyncTask.dispose();
}
throw new Error('Invalid call');
@@ -225,6 +228,9 @@ export class UserDataSyncMachinesServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncMachinesService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChange': return this.service.onDidChange;
}
throw new Error(`Event not found: ${event}`);
}
@@ -261,3 +267,18 @@ export class UserDataSyncAccountServiceChannel implements IServerChannel {
}
}
export class UserDataSyncStoreManagementServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncStoreManagementService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'switch': return this.service.switch(args[0]);
case 'getPreviousUserDataSyncStore': return this.service.getPreviousUserDataSyncStore();
}
throw new Error('Invalid call');
}
}

View File

@@ -14,6 +14,7 @@ import { localize } from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
import { PlatformToString, isWeb, Platform, platform } from 'vs/base/common/platform';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
import { Event, Emitter } from 'vs/base/common/event';
interface IMachineData {
id: string;
@@ -32,6 +33,8 @@ export const IUserDataSyncMachinesService = createDecorator<IUserDataSyncMachine
export interface IUserDataSyncMachinesService {
_serviceBrand: any;
readonly onDidChange: Event<void>;
getMachines(manifest?: IUserDataManifest): Promise<IUserDataSyncMachine[]>;
addCurrentMachine(manifest?: IUserDataManifest): Promise<void>;
@@ -49,6 +52,9 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
_serviceBrand: any;
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
private readonly currentMachineIdPromise: Promise<string>;
private userData: IUserData | null = null;
@@ -118,7 +124,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
}
const namePrefix = `${this.productService.nameLong} (${PlatformToString(isWeb ? Platform.Web : platform)})`;
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`);
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d+)`);
let nameIndex = 0;
for (const machine of machines) {
const matches = nameRegEx.exec(machine.name);
@@ -141,6 +147,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
const content = JSON.stringify(machinesData);
const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null);
this.userData = { ref, content };
this._onDidChange.fire();
}
private async readUserData(manifest?: IUserDataManifest): Promise<IUserData> {

View File

@@ -5,7 +5,7 @@
import {
IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode,
UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID, MergeState, Change
UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID, MergeState, Change, IUserDataSyncStoreManagementService
} from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -28,6 +28,8 @@ import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async
import { isPromiseCanceledError } from 'vs/base/common/errors';
type SyncErrorClassification = {
code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
service: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
executionId?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
@@ -67,6 +69,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private _onDidChangeLastSyncTime: Emitter<number> = this._register(new Emitter<number>());
readonly onDidChangeLastSyncTime: Event<number> = this._onDidChangeLastSyncTime.event;
private _onDidResetLocal = this._register(new Emitter<void>());
readonly onDidResetLocal = this._onDidResetLocal.event;
private _onDidResetRemote = this._register(new Emitter<void>());
readonly onDidResetRemote = this._onDidResetRemote.event;
private readonly settingsSynchroniser: SettingsSynchroniser;
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
private readonly snippetsSynchroniser: SnippetsSynchroniser;
@@ -75,6 +82,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@@ -89,7 +97,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser];
this.updateStatus();
if (this.userDataSyncStoreService.userDataSyncStore) {
if (this.userDataSyncStoreManagementService.userDataSyncStore) {
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeConflicts, () => undefined)))(() => this.updateConflicts()));
}
@@ -98,36 +106,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.resource)));
}
async pull(): Promise<void> {
await this.checkEnablement();
try {
for (const synchroniser of this.synchronisers) {
await synchroniser.pull();
}
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource });
}
throw error;
}
}
async push(): Promise<void> {
await this.checkEnablement();
try {
for (const synchroniser of this.synchronisers) {
await synchroniser.push();
}
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource });
}
throw error;
}
}
async createSyncTask(): Promise<ISyncTask> {
await this.checkEnablement();
@@ -136,9 +114,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
try {
manifest = await this.userDataSyncStoreService.manifest(createSyncHeaders(executionId));
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
}
error = UserDataSyncError.toUserDataSyncError(error);
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
throw error;
}
@@ -173,9 +150,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
try {
manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
}
error = UserDataSyncError.toUserDataSyncError(error);
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
throw error;
}
@@ -220,9 +196,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
}
error = UserDataSyncError.toUserDataSyncError(error);
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
throw error;
} finally {
this.updateStatus();
@@ -256,7 +231,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
async accept(syncResource: SyncResource, resource: URI, content: string | null, apply: boolean): Promise<void> {
async accept(syncResource: SyncResource, resource: URI, content: string | null | undefined, apply: boolean): Promise<void> {
await this.checkEnablement();
const synchroniser = this.getSynchroniser(syncResource);
await synchroniser.accept(resource, content);
@@ -283,7 +258,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.getSynchroniser(resource).getLocalSyncResourceHandles();
}
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle);
}
@@ -316,6 +291,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
} catch (e) {
this.logService.error(e);
}
this._onDidResetRemote.fire();
}
async resetLocal(): Promise<void> {
@@ -329,6 +305,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.logService.error(e);
}
}
this._onDidResetLocal.fire();
this.logService.info('Did reset the local sync state.');
}
@@ -367,7 +344,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
private computeStatus(): SyncStatus {
if (!this.userDataSyncStoreService.userDataSyncStore) {
if (!this.userDataSyncStoreManagementService.userDataSyncStore) {
return SyncStatus.Uninitialized;
}
if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) {
@@ -417,7 +394,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
private async checkEnablement(): Promise<void> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
if (!this.userDataSyncStoreManagementService.userDataSyncStore) {
throw new Error('Not enabled');
}
}
@@ -435,6 +412,16 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
private isDisposed: boolean = false;
get status(): SyncStatus {
if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) {
return SyncStatus.HasConflicts;
}
if (this.synchronisers.some(s => s.status === SyncStatus.Syncing)) {
return SyncStatus.Syncing;
}
return SyncStatus.Idle;
}
constructor(
readonly id: string,
readonly manifest: IUserDataManifest | null,
@@ -452,60 +439,48 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
if (!this.previewsPromise) {
this.previewsPromise = createCancelablePromise(token => this.getPreviews(token));
}
this.previews = await this.previewsPromise;
if (!this.previews) {
this.previews = await this.previewsPromise;
}
return this.previews;
}
async accept(resource: URI, content: string | null): Promise<[SyncResource, ISyncResourcePreview][]> {
async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> {
return this.performAction(resource, sychronizer => sychronizer.accept(resource, content));
}
async merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
return this.performAction(resource, sychronizer => sychronizer.merge(resource));
async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
if (resource) {
return this.performAction(resource, sychronizer => sychronizer.merge(resource));
} else {
return this.mergeAll();
}
}
async discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
return this.performAction(resource, sychronizer => sychronizer.discard(resource));
}
private async performAction(resource: URI, action: (synchroniser: IUserDataSynchroniser) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> {
if (!this.previews) {
throw new Error('Missing preview. Create preview and try again.');
}
const index = this.previews.findIndex(([, preview]) => preview.resourcePreviews.some(({ localResource, previewResource, remoteResource }) =>
isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource)));
if (index === -1) {
return this.previews;
if (this.synchronizingResources.length) {
throw new Error('Cannot discard while synchronizing resources');
}
const [syncResource, previews] = this.previews[index];
const resourcePreview = previews.resourcePreviews.find(({ localResource, remoteResource, previewResource }) => isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
if (!resourcePreview) {
return this.previews;
const conflictResources: URI[] = [];
for (const [, syncResourcePreview] of this.previews) {
for (const resourcePreview of syncResourcePreview.resourcePreviews) {
if (resourcePreview.mergeState === MergeState.Conflict) {
conflictResources.push(resourcePreview.previewResource);
}
}
}
let synchronizingResources = this.synchronizingResources.find(s => s[0] === syncResource);
if (!synchronizingResources) {
synchronizingResources = [syncResource, []];
this.synchronizingResources.push(synchronizingResources);
for (const resource of conflictResources) {
await this.discard(resource);
}
if (!synchronizingResources[1].some(s => isEqual(s, resourcePreview.localResource))) {
synchronizingResources[1].push(resourcePreview.localResource);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!;
const preview = await action(synchroniser);
preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1);
const i = this.synchronizingResources.findIndex(s => s[0] === syncResource);
this.synchronizingResources[i][1].splice(synchronizingResources[1].findIndex(r => isEqual(r, resourcePreview.localResource)), 1);
if (!synchronizingResources[1].length) {
this.synchronizingResources.splice(i, 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
return this.previews;
}
@@ -555,8 +530,7 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
this._onSynchronizeResources.fire(this.synchronizingResources);
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
for (const resourcePreview of preview.resourcePreviews) {
const content = await synchroniser.resolveContent(resourcePreview.remoteResource);
await synchroniser.accept(resourcePreview.remoteResource, content);
await synchroniser.accept(resourcePreview.remoteResource);
}
await synchroniser.apply(true, this.syncHeaders);
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
@@ -577,8 +551,7 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
this._onSynchronizeResources.fire(this.synchronizingResources);
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
for (const resourcePreview of preview.resourcePreviews) {
const content = await synchroniser.resolveContent(resourcePreview.localResource);
await synchroniser.accept(resourcePreview.localResource, content);
await synchroniser.accept(resourcePreview.localResource);
}
await synchroniser.apply(true, this.syncHeaders);
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
@@ -600,6 +573,80 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
this.reset();
}
private async performAction(resource: URI, action: (synchroniser: IUserDataSynchroniser) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
if (!this.previews) {
throw new Error('Missing preview. Create preview and try again.');
}
const index = this.previews.findIndex(([, preview]) => preview.resourcePreviews.some(({ localResource, previewResource, remoteResource }) =>
isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource)));
if (index === -1) {
return this.previews;
}
const [syncResource, previews] = this.previews[index];
const resourcePreview = previews.resourcePreviews.find(({ localResource, remoteResource, previewResource }) => isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
if (!resourcePreview) {
return this.previews;
}
let synchronizingResources = this.synchronizingResources.find(s => s[0] === syncResource);
if (!synchronizingResources) {
synchronizingResources = [syncResource, []];
this.synchronizingResources.push(synchronizingResources);
}
if (!synchronizingResources[1].some(s => isEqual(s, resourcePreview.localResource))) {
synchronizingResources[1].push(resourcePreview.localResource);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!;
const preview = await action(synchroniser);
preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1);
const i = this.synchronizingResources.findIndex(s => s[0] === syncResource);
this.synchronizingResources[i][1].splice(synchronizingResources[1].findIndex(r => isEqual(r, resourcePreview.localResource)), 1);
if (!synchronizingResources[1].length) {
this.synchronizingResources.splice(i, 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
return this.previews;
}
private async mergeAll(): Promise<[SyncResource, ISyncResourcePreview][]> {
if (!this.previews) {
throw new Error('You need to create preview before merging or applying');
}
if (this.synchronizingResources.length) {
throw new Error('Cannot merge or apply while synchronizing resources');
}
const previews: [SyncResource, ISyncResourcePreview][] = [];
for (const [syncResource, preview] of this.previews) {
this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]);
this._onSynchronizeResources.fire(this.synchronizingResources);
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
/* merge those which are not yet merged */
let newPreview: ISyncResourcePreview | null = preview;
for (const resourcePreview of preview.resourcePreviews) {
if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) {
newPreview = await synchroniser.merge(resourcePreview.previewResource);
}
}
if (newPreview) {
previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview));
}
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
this.previews = previews;
return this.previews;
}
private async getPreviews(token: CancellationToken): Promise<[SyncResource, ISyncResourcePreview][]> {
const result: [SyncResource, ISyncResourcePreview][] = [];
for (const synchroniser of this.synchronisers) {

View File

@@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, } from 'vs/base/common/lifecycle';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID } from 'vs/platform/userDataSync/common/userDataSync';
import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync';
import { IRequestService, asText, isSuccess as isSuccessContext, asJson } from 'vs/platform/request/common/request';
import { joinPath, relativePath } from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IProductService } from 'vs/platform/product/common/productService';
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
@@ -20,18 +20,117 @@ import { generateUuid } from 'vs/base/common/uuid';
import { isWeb } from 'vs/base/common/platform';
import { Emitter, Event } from 'vs/base/common/event';
import { createCancelablePromise, timeout, CancelablePromise } from 'vs/base/common/async';
import { isString, isObject, isArray } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
const SYNC_SERVICE_URL_TYPE = 'sync.store.url.type';
const SYNC_PREVIOUS_STORE = 'sync.previous.store';
const DONOT_MAKE_REQUESTS_UNTIL_KEY = 'sync.donot-make-requests-until';
const USER_SESSION_ID_KEY = 'sync.user-session-id';
const MACHINE_SESSION_ID_KEY = 'sync.machine-session-id';
const REQUEST_SESSION_LIMIT = 100;
const REQUEST_SESSION_INTERVAL = 1000 * 60 * 5; /* 5 minutes */
type UserDataSyncStore = IUserDataSyncStore & { defaultType?: UserDataSyncStoreType; type?: UserDataSyncStoreType };
export abstract class AbstractUserDataSyncStoreManagementService extends Disposable implements IUserDataSyncStoreManagementService {
_serviceBrand: any;
readonly userDataSyncStore: UserDataSyncStore | undefined;
constructor(
@IProductService protected readonly productService: IProductService,
@IConfigurationService protected readonly configurationService: IConfigurationService,
@IStorageService protected readonly storageService: IStorageService,
) {
super();
this.userDataSyncStore = this.toUserDataSyncStore(productService[CONFIGURATION_SYNC_STORE_KEY], configurationService.getValue<ConfigurationSyncStore>(CONFIGURATION_SYNC_STORE_KEY));
}
protected toUserDataSyncStore(productStore: ConfigurationSyncStore | undefined, configuredStore?: ConfigurationSyncStore): UserDataSyncStore | undefined {
const value: Partial<ConfigurationSyncStore> = { ...(productStore || {}), ...(configuredStore || {}) };
if (value
&& isString(value.url)
&& isObject(value.authenticationProviders)
&& Object.keys(value.authenticationProviders).every(authenticationProviderId => isArray(value!.authenticationProviders![authenticationProviderId].scopes))
) {
const syncStore = value as ConfigurationSyncStore;
const type: UserDataSyncStoreType | undefined = this.storageService.get(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL) as UserDataSyncStoreType | undefined;
const url = configuredStore?.url
|| (type === 'insiders' ? syncStore.insidersUrl : type === 'stable' ? syncStore.stableUrl : undefined)
|| syncStore.url;
return {
url: URI.parse(url),
type,
defaultType: syncStore.url === syncStore.insidersUrl ? 'insiders' : syncStore.url === syncStore.stableUrl ? 'stable' : undefined,
defaultUrl: URI.parse(syncStore.url),
stableUrl: syncStore.stableUrl ? URI.parse(syncStore.stableUrl) : undefined,
insidersUrl: syncStore.insidersUrl ? URI.parse(syncStore.insidersUrl) : undefined,
authenticationProviders: Object.keys(syncStore.authenticationProviders).reduce<IAuthenticationProvider[]>((result, id) => {
result.push({ id, scopes: syncStore!.authenticationProviders[id].scopes });
return result;
}, [])
};
}
return undefined;
}
abstract switch(type: UserDataSyncStoreType): Promise<void>;
abstract getPreviousUserDataSyncStore(): Promise<IUserDataSyncStore | undefined>;
}
export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService {
private readonly previousConfigurationSyncStore: ConfigurationSyncStore | undefined;
constructor(
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService storageService: IStorageService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
) {
super(productService, configurationService, storageService);
const previousConfigurationSyncStore = this.storageService.get(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL);
if (previousConfigurationSyncStore) {
this.previousConfigurationSyncStore = JSON.parse(previousConfigurationSyncStore);
}
const syncStore = this.productService[CONFIGURATION_SYNC_STORE_KEY];
if (syncStore) {
this.storageService.store(SYNC_PREVIOUS_STORE, JSON.stringify(syncStore), StorageScope.GLOBAL);
} else {
this.storageService.remove(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL);
}
if (this.userDataSyncStore) {
logService.info('Using settings sync service', this.userDataSyncStore.url.toString());
}
}
async switch(type: UserDataSyncStoreType): Promise<void> {
if (type !== this.userDataSyncStore?.type) {
if (type === this.userDataSyncStore?.defaultType) {
this.storageService.remove(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL);
} else {
this.storageService.store(SYNC_SERVICE_URL_TYPE, type, StorageScope.GLOBAL);
}
}
}
async getPreviousUserDataSyncStore(): Promise<IUserDataSyncStore | undefined> {
return this.toUserDataSyncStore(this.previousConfigurationSyncStore);
}
}
export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService {
_serviceBrand: any;
readonly userDataSyncStore: IUserDataSyncStore | undefined;
private readonly userDataSyncStoreUrl: URI | undefined;
private authToken: { token: string, type: string } | undefined;
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
private readonly session: RequestsSession;
@@ -49,15 +148,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
constructor(
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IRequestService private readonly requestService: IRequestService,
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.userDataSyncStore = getUserDataSyncStore(productService, configurationService);
this.userDataSyncStoreUrl = this.userDataSyncStoreManagementService.userDataSyncStore ? joinPath(this.userDataSyncStoreManagementService.userDataSyncStore.url, 'v1') : undefined;
this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService)
.then(uuid => {
const headers: IHeaders = {
@@ -109,63 +208,50 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
async getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]> {
if (!this.userDataSyncStore) {
if (!this.userDataSyncStoreUrl) {
throw new Error('No settings sync store url configured.');
}
const uri = joinPath(this.userDataSyncStore.url, 'resource', resource);
const uri = joinPath(this.userDataSyncStoreUrl, 'resource', resource);
const headers: IHeaders = {};
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, [], CancellationToken.None);
const result = await asJson<{ url: string, created: number }[]>(context) || [];
return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ }));
}
async resolveContent(resource: ServerResource, ref: string): Promise<string | null> {
if (!this.userDataSyncStore) {
if (!this.userDataSyncStoreUrl) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, ref).toString();
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, ref).toString();
const headers: IHeaders = {};
headers['Cache-Control'] = 'no-cache';
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const context = await this.request({ type: 'GET', url, headers }, [], CancellationToken.None);
const content = await asText(context);
return content;
}
async delete(resource: ServerResource): Promise<void> {
if (!this.userDataSyncStore) {
if (!this.userDataSyncStoreUrl) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString();
const headers: IHeaders = {};
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
await this.request({ type: 'DELETE', url, headers }, [], CancellationToken.None);
}
async read(resource: ServerResource, oldValue: IUserData | null, headers: IHeaders = {}): Promise<IUserData> {
if (!this.userDataSyncStore) {
if (!this.userDataSyncStoreUrl) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, 'latest').toString();
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, 'latest').toString();
headers = { ...headers };
// Disable caching as they are cached by synchronisers
headers['Cache-Control'] = 'no-cache';
@@ -173,17 +259,13 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
headers['If-None-Match'] = oldValue.ref;
}
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
const context = await this.request({ type: 'GET', url, headers }, [304], CancellationToken.None);
if (context.res.statusCode === 304) {
// There is no new value. Hence return the old value.
return oldValue!;
}
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const ref = context.res.headers['etag'];
if (!ref) {
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, context.res.headers[HEADER_OPERATION_ID]);
@@ -193,22 +275,18 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
async write(resource: ServerResource, data: string, ref: string | null, headers: IHeaders = {}): Promise<string> {
if (!this.userDataSyncStore) {
if (!this.userDataSyncStoreUrl) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString();
headers = { ...headers };
headers['Content-Type'] = 'text/plain';
if (ref) {
headers['If-Match'] = ref;
}
const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const context = await this.request({ type: 'POST', url, data, headers }, [], CancellationToken.None);
const newRef = context.res.headers['etag'];
if (!newRef) {
@@ -218,18 +296,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
async manifest(headers: IHeaders = {}): Promise<IUserDataManifest | null> {
if (!this.userDataSyncStore) {
if (!this.userDataSyncStoreUrl) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(this.userDataSyncStore.url, 'manifest').toString();
const url = joinPath(this.userDataSyncStoreUrl, 'manifest').toString();
headers = { ...headers };
headers['Content-Type'] = 'application/json';
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
const context = await this.request({ type: 'GET', url, headers }, [], CancellationToken.None);
const manifest = await asJson<IUserDataManifest>(context);
const currentSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
@@ -253,18 +328,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
async clear(): Promise<void> {
if (!this.userDataSyncStore) {
if (!this.userDataSyncStoreUrl) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(this.userDataSyncStore.url, 'resource').toString();
const url = joinPath(this.userDataSyncStoreUrl, 'resource').toString();
const headers: IHeaders = { 'Content-Type': 'text/plain' };
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
}
await this.request({ type: 'DELETE', url, headers }, [], CancellationToken.None);
// clear cached session.
this.clearSession();
@@ -275,7 +346,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL);
}
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
private async request(options: IRequestOptions, successCodes: number[], token: CancellationToken): Promise<IRequestContext> {
if (!this.authToken) {
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, undefined);
}
@@ -309,7 +380,8 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const operationId = context.res.headers[HEADER_OPERATION_ID];
const requestInfo = { url: options.url, status: context.res.statusCode, 'execution-id': options.headers[HEADER_EXECUTION_ID], 'operation-id': operationId };
if (isSuccess(context)) {
const isSuccess = isSuccessContext(context) || (context.res.statusCode && successCodes.indexOf(context.res.statusCode) !== -1);
if (isSuccess) {
this.logService.trace('Request succeeded', requestInfo);
} else {
this.logService.info('Request failed', requestInfo);
@@ -349,6 +421,10 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
}
if (!isSuccess) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, operationId);
}
return context;
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
@@ -16,6 +16,7 @@ import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/us
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
constructor(
@IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@IUserDataSyncService userDataSyncService: IUserDataSyncService,
@@ -27,7 +28,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
@IStorageService storageService: IStorageService,
@IEnvironmentService environmentService: IEnvironmentService,
) {
super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService);
super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService);
this._register(Event.debounce<string, string[]>(Event.any<string>(
Event.map(electronService.onWindowFocus, () => 'windowFocus'),

View File

@@ -207,33 +207,6 @@ suite('GlobalStateSync', () => {
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' } });
});
test('first time sync - push', async () => {
updateStorage('a', 'value1', testClient);
updateStorage('b', 'value2', testClient);
await testObject.push();
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
const { content } = await testClient.read(testObject.resource);
assert.ok(content !== null);
const actual = parseGlobalState(content!);
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } });
});
test('first time sync - pull', async () => {
updateStorage('a', 'value1', client2);
updateStorage('b', 'value2', client2);
await client2.sync();
await testObject.pull();
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
assert.equal(readStorage('a', testClient), 'value1');
assert.equal(readStorage('b', testClient), 'value2');
});
function parseGlobalState(content: string): IGlobalState {
const syncData: ISyncData = JSON.parse(content);
return JSON.parse(syncData.content);

View File

@@ -61,6 +61,45 @@ suite('KeybindingsSync', () => {
assert.deepEqual(server.requests, []);
});
test('when keybindings file is empty and remote has no changes', async () => {
const fileService = client.instantiationService.get(IFileService);
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
await fileService.writeFile(keybindingsResource, VSBuffer.fromString(''));
await testObject.sync(await client.manifest());
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]');
assert.equal(testObject.getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!), '[]');
assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), '');
});
test('when keybindings file is empty and remote has changes', async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const content = JSON.stringify([
{
'key': 'shift+cmd+w',
'command': 'workbench.action.closeAllEditors',
}
]);
await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString(content));
await client2.sync();
const fileService = client.instantiationService.get(IFileService);
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
await fileService.writeFile(keybindingsResource, VSBuffer.fromString(''));
await testObject.sync(await client.manifest());
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), content);
assert.equal(testObject.getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!), content);
assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), content);
});
test('when keybindings file is created after first sync', async () => {
const fileService = client.instantiationService.get(IFileService);
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, ISyncData } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, ISyncData, SyncStatus } 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 { SettingsSynchroniser, ISettingsSyncContent } from 'vs/platform/userDataSync/common/settingsSync';
@@ -15,32 +15,30 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { Event } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
suite('SettingsSync', () => {
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
'id': 'settingsSync',
'type': 'object',
'properties': {
'settingsSync.machine': {
'type': 'string',
'scope': ConfigurationScope.MACHINE
},
'settingsSync.machineOverridable': {
'type': 'string',
'scope': ConfigurationScope.MACHINE_OVERRIDABLE
}
}
});
suite('SettingsSync - Auto', () => {
const disposableStore = new DisposableStore();
const server = new UserDataSyncTestServer();
let client: UserDataSyncClient;
let testObject: SettingsSynchroniser;
suiteSetup(() => {
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
'id': 'settingsSync',
'type': 'object',
'properties': {
'settingsSync.machine': {
'type': 'string',
'scope': ConfigurationScope.MACHINE
},
'settingsSync.machineOverridable': {
'type': 'string',
'scope': ConfigurationScope.MACHINE_OVERRIDABLE
}
}
});
});
setup(async () => {
client = disposableStore.add(new UserDataSyncClient(server));
await client.setUp(true);
@@ -81,6 +79,61 @@ suite('SettingsSync', () => {
assert.deepEqual(server.requests, []);
});
test('when settings file is empty and remote has no changes', async () => {
const fileService = client.instantiationService.get(IFileService);
const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource;
await fileService.writeFile(settingsResource, VSBuffer.fromString(''));
await testObject.sync(await client.manifest());
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}');
assert.equal(testObject.parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, '{}');
assert.equal((await fileService.readFile(settingsResource)).value.toString(), '');
});
test('when settings file is empty and remote has changes', async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
const content =
`{
// Always
"files.autoSave": "afterDelay",
"files.simpleDialog.enable": true,
// Workbench
"workbench.colorTheme": "GitHub Sharp",
"workbench.tree.indent": 20,
"workbench.colorCustomizations": {
"editorLineNumber.activeForeground": "#ff0000",
"[GitHub Sharp]": {
"statusBarItem.remoteBackground": "#24292E",
"editorPane.background": "#f3f1f11a"
}
},
"gitBranch.base": "remote-repo/master",
// Experimental
"workbench.view.experimental.allowMovingToNewContainer": true,
}`;
await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content));
await client2.sync();
const fileService = client.instantiationService.get(IFileService);
const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource;
await fileService.writeFile(settingsResource, VSBuffer.fromString(''));
await testObject.sync(await client.manifest());
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, content);
assert.equal(testObject.parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, content);
assert.equal((await fileService.readFile(settingsResource)).value.toString(), content);
});
test('when settings file is created after first sync', async () => {
const fileService = client.instantiationService.get(IFileService);
@@ -128,7 +181,7 @@ suite('SettingsSync', () => {
"workbench.view.experimental.allowMovingToNewContainer": true,
}`;
await updateSettings(expected);
await updateSettings(expected, client);
await testObject.sync(await client.manifest());
const { content } = await client.read(testObject.resource);
@@ -151,7 +204,7 @@ suite('SettingsSync', () => {
"settingsSync.machine": "someValue",
"settingsSync.machineOverridable": "someValue"
}`;
await updateSettings(settingsContent);
await updateSettings(settingsContent, client);
await testObject.sync(await client.manifest());
@@ -182,7 +235,7 @@ suite('SettingsSync', () => {
// Machine
"settingsSync.machineOverridable": "someValue"
}`;
await updateSettings(settingsContent);
await updateSettings(settingsContent, client);
await testObject.sync(await client.manifest());
@@ -213,7 +266,7 @@ suite('SettingsSync', () => {
"settingsSync.machineOverridable": "someValue",
"files.simpleDialog.enable": true,
}`;
await updateSettings(settingsContent);
await updateSettings(settingsContent, client);
await testObject.sync(await client.manifest());
@@ -237,7 +290,7 @@ suite('SettingsSync', () => {
"settingsSync.machine": "someValue",
"settingsSync.machineOverridable": "someValue"
}`;
await updateSettings(settingsContent);
await updateSettings(settingsContent, client);
await testObject.sync(await client.manifest());
@@ -255,7 +308,7 @@ suite('SettingsSync', () => {
"settingsSync.machine": "someValue",
"settingsSync.machineOverridable": "someValue",
}`;
await updateSettings(settingsContent);
await updateSettings(settingsContent, client);
await testObject.sync(await client.manifest());
@@ -274,14 +327,14 @@ suite('SettingsSync', () => {
"files.simpleDialog.enable": true,
}`;
await updateSettings(content);
await updateSettings(content, client);
await testObject.sync(await client.manifest());
const promise = Event.toPromise(testObject.onDidChangeLocal);
await updateSettings(`{
"files.autoSave": "off",
"files.simpleDialog.enable": true,
}`);
}`, client);
await promise;
});
@@ -302,12 +355,12 @@ suite('SettingsSync', () => {
"workbench.colorTheme": "GitHub Sharp",
// Ignored
"sync.ignoredSettings": [
"settingsSync.ignoredSettings": [
"editor.fontFamily",
"terminal.integrated.shell.osx"
]
}`;
await updateSettings(settingsContent);
await updateSettings(settingsContent, client);
await testObject.sync(await client.manifest());
@@ -323,7 +376,7 @@ suite('SettingsSync', () => {
"workbench.colorTheme": "GitHub Sharp",
// Ignored
"sync.ignoredSettings": [
"settingsSync.ignoredSettings": [
"editor.fontFamily",
"terminal.integrated.shell.osx"
]
@@ -347,7 +400,7 @@ suite('SettingsSync', () => {
"workbench.colorTheme": "GitHub Sharp",
// Ignored
"sync.ignoredSettings": [
"settingsSync.ignoredSettings": [
"editor.fontFamily",
"terminal.integrated.shell.osx"
],
@@ -355,7 +408,7 @@ suite('SettingsSync', () => {
// Machine
"settingsSync.machine": "someValue",
}`;
await updateSettings(settingsContent);
await updateSettings(settingsContent, client);
await testObject.sync(await client.manifest());
@@ -371,7 +424,7 @@ suite('SettingsSync', () => {
"workbench.colorTheme": "GitHub Sharp",
// Ignored
"sync.ignoredSettings": [
"settingsSync.ignoredSettings": [
"editor.fontFamily",
"terminal.integrated.shell.osx"
],
@@ -402,7 +455,7 @@ suite('SettingsSync', () => {
"workbench.view.experimental.allowMovingToNewContainer": true,
}`;
await updateSettings(expected);
await updateSettings(expected, client);
try {
await testObject.sync(await client.manifest());
@@ -413,15 +466,109 @@ suite('SettingsSync', () => {
}
});
function parseSettings(content: string): string {
const syncData: ISyncData = JSON.parse(content);
const settingsSyncContent: ISettingsSyncContent = JSON.parse(syncData.content);
return settingsSyncContent.settings;
}
test('sync when there are conflicts', async () => {
const client2 = disposableStore.add(new UserDataSyncClient(server));
await client2.setUp(true);
await updateSettings(JSON.stringify({
'a': 1,
'b': 2,
'settingsSync.ignoredSettings': ['a']
}), client2);
await client2.sync();
async function updateSettings(content: string): Promise<void> {
await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content));
}
await updateSettings(JSON.stringify({
'a': 2,
'b': 1,
'settingsSync.ignoredSettings': ['a']
}), client);
await testObject.sync(await client.manifest());
assert.equal(testObject.status, SyncStatus.HasConflicts);
assert.equal(testObject.conflicts[0].localResource.toString(), testObject.localResource);
const fileService = client.instantiationService.get(IFileService);
const mergeContent = (await fileService.readFile(testObject.conflicts[0].previewResource)).value.toString();
assert.deepEqual(JSON.parse(mergeContent), {
'b': 1,
'settingsSync.ignoredSettings': ['a']
});
});
});
suite('SettingsSync - Manual', () => {
const disposableStore = new DisposableStore();
const server = new UserDataSyncTestServer();
let client: UserDataSyncClient;
let testObject: SettingsSynchroniser;
setup(async () => {
client = disposableStore.add(new UserDataSyncClient(server));
await client.setUp(true);
testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser;
disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear()));
});
teardown(() => disposableStore.clear());
test('do not sync ignored settings', async () => {
const settingsContent =
`{
// Always
"files.autoSave": "afterDelay",
"files.simpleDialog.enable": true,
// Editor
"editor.fontFamily": "Fira Code",
// Terminal
"terminal.integrated.shell.osx": "some path",
// Workbench
"workbench.colorTheme": "GitHub Sharp",
// Ignored
"settingsSync.ignoredSettings": [
"editor.fontFamily",
"terminal.integrated.shell.osx"
]
}`;
await updateSettings(settingsContent, client);
let preview = await testObject.preview(await client.manifest());
assert.equal(testObject.status, SyncStatus.Syncing);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.apply(false);
const { content } = await client.read(testObject.resource);
assert.ok(content !== null);
const actual = parseSettings(content!);
assert.deepEqual(actual, `{
// Always
"files.autoSave": "afterDelay",
"files.simpleDialog.enable": true,
// Workbench
"workbench.colorTheme": "GitHub Sharp",
// Ignored
"settingsSync.ignoredSettings": [
"editor.fontFamily",
"terminal.integrated.shell.osx"
]
}`);
});
});
function parseSettings(content: string): string {
const syncData: ISyncData = JSON.parse(content);
const settingsSyncContent: ISettingsSyncContent = JSON.parse(syncData.content);
return settingsSyncContent.settings;
}
async function updateSettings(content: string, client: UserDataSyncClient): Promise<void> {
await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content));
await client.instantiationService.get(IConfigurationService).reloadConfiguration();
}

View File

@@ -613,35 +613,6 @@ suite('SnippetsSync', () => {
assert.deepEqual(actual, { 'typescript.json': tsSnippet1 });
});
test('first time sync - push', async () => {
await updateSnippet('html.json', htmlSnippet1, testClient);
await updateSnippet('typescript.json', tsSnippet1, testClient);
await testObject.push();
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
const { content } = await testClient.read(testObject.resource);
assert.ok(content !== null);
const actual = parseSnippets(content!);
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 });
});
test('first time sync - pull', async () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await testObject.pull();
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
const actual1 = await readSnippet('html.json', testClient);
assert.equal(actual1, htmlSnippet1);
const actual2 = await readSnippet('typescript.json', testClient);
assert.equal(actual2, tsSnippet1);
});
test('sync global and language snippet', async () => {
await updateSnippet('global.code-snippets', globalSnippet, client2);
await updateSnippet('html.json', htmlSnippet1, client2);

View File

@@ -4,18 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData, Change, USER_DATA_SYNC_SCHEME, IUserDataManifest, MergeState } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, Change, USER_DATA_SYNC_SCHEME, IUserDataManifest, MergeState, IResourcePreview as IBaseResourcePreview } 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, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IAcceptResult, IMergeResult, 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';
import { IFileService } from 'vs/platform/files/common/files';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { VSBuffer } from 'vs/base/common/buffer';
import { isEqual, joinPath } from 'vs/base/common/resources';
const resource = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'testResource', path: `/current.json` });
interface ITestResourcePreview extends IResourcePreview {
ref: string;
}
class TestSynchroniser extends AbstractSynchroniser {
@@ -28,6 +32,7 @@ class TestSynchroniser extends AbstractSynchroniser {
protected readonly version: number = 1;
private cancelled: boolean = false;
readonly localResource = joinPath(this.environmentService.userRoamingDataHome, 'testResource.json');
protected getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
if (this.failWhenGettingLatestRemoteUserData) {
@@ -48,33 +53,95 @@ class TestSynchroniser extends AbstractSynchroniser {
return super.doSync(remoteUserData, lastSyncUserData, apply);
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IResourcePreview[]> {
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
}
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestResourcePreview[]> {
if (this.syncResult.hasError) {
throw new Error('failed');
}
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
let fileContent = null;
try {
fileContent = await this.fileService.readFile(this.localResource);
} catch (error) { }
return [{
localResource: this.localResource,
localContent: fileContent ? fileContent.value.toString() : null,
remoteResource: this.localResource.with(({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' })),
remoteContent: remoteUserData.syncData ? remoteUserData.syncData.content : null,
previewResource: this.localResource.with(({ scheme: USER_DATA_SYNC_SCHEME, authority: 'preview' })),
ref: remoteUserData.ref,
localChange: Change.Modified,
remoteChange: Change.Modified,
acceptedResource: this.localResource.with(({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })),
}];
}
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, preview: IResourcePreview[], forcePush: boolean): Promise<void> {
if (preview[0]?.acceptedContent) {
await this.applyRef(preview[0].acceptedContent);
protected async getMergeResult(resourcePreview: ITestResourcePreview, token: CancellationToken): Promise<IMergeResult> {
return {
content: resourcePreview.ref,
localChange: Change.Modified,
remoteChange: Change.Modified,
hasConflicts: this.syncResult.hasConflicts,
};
}
protected async getAcceptResult(resourcePreview: ITestResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
if (isEqual(resource, resourcePreview.localResource)) {
return {
content: resourcePreview.localContent,
localChange: Change.None,
remoteChange: resourcePreview.localContent === null ? Change.Deleted : Change.Modified,
};
}
if (isEqual(resource, resourcePreview.remoteResource)) {
return {
content: resourcePreview.remoteContent,
localChange: resourcePreview.remoteContent === null ? Change.Deleted : Change.Modified,
remoteChange: Change.None,
};
}
if (isEqual(resource, resourcePreview.previewResource)) {
if (content === undefined) {
return {
content: resourcePreview.ref,
localChange: Change.Modified,
remoteChange: Change.Modified,
};
} else {
return {
content,
localChange: content === null ? resourcePreview.localContent !== null ? Change.Deleted : Change.None : Change.Modified,
remoteChange: content === null ? resourcePreview.remoteContent !== null ? Change.Deleted : Change.None : Change.Modified,
};
}
}
throw new Error(`Invalid Resource: ${resource.toString()}`);
}
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
if (resourcePreviews[0][1].localChange === Change.Deleted) {
await this.fileService.del(this.localResource);
}
if (resourcePreviews[0][1].localChange === Change.Added || resourcePreviews[0][1].localChange === Change.Modified) {
await this.fileService.writeFile(this.localResource, VSBuffer.fromString(resourcePreviews[0][1].content!));
}
if (resourcePreviews[0][1].remoteChange === Change.Deleted) {
await this.applyRef(null, remoteUserData.ref);
}
if (resourcePreviews[0][1].remoteChange === Change.Added || resourcePreviews[0][1].remoteChange === Change.Modified) {
await this.applyRef(resourcePreviews[0][1].content, remoteUserData.ref);
}
}
async applyRef(ref: string): Promise<void> {
const remoteUserData = await this.updateRemoteUserData('', ref);
async applyRef(content: string | null, ref: string): Promise<void> {
const remoteUserData = await this.updateRemoteUserData(content === null ? '' : content, ref);
await this.updateLastSyncUserData(remoteUserData);
}
@@ -96,7 +163,7 @@ class TestSynchroniser extends AbstractSynchroniser {
}
suite('TestSynchronizer', () => {
suite('TestSynchronizer - Auto Sync', () => {
const disposableStore = new DisposableStore();
const server = new UserDataSyncTestServer();
@@ -167,7 +234,7 @@ suite('TestSynchronizer', () => {
await testObject.sync(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assertConflicts(testObject.conflicts, [resource]);
assertConflicts(testObject.conflicts, [testObject.localResource]);
});
test('sync should not run if syncing already', async () => {
@@ -214,6 +281,155 @@ suite('TestSynchronizer', () => {
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
});
test('accept preview during conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
await testObject.accept(testObject.conflicts[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertConflicts(testObject.conflicts, []);
await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
const fileService = client.instantiationService.get(IFileService);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, (await fileService.readFile(testObject.localResource)).value.toString());
});
test('accept remote during conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const fileService = client.instantiationService.get(IFileService);
const currentRemoteContent = (await testObject.getRemoteUserData(null)).syncData?.content;
const newLocalContent = 'conflict';
await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent));
testObject.syncResult = { hasConflicts: true, hasError: false };
await testObject.sync(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
await testObject.accept(testObject.conflicts[0].remoteResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertConflicts(testObject.conflicts, []);
await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, currentRemoteContent);
assert.equal((await fileService.readFile(testObject.localResource)).value.toString(), currentRemoteContent);
});
test('accept local during conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const fileService = client.instantiationService.get(IFileService);
const newLocalContent = 'conflict';
await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent));
testObject.syncResult = { hasConflicts: true, hasError: false };
await testObject.sync(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
await testObject.accept(testObject.conflicts[0].localResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertConflicts(testObject.conflicts, []);
await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, newLocalContent);
assert.equal((await fileService.readFile(testObject.localResource)).value.toString(), newLocalContent);
});
test('accept new content during conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const fileService = client.instantiationService.get(IFileService);
const newLocalContent = 'conflict';
await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent));
testObject.syncResult = { hasConflicts: true, hasError: false };
await testObject.sync(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
const mergeContent = 'newContent';
await testObject.accept(testObject.conflicts[0].previewResource, mergeContent);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertConflicts(testObject.conflicts, []);
await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, mergeContent);
assert.equal((await fileService.readFile(testObject.localResource)).value.toString(), mergeContent);
});
test('accept delete during conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const fileService = client.instantiationService.get(IFileService);
const newLocalContent = 'conflict';
await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent));
testObject.syncResult = { hasConflicts: true, hasError: false };
await testObject.sync(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
await testObject.accept(testObject.conflicts[0].previewResource, null);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertConflicts(testObject.conflicts, []);
await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, '');
assert.ok(!(await fileService.exists(testObject.localResource)));
});
test('accept deleted local during conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const fileService = client.instantiationService.get(IFileService);
await fileService.del(testObject.localResource);
testObject.syncResult = { hasConflicts: true, hasError: false };
await testObject.sync(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
await testObject.accept(testObject.conflicts[0].localResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertConflicts(testObject.conflicts, []);
await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, '');
assert.ok(!(await fileService.exists(testObject.localResource)));
});
test('accept deleted remote during conflicts', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncBarrier.open();
const fileService = client.instantiationService.get(IFileService);
await fileService.writeFile(testObject.localResource, VSBuffer.fromString('some content'));
testObject.syncResult = { hasConflicts: true, hasError: false };
await testObject.sync(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
await testObject.accept(testObject.conflicts[0].remoteResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertConflicts(testObject.conflicts, []);
await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal((await testObject.getRemoteUserData(null)).syncData, null);
assert.ok(!(await fileService.exists(testObject.localResource)));
});
test('request latest data on precondition failure', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
// Sync once
@@ -224,7 +440,7 @@ suite('TestSynchronizer', () => {
// update remote data before syncing so that 412 is thrown by server
const disposable = testObject.onDoSyncCall.event(async () => {
disposable.dispose();
await testObject.applyRef(ref);
await testObject.applyRef(ref, ref);
server.reset();
testObject.syncBarrier.open();
});
@@ -266,8 +482,26 @@ suite('TestSynchronizer', () => {
assert.equal(testObject.status, SyncStatus.Idle);
});
});
test('preview: status is set to syncing when asked for preview if there are no conflicts', async () => {
suite('TestSynchronizer - Manual Sync', () => {
const disposableStore = new DisposableStore();
const server = new UserDataSyncTestServer();
let client: UserDataSyncClient;
let userDataSyncStoreService: IUserDataSyncStoreService;
setup(async () => {
client = disposableStore.add(new UserDataSyncClient(server));
await client.setUp();
userDataSyncStoreService = client.instantiationService.get(IUserDataSyncStoreService);
disposableStore.add(toDisposable(() => userDataSyncStoreService.clear()));
client.instantiationService.get(IFileService).registerProvider(USER_DATA_SYNC_SCHEME, new InMemoryFileSystemProvider());
});
teardown(() => disposableStore.clear());
test('preview', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
@@ -275,11 +509,11 @@ suite('TestSynchronizer', () => {
const preview = await testObject.preview(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is syncing after merging if there are no conflicts', async () => {
test('preview -> merge', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
@@ -288,26 +522,134 @@ suite('TestSynchronizer', () => {
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to idle after merging and applying if there are no conflicts', async () => {
test('preview -> accept', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
assertConflicts(testObject.conflicts, []);
});
test('preview -> merge -> accept', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
assertConflicts(testObject.conflicts, []);
});
test('preview -> merge -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const manifest = await client.manifest();
let preview = await testObject.preview(manifest);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
const expectedContent = manifest!.latest![testObject.resource];
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
test('preview: discarding the merge', async () => {
test('preview -> accept -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const manifest = await client.manifest();
const expectedContent = manifest!.latest![testObject.resource];
let preview = await testObject.preview(manifest);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
test('preview -> merge -> accept -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
test('preview -> accept', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assertConflicts(testObject.conflicts, []);
});
test('preview -> accept -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const manifest = await client.manifest();
const expectedContent = manifest!.latest![testObject.resource];
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
test('preivew -> merge -> discard', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
@@ -317,39 +659,155 @@ suite('TestSynchronizer', () => {
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is syncing after accepting when there are no conflicts', async () => {
test('preivew -> merge -> discard -> accept', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to idle and sync is applied after accepting when there are no conflicts before merging', async () => {
test('preivew -> accept -> discard', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
assertConflicts(testObject.conflicts, []);
});
test('preivew -> accept -> discard -> accept', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
assertConflicts(testObject.conflicts, []);
});
test('preivew -> accept -> discard -> merge', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
assertConflicts(testObject.conflicts, []);
});
test('preivew -> merge -> accept -> discard', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
assertConflicts(testObject.conflicts, []);
});
test('preivew -> merge -> discard -> accept -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
test('preview: status is set to syncing when asked for preview if there are conflicts', async () => {
test('preivew -> accept -> discard -> accept -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
test('preivew -> accept -> discard -> merge -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const manifest = await client.manifest();
const expectedContent = manifest!.latest![testObject.resource];
let preview = await testObject.preview(manifest);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
test('conflicts: preview', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
@@ -357,11 +815,11 @@ suite('TestSynchronizer', () => {
const preview = await testObject.preview(await client.manifest());
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to hasConflicts after merging', async () => {
test('conflicts: preview -> merge', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
@@ -370,12 +828,12 @@ suite('TestSynchronizer', () => {
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assertPreviews(preview!.resourcePreviews, [resource]);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Conflict);
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].previewResource]);
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].localResource]);
});
test('preview: discarding the conflict', async () => {
test('conflicts: preview -> merge -> discard', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
@@ -385,73 +843,242 @@ suite('TestSynchronizer', () => {
await testObject.discard(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is syncing after accepting when there are conflicts', async () => {
test('conflicts: preview -> accept', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.deepEqual(testObject.conflicts, []);
});
test('preview: status is set to idle and sync is applied after accepting when there are conflicts', async () => {
test('conflicts: preview -> merge -> accept -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
testObject.syncResult = { hasConflicts: true, hasError: false };
const manifest = await client.manifest();
const expectedContent = manifest!.latest![testObject.resource];
let preview = await testObject.preview(manifest);
let preview = await testObject.preview(await client.manifest());
await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
test('preview: status is set to syncing after accepting when there are conflicts before merging', async () => {
test('conflicts: preview -> accept', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to idle and sync is applied after accepting when there are conflicts before merging', async () => {
test('conflicts: preview -> accept -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
testObject.syncResult = { hasConflicts: true, hasError: false };
const manifest = await client.manifest();
const expectedContent = manifest!.latest![testObject.resource];
let preview = await testObject.preview(manifest);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
function assertConflicts(actual: IResourcePreview[], expected: URI[]) {
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
}
test('conflicts: preivew -> merge -> discard', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
function assertPreviews(actual: IResourcePreview[], expected: URI[]) {
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
}
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
assertConflicts(testObject.conflicts, []);
});
test('conflicts: preivew -> merge -> discard -> accept', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
assertConflicts(testObject.conflicts, []);
});
test('conflicts: preivew -> accept -> discard', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
assertConflicts(testObject.conflicts, []);
});
test('conflicts: preivew -> accept -> discard -> accept', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
assertConflicts(testObject.conflicts, []);
});
test('conflicts: preivew -> accept -> discard -> merge', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource);
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Conflict);
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].localResource]);
});
test('conflicts: preivew -> merge -> discard -> merge', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: true, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource);
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Conflict);
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].localResource]);
});
test('conflicts: preivew -> merge -> accept -> discard', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
assertConflicts(testObject.conflicts, []);
});
test('conflicts: preivew -> merge -> discard -> accept -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
test('conflicts: preivew -> accept -> discard -> accept -> apply', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { hasConflicts: false, hasError: false };
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
let preview = await testObject.preview(await client.manifest());
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
});
});
function assertConflicts(actual: IBaseResourcePreview[], expected: URI[]) {
assert.deepEqual(actual.map(({ localResource }) => localResource.toString()), expected.map(uri => uri.toString()));
}
function assertPreviews(actual: IBaseResourcePreview[], expected: URI[]) {
assert.deepEqual(actual.map(({ localResource }) => localResource.toString()), expected.map(uri => uri.toString()));
}

View File

@@ -6,14 +6,14 @@
import { IRequestService } from 'vs/platform/request/common/request';
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync';
import { bufferToStream, VSBuffer } from 'vs/base/common/buffer';
import { generateUuid } from 'vs/base/common/uuid';
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { NullLogService, ILogService } from 'vs/platform/log/common/log';
import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IFileService } from 'vs/platform/files/common/files';
@@ -49,14 +49,15 @@ export class UserDataSyncClient extends Disposable {
}
async setUp(empty: boolean = false): Promise<void> {
const userDataDirectory = URI.file('userdata').with({ scheme: Schemas.inMemory });
const userDataSyncHome = joinPath(userDataDirectory, '.sync');
const userRoamingDataHome = URI.file('userdata').with({ scheme: Schemas.inMemory });
const userDataSyncHome = joinPath(userRoamingDataHome, '.sync');
const environmentService = this.instantiationService.stub(IEnvironmentService, <Partial<IEnvironmentService>>{
userDataSyncHome,
settingsResource: joinPath(userDataDirectory, 'settings.json'),
keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'),
snippetsHome: joinPath(userDataDirectory, 'snippets'),
argvResource: joinPath(userDataDirectory, 'argv.json'),
userRoamingDataHome,
settingsResource: joinPath(userRoamingDataHome, 'settings.json'),
keybindingsResource: joinPath(userRoamingDataHome, 'keybindings.json'),
snippetsHome: joinPath(userRoamingDataHome, 'snippets'),
argvResource: joinPath(userRoamingDataHome, 'argv.json'),
sync: 'on',
});
@@ -86,6 +87,7 @@ export class UserDataSyncClient extends Disposable {
this.instantiationService.stub(IUserDataSyncLogService, logService);
this.instantiationService.stub(ITelemetryService, NullTelemetryService);
this.instantiationService.stub(IUserDataSyncStoreManagementService, this.instantiationService.createInstance(UserDataSyncStoreManagementService));
this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService));
const userDataSyncAccountService: IUserDataSyncAccountService = this.instantiationService.createInstance(UserDataSyncAccountService);

View File

@@ -76,65 +76,6 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
});
test('test first time sync from the client with no changes - pull', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Setup the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
// Sync (pull) from the test client
target.reset();
await testObject.pull();
assert.deepEqual(target.requests, [
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
]);
});
test('test first time sync from the client with changes - pull', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
// Setup the test client with changes
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
const fileService = testClient.instantiationService.get(IFileService);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
// Sync (pull) from the test client
target.reset();
await testObject.pull();
assert.deepEqual(target.requests, [
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
]);
});
test('test first time sync from the client with no changes - merge', async () => {
const target = new UserDataSyncTestServer();

View File

@@ -4,18 +4,64 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IUserDataSyncStoreService, SyncResource, UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncStoreService, SyncResource, UserDataSyncErrorCode, UserDataSyncStoreError, IUserDataSyncStoreManagementService, IUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IProductService } from 'vs/platform/product/common/productService';
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
import { isWeb } from 'vs/base/common/platform';
import { RequestsSession, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
import { RequestsSession, UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IRequestService } from 'vs/platform/request/common/request';
import { newWriteableBufferStream } from 'vs/base/common/buffer';
import { newWriteableBufferStream, VSBuffer } from 'vs/base/common/buffer';
import { timeout } from 'vs/base/common/async';
import { NullLogService } from 'vs/platform/log/common/log';
import { Event } from 'vs/base/common/event';
import product from 'vs/platform/product/common/product';
import { IFileService } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { URI } from 'vs/base/common/uri';
suite('UserDataSyncStoreManagementService', () => {
const disposableStore = new DisposableStore();
teardown(() => disposableStore.clear());
test('test sync store is read from settings', async () => {
const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer()));
await client.setUp();
client.instantiationService.stub(IProductService, {
_serviceBrand: undefined, ...product, ...{
'configurationSync.store': undefined
}
});
const configuredStore: ConfigurationSyncStore = {
url: 'http://configureHost:3000',
authenticationProviders: { 'configuredAuthProvider': { scopes: [] } }
};
await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(JSON.stringify({
'configurationSync.store': configuredStore
})));
await client.instantiationService.get(IConfigurationService).reloadConfiguration();
const expected: IUserDataSyncStore = {
url: URI.parse('http://configureHost:3000'),
defaultUrl: URI.parse('http://configureHost:3000'),
stableUrl: undefined,
insidersUrl: undefined,
authenticationProviders: [{ id: 'configuredAuthProvider', scopes: [] }]
};
const testObject: IUserDataSyncStoreManagementService = client.instantiationService.createInstance(UserDataSyncStoreManagementService);
assert.equal(testObject.userDataSyncStore?.url.toString(), expected.url.toString());
assert.equal(testObject.userDataSyncStore?.defaultUrl.toString(), expected.defaultUrl.toString());
assert.deepEqual(testObject.userDataSyncStore?.authenticationProviders, expected.authenticationProviders);
});
});
suite('UserDataSyncStoreService', () => {
@@ -388,6 +434,20 @@ suite('UserDataSyncStoreService', () => {
assert.ok(!target.donotMakeRequestsUntil);
});
test('test read resource request handles 304', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.sync();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
const expected = await testObject.read(SyncResource.Settings, null);
const actual = await testObject.read(SyncResource.Settings, expected);
assert.equal(actual, expected);
});
});
suite('UserDataSyncRequestsSession', () => {