mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode 1b314ab317fbff7d799b21754326b7d849889ceb
This commit is contained in:
@@ -9,7 +9,8 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import {
|
||||
SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
|
||||
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview, IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME
|
||||
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview,
|
||||
IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
|
||||
@@ -52,9 +53,20 @@ function isSyncData(thing: any): thing is ISyncData {
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface IMergableResourcePreview extends IBaseResourcePreview {
|
||||
readonly remoteContent: string | null;
|
||||
readonly localContent: string | null;
|
||||
readonly previewContent: string | null;
|
||||
readonly hasConflicts: boolean;
|
||||
merged: boolean;
|
||||
}
|
||||
|
||||
export type IResourcePreview = Omit<IMergableResourcePreview, 'merged'>;
|
||||
|
||||
export interface ISyncResourcePreview extends IBaseSyncResourcePreview {
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly resourcePreviews: IMergableResourcePreview[];
|
||||
}
|
||||
|
||||
export abstract class AbstractSynchroniser extends Disposable {
|
||||
@@ -70,10 +82,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: Conflict[] = [];
|
||||
get conflicts(): Conflict[] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<Conflict[]> = this._register(new Emitter<Conflict[]>());
|
||||
readonly onDidChangeConflicts: Event<Conflict[]> = this._onDidChangeConflicts.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 readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
|
||||
private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
@@ -119,7 +131,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: In conflicts state and local change detected. Syncing again...`);
|
||||
const preview = await this.syncPreviewPromise!;
|
||||
this.syncPreviewPromise = null;
|
||||
const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData);
|
||||
const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, true);
|
||||
this.setStatus(status);
|
||||
}
|
||||
|
||||
@@ -127,7 +139,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData, CancellationToken.None)).hasRemoteChanged : true;
|
||||
const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, true, CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true;
|
||||
if (hasRemoteChanged) {
|
||||
this._onDidChangeLocal.fire();
|
||||
}
|
||||
@@ -137,8 +149,6 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
protected setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
const oldStatus = this._status;
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
if (status === SyncStatus.HasConflicts) {
|
||||
// Log to telemetry when there is a sync conflict
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsDetected', { source: this.resource });
|
||||
@@ -147,16 +157,8 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
// Log to telemetry when conflicts are resolved
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.resource });
|
||||
}
|
||||
if (this.status !== SyncStatus.HasConflicts) {
|
||||
this.setConflicts([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected setConflicts(conflicts: Conflict[]) {
|
||||
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.local, b.local) && isEqual(a.remote, b.remote))) {
|
||||
this._conflicts = conflicts;
|
||||
this._onDidChangeConflicts.fire(this._conflicts);
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +178,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
await this.applyPreview(preview, false);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
@@ -199,7 +201,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
await this.applyPreview(preview, true);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, true);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`);
|
||||
|
||||
} finally {
|
||||
@@ -208,38 +210,49 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
|
||||
async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<void> {
|
||||
await this._sync(manifest, true, headers);
|
||||
}
|
||||
|
||||
async preview(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
|
||||
return this._sync(manifest, false, headers);
|
||||
}
|
||||
|
||||
private async _sync(manifest: IUserDataManifest | null, apply: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null> {
|
||||
try {
|
||||
this.syncHeaders = { ...headers };
|
||||
|
||||
if (!this.isEnabled()) {
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
await this.stop();
|
||||
}
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`);
|
||||
return;
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
if (this.status === SyncStatus.Syncing) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`);
|
||||
return;
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
|
||||
|
||||
let status: SyncStatus = SyncStatus.Idle;
|
||||
try {
|
||||
status = await this.performSync(remoteUserData, lastSyncUserData);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
|
||||
status = await this.performSync(remoteUserData, lastSyncUserData, apply);
|
||||
if (status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`);
|
||||
} else if (status === SyncStatus.Idle) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`);
|
||||
}
|
||||
return this.syncPreviewPromise || null;
|
||||
} finally {
|
||||
this.setStatus(status);
|
||||
}
|
||||
@@ -267,7 +280,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
|
||||
const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData);
|
||||
await this.applyPreview(preview, false);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
@@ -276,7 +289,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
|
||||
protected async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
|
||||
if (lastSyncUserData) {
|
||||
|
||||
const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined;
|
||||
@@ -294,24 +307,15 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return this.getRemoteUserData(lastSyncUserData);
|
||||
}
|
||||
|
||||
async generateSyncPreview(): Promise<ISyncResourcePreview | null> {
|
||||
if (this.isEnabled()) {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
return this.generatePreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
|
||||
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
|
||||
// current version is not compatible with cloud version
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource });
|
||||
throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource);
|
||||
throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.IncompatibleLocalContent, this.resource);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.doSync(remoteUserData, lastSyncUserData);
|
||||
return await this.doSync(remoteUserData, lastSyncUserData, apply);
|
||||
} catch (e) {
|
||||
if (e instanceof UserDataSyncError) {
|
||||
switch (e.code) {
|
||||
@@ -319,7 +323,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
case UserDataSyncErrorCode.LocalPreconditionFailed:
|
||||
// Rejected as there is a new local version. Syncing again...
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`);
|
||||
return this.performSync(remoteUserData, lastSyncUserData);
|
||||
return this.performSync(remoteUserData, lastSyncUserData, apply);
|
||||
|
||||
case UserDataSyncErrorCode.PreconditionFailed:
|
||||
// Rejected as there is a new remote version. Syncing again...
|
||||
@@ -332,32 +336,28 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
// and one of them successfully updated remote and last sync state.
|
||||
lastSyncUserData = await this.getLastSyncUserData();
|
||||
|
||||
return this.performSync(remoteUserData, lastSyncUserData);
|
||||
return this.performSync(remoteUserData, lastSyncUserData, apply);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
|
||||
try {
|
||||
// generate or use existing preview
|
||||
if (!this.syncPreviewPromise) {
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
|
||||
}
|
||||
const preview = await this.syncPreviewPromise;
|
||||
|
||||
if (preview.hasConflicts) {
|
||||
return SyncStatus.HasConflicts;
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, apply, token));
|
||||
}
|
||||
|
||||
// apply preview
|
||||
await this.applyPreview(preview, false);
|
||||
if (apply) {
|
||||
const preview = await this.syncPreviewPromise;
|
||||
const newConflicts = preview.resourcePreviews.filter(({ hasConflicts }) => hasConflicts);
|
||||
return await this.updateConflictsAndApply(newConflicts, false);
|
||||
} else {
|
||||
return SyncStatus.Syncing;
|
||||
}
|
||||
|
||||
// reset preview
|
||||
this.syncPreviewPromise = null;
|
||||
|
||||
return SyncStatus.Idle;
|
||||
} catch (error) {
|
||||
|
||||
// reset preview on error
|
||||
@@ -367,32 +367,128 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected async getSyncPreviewInProgress(): Promise<ISyncResourcePreview | null> {
|
||||
return this.syncPreviewPromise ? this.syncPreviewPromise : null;
|
||||
async acceptPreviewContent(resource: URI, content: string, force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.syncHeaders = { ...headers };
|
||||
const preview = await this.syncPreviewPromise;
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.updateSyncResourcePreviewContent(preview, resource, content, token));
|
||||
return this.merge(resource, force, headers);
|
||||
} finally {
|
||||
this.syncHeaders = {};
|
||||
}
|
||||
}
|
||||
|
||||
async acceptConflict(conflictUri: URI, conflictContent: string): Promise<void> {
|
||||
let preview = await this.getSyncPreviewInProgress();
|
||||
|
||||
if (!preview || !preview.hasConflicts) {
|
||||
return;
|
||||
async merge(resource: URI, force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.syncHeaders = { ...headers };
|
||||
const preview = await this.syncPreviewPromise;
|
||||
const resourcePreview = preview.resourcePreviews.find(({ localResource, remoteResource, previewResource }) =>
|
||||
isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
|
||||
if (!resourcePreview) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.updatePreviewWithConflict(preview!, conflictUri, conflictContent, token));
|
||||
preview = await this.syncPreviewPromise;
|
||||
/* mark merged */
|
||||
resourcePreview.merged = true;
|
||||
|
||||
if (!preview.hasConflicts) {
|
||||
/* Add or remove the preview from conflicts */
|
||||
const newConflicts = [...this._conflicts];
|
||||
const index = newConflicts.findIndex(({ localResource, remoteResource, previewResource }) =>
|
||||
isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
|
||||
if (resourcePreview.hasConflicts) {
|
||||
if (newConflicts.indexOf(resourcePreview) === -1) {
|
||||
newConflicts.push(resourcePreview);
|
||||
}
|
||||
} else {
|
||||
if (index !== -1) {
|
||||
newConflicts.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// apply preview
|
||||
await this.applyPreview(preview, false);
|
||||
const status = await this.updateConflictsAndApply(newConflicts, force);
|
||||
this.setStatus(status);
|
||||
return this.syncPreviewPromise;
|
||||
|
||||
// reset preview
|
||||
this.syncPreviewPromise = null;
|
||||
} finally {
|
||||
this.syncHeaders = {};
|
||||
}
|
||||
}
|
||||
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
private async updateConflictsAndApply(conflicts: IMergableResourcePreview[], force: boolean): Promise<SyncStatus> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
const preview = await this.syncPreviewPromise;
|
||||
|
||||
// update conflicts
|
||||
this.updateConflicts(conflicts);
|
||||
if (this._conflicts.length) {
|
||||
return SyncStatus.HasConflicts;
|
||||
}
|
||||
|
||||
// check if all are merged
|
||||
if (preview.resourcePreviews.some(r => !r.merged)) {
|
||||
return SyncStatus.Syncing;
|
||||
}
|
||||
|
||||
// apply preview
|
||||
await this.applyPreview(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews, force);
|
||||
|
||||
// reset preview
|
||||
this.syncPreviewPromise = null;
|
||||
|
||||
// reset preview folder
|
||||
await this.clearPreviewFolder();
|
||||
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
private async updateSyncResourcePreviewContent(preview: ISyncResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<ISyncResourcePreview> {
|
||||
const index = preview.resourcePreviews.findIndex(({ localResource, remoteResource, previewResource, localChange, remoteChange }) =>
|
||||
(localChange !== Change.None || remoteChange !== Change.None)
|
||||
&& (isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource)));
|
||||
if (index !== -1) {
|
||||
const resourcePreviews = [...preview.resourcePreviews];
|
||||
const resourcePreview = await this.updateResourcePreviewContent(resourcePreviews[index], resource, previewContent, token);
|
||||
resourcePreviews[index] = { ...resourcePreview, merged: resourcePreviews[index].merged };
|
||||
preview = {
|
||||
...preview,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
protected async updateResourcePreviewContent(resourcePreview: IResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IResourcePreview> {
|
||||
return {
|
||||
...resourcePreview,
|
||||
previewContent,
|
||||
hasConflicts: false,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
private async clearPreviewFolder(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.syncPreviewFolder, { recursive: true });
|
||||
} catch (error) { /* Ignore */ }
|
||||
}
|
||||
|
||||
private updateConflicts(conflicts: IMergableResourcePreview[]): void {
|
||||
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) {
|
||||
this._conflicts = conflicts;
|
||||
this._onDidChangeConflicts.fire(conflicts);
|
||||
}
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
@@ -400,11 +496,6 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return !!lastSyncData;
|
||||
}
|
||||
|
||||
protected async isLastSyncFromCurrentMachine(remoteUserData: IRemoteUserData): Promise<boolean> {
|
||||
const machineId = await this.currentMachineIdPromise;
|
||||
return !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
|
||||
}
|
||||
|
||||
async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
|
||||
const handles = await this.userDataSyncStoreService.getAllRefs(this.resource);
|
||||
return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) }));
|
||||
@@ -447,12 +538,47 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(uri: URI): Promise<string | null> {
|
||||
const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null;
|
||||
if (syncPreview) {
|
||||
for (const resourcePreview of syncPreview.resourcePreviews) {
|
||||
if (resourcePreview.previewResource && isEqual(resourcePreview.previewResource, uri)) {
|
||||
return resourcePreview.previewContent || '';
|
||||
}
|
||||
if (resourcePreview.remoteResource && isEqual(resourcePreview.remoteResource, uri)) {
|
||||
return resourcePreview.remoteContent || '';
|
||||
}
|
||||
if (resourcePreview.localResource && isEqual(resourcePreview.localResource, uri)) {
|
||||
return resourcePreview.localContent || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.lastSyncResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, merge: boolean, token: CancellationToken): Promise<ISyncResourcePreview> {
|
||||
const machineId = await this.currentMachineIdPromise;
|
||||
const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
|
||||
|
||||
// For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine
|
||||
const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData;
|
||||
const resourcePreviews = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
|
||||
|
||||
// Mark merge
|
||||
const mergableResourcePreviews = resourcePreviews.map(r => ({
|
||||
...r,
|
||||
merged: merge || (r.localChange === Change.None && r.remoteChange === Change.None) /* Mark previews with no changes as merged */
|
||||
}));
|
||||
|
||||
return { remoteUserData, lastSyncUserData, resourcePreviews: mergableResourcePreviews, isLastSyncFromCurrentMachine };
|
||||
}
|
||||
|
||||
async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncResource);
|
||||
@@ -500,7 +626,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.Incompatible, this.resource);
|
||||
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource);
|
||||
}
|
||||
|
||||
private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> {
|
||||
@@ -526,26 +652,33 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
|
||||
if (this.status === SyncStatus.Idle) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Stopping synchronizing ${this.resource.toLowerCase()}.`);
|
||||
if (this.syncPreviewPromise) {
|
||||
this.syncPreviewPromise.cancel();
|
||||
this.syncPreviewPromise = null;
|
||||
}
|
||||
|
||||
this.updateConflicts([]);
|
||||
await this.clearPreviewFolder();
|
||||
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
|
||||
}
|
||||
|
||||
protected abstract readonly version: number;
|
||||
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncResourcePreview>;
|
||||
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract applyPreview(preview: ISyncResourcePreview, forcePush: boolean): Promise<void>;
|
||||
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IResourcePreview[]>;
|
||||
protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IResourcePreview[], forcePush: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IFileSyncPreview extends ISyncResourcePreview {
|
||||
export interface IFileResourcePreview extends IResourcePreview {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly content: string | null;
|
||||
}
|
||||
|
||||
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
@@ -568,28 +701,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await super.stop();
|
||||
try {
|
||||
await this.fileService.del(this.localPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) {
|
||||
const syncPreview = await this.getSyncPreviewInProgress();
|
||||
if (syncPreview) {
|
||||
if (isEqual(this.remotePreviewResource, conflictResource)) {
|
||||
return syncPreview.remoteUserData && syncPreview.remoteUserData.syncData ? syncPreview.remoteUserData.syncData.content : null;
|
||||
}
|
||||
if (isEqual(this.localPreviewResource, conflictResource)) {
|
||||
return (syncPreview as IFileSyncPreview).fileContent ? (syncPreview as IFileSyncPreview).fileContent!.value.toString() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async getLocalFileContent(): Promise<IFileContent | null> {
|
||||
try {
|
||||
return await this.fileService.readFile(this.file);
|
||||
@@ -598,14 +709,14 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
|
||||
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null, force: boolean): Promise<void> {
|
||||
try {
|
||||
if (oldContent) {
|
||||
// file exists already
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent);
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), force ? undefined : oldContent);
|
||||
} else {
|
||||
// file does not exist
|
||||
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
|
||||
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: force });
|
||||
}
|
||||
} catch (e) {
|
||||
if ((e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) ||
|
||||
@@ -624,8 +735,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
this.triggerLocalChange();
|
||||
}
|
||||
|
||||
protected abstract readonly localPreviewResource: URI;
|
||||
protected abstract readonly remotePreviewResource: URI;
|
||||
}
|
||||
|
||||
export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { values, keys } from 'vs/base/common/map';
|
||||
import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -78,7 +76,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
|
||||
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
|
||||
|
||||
// Remotely removed extension.
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
const e = localExtensionsMap.get(key);
|
||||
if (e) {
|
||||
removed.push(e.identifier);
|
||||
@@ -86,7 +84,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
|
||||
}
|
||||
|
||||
// Remotely added extension
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Is different from local to remote
|
||||
@@ -103,13 +101,13 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
|
||||
}
|
||||
|
||||
// Remotely updated extensions
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
// Update in local always
|
||||
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
|
||||
}
|
||||
|
||||
// Locally added extensions
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
// Not there in remote
|
||||
if (!baseToRemote.added.has(key)) {
|
||||
newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!);
|
||||
@@ -117,7 +115,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
|
||||
}
|
||||
|
||||
// Locally updated extensions
|
||||
for (const key of values(baseToLocal.updated)) {
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
// If removed in remote
|
||||
if (baseToRemote.removed.has(key)) {
|
||||
continue;
|
||||
@@ -135,7 +133,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
|
||||
}
|
||||
|
||||
// Locally removed extensions
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// If not skipped and not updated in remote
|
||||
if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) {
|
||||
// Remove only if it is an installed extension
|
||||
@@ -156,8 +154,8 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
|
||||
}
|
||||
|
||||
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>, { checkInstalledProperty }: { checkInstalledProperty: boolean } = { checkInstalledProperty: false }): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = from ? keys(from).filter(key => !ignoredExtensions.has(key)) : [];
|
||||
const toKeys = keys(to).filter(key => !ignoredExtensions.has(key));
|
||||
const fromKeys = from ? [...from.keys()].filter(key => !ignoredExtensions.has(key)) : [];
|
||||
const toKeys = [...to.keys()].filter(key => !ignoredExtensions.has(key));
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
@@ -190,7 +188,7 @@ function massageOutgoingExtension(extension: ISyncExtension, key: string): ISync
|
||||
const massagedExtension: ISyncExtension = {
|
||||
identifier: {
|
||||
id: extension.identifier.id,
|
||||
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
|
||||
uuid: key.startsWith('uuid:') ? key.substring('uuid:'.length) : undefined
|
||||
},
|
||||
};
|
||||
if (extension.disabled) {
|
||||
@@ -211,7 +209,7 @@ export function getIgnoredExtensions(installed: ILocalExtension[], configuration
|
||||
const added: string[] = [], removed: string[] = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const key of value) {
|
||||
if (startsWith(key, '-')) {
|
||||
if (key.startsWith('-')) {
|
||||
removed.push(key.substring(1));
|
||||
} else {
|
||||
added.push(key);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService,
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
@@ -14,8 +14,8 @@ import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/comm
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { merge, getIgnoredExtensions, IMergeResult } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources';
|
||||
@@ -25,9 +25,8 @@ import { compare } from 'vs/base/common/strings';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface IExtensionsSyncPreview extends ISyncResourcePreview {
|
||||
export interface IExtensionResourcePreview extends IResourcePreview {
|
||||
readonly localExtensions: ISyncExtension[];
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
readonly added: ISyncExtension[];
|
||||
readonly removed: IExtensionIdentifier[];
|
||||
readonly updated: ISyncExtension[];
|
||||
@@ -41,7 +40,7 @@ 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: `/current.json` });
|
||||
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
|
||||
*/
|
||||
@@ -74,84 +73,49 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
() => undefined, 500)(() => this.triggerLocalChange()));
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionsSyncPreview> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData);
|
||||
const mergeResult = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
added, removed, updated, remote, localExtensions, skippedExtensions: [],
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
added: [], removed: [], updated: [], remote: null, localExtensions, skippedExtensions: [],
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: false,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: [],
|
||||
};
|
||||
}
|
||||
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<IExtensionsSyncPreview> {
|
||||
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;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
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<IExtensionsSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
|
||||
const syncExtensions = await this.parseAndMigrateExtensions(syncData);
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const mergeResult = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
return [{
|
||||
localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
|
||||
localContent: this.format(localExtensions),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote: syncExtensions,
|
||||
localExtensions,
|
||||
skippedExtensions: [],
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
|
||||
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
let lastSyncExtensions: ISyncExtension[] | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSyncExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData!);
|
||||
}
|
||||
} else {
|
||||
lastSyncExtensions = await this.parseAndMigrateExtensions(lastSyncUserData.syncData!);
|
||||
}
|
||||
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null;
|
||||
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
@@ -165,36 +129,34 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
|
||||
const mergeResult = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
|
||||
localContent: this.format(localExtensions),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
skippedExtensions,
|
||||
remoteUserData,
|
||||
localExtensions,
|
||||
lastSyncUserData,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
isLastSyncFromCurrentMachine,
|
||||
skippedExtensions,
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IExtensionsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<IExtensionsSyncPreview> {
|
||||
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
|
||||
}
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IExtensionResourcePreview[], force: boolean): Promise<void> {
|
||||
let { added, removed, updated, remote, skippedExtensions, localExtensions, localChange, remoteChange } = resourcePreviews[0];
|
||||
|
||||
protected async applyPreview({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreview, forcePush: boolean): Promise<void> {
|
||||
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (localChange !== Change.None) {
|
||||
await this.backupLocal(JSON.stringify(localExtensions));
|
||||
skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
|
||||
}
|
||||
@@ -203,7 +165,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote extensions...`);
|
||||
const content = JSON.stringify(remote);
|
||||
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions`);
|
||||
}
|
||||
|
||||
@@ -215,16 +177,88 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
}
|
||||
}
|
||||
|
||||
private getResourcePreviews({ added, removed, updated, remote }: IMergeResult): IResourcePreview[] {
|
||||
const hasLocalChanged = added.length > 0 || removed.length > 0 || updated.length > 0;
|
||||
const hasRemoteChanged = remote !== null;
|
||||
return [{
|
||||
hasLocalChanged,
|
||||
protected async updateResourcePreviewContent(resourcePreview: IExtensionResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IExtensionResourcePreview> {
|
||||
if (isEqual(resource, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
|
||||
const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
|
||||
return this.getPushPreview(remoteExtensions);
|
||||
}
|
||||
return {
|
||||
...resourcePreview,
|
||||
previewContent,
|
||||
hasConflicts: false,
|
||||
hasRemoteChanged,
|
||||
localResouce: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
|
||||
remoteResource: this.remotePreviewResource
|
||||
}];
|
||||
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 = ExtensionsSynchroniser.EXTENSIONS_DATA_URI;
|
||||
const localContent = this.format(localExtensions);
|
||||
const remoteResource = this.remotePreviewResource;
|
||||
const previewResource = this.localPreviewResource;
|
||||
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,
|
||||
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,
|
||||
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: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
|
||||
localContent: this.format(localExtensions),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localExtensions,
|
||||
skippedExtensions: [],
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
};
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
@@ -238,6 +272,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
return this.format(localExtensions);
|
||||
}
|
||||
|
||||
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { IStorageValue } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
@@ -35,7 +34,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
const skipped: string[] = [];
|
||||
|
||||
// Added in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
@@ -59,7 +58,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
}
|
||||
|
||||
// Updated in Remote
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
@@ -79,7 +78,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
}
|
||||
|
||||
// Removed in remote
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.trace(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
|
||||
@@ -89,14 +88,14 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
}
|
||||
|
||||
// Added in local
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
if (!baseToRemote.added.has(key)) {
|
||||
remote[key] = localStorage[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Updated in local
|
||||
for (const key of values(baseToLocal.updated)) {
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
if (baseToRemote.updated.has(key) || baseToRemote.removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -110,7 +109,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
}
|
||||
|
||||
// Removed in local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// do not remove from remote if it is updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
continue;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService,
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
@@ -14,9 +14,9 @@ import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { merge, IMergeResult } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
@@ -30,12 +30,11 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
const argvStoragePrefx = 'globalState.argv.';
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
interface IGlobalStateSyncPreview extends ISyncResourcePreview {
|
||||
export interface IGlobalStateResourcePreview extends IResourcePreview {
|
||||
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
readonly remote: IStringDictionary<IStorageValue> | null;
|
||||
readonly skippedStorageKeys: string[];
|
||||
readonly localUserData: IGlobalState;
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
}
|
||||
|
||||
interface ILastSyncUserData extends IRemoteUserData {
|
||||
@@ -44,7 +43,7 @@ interface ILastSyncUserData extends IRemoteUserData {
|
||||
|
||||
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/current.json` });
|
||||
private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/globalState.json` });
|
||||
protected readonly version: number = 1;
|
||||
private readonly localPreviewResource: URI = joinPath(this.syncPreviewFolder, 'globalState.json');
|
||||
private readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME });
|
||||
@@ -75,76 +74,44 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
|
||||
const localGlobalState = await this.getLocalGlobalState();
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
|
||||
const mergeResult = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, remote, skipped } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
local, remote, localUserData: localGlobalState, skippedStorageKeys: skipped,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
local: { added: {}, removed: [], updated: {} }, remote: null, localUserData: localGlobalState, skippedStorageKeys: [],
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: false,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: []
|
||||
};
|
||||
}
|
||||
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<IGlobalStateSyncPreview> {
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
return {
|
||||
local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData,
|
||||
skippedStorageKeys: [],
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
hasConflicts: false,
|
||||
resourcePreviews: this.getResourcePreviews({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, skipped: [] })
|
||||
};
|
||||
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<IGlobalStateSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalStateResourcePreview[]> {
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const mergeResult = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, skipped } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData,
|
||||
return [{
|
||||
localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
|
||||
localContent: this.format(localUserData),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
local,
|
||||
remote: syncGlobalState.storage,
|
||||
localUserData,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: false,
|
||||
resourcePreviews: [],
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
let lastSyncGlobalState: IGlobalState | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSyncGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
}
|
||||
} else {
|
||||
lastSyncGlobalState = lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
}
|
||||
const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
const localGloablState = await this.getLocalGlobalState();
|
||||
|
||||
@@ -156,30 +123,32 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
|
||||
const mergeResult = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, remote, skipped } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
|
||||
return {
|
||||
local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData,
|
||||
return [{
|
||||
localResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
|
||||
localContent: this.format(localGloablState),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
local,
|
||||
remote,
|
||||
localUserData: localGloablState,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
isLastSyncFromCurrentMachine,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IGlobalStateSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
|
||||
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
|
||||
}
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IGlobalStateResourcePreview[], force: boolean): Promise<void> {
|
||||
let { local, remote, localUserData, localChange, remoteChange, skippedStorageKeys } = resourcePreviews[0];
|
||||
|
||||
protected async applyPreview({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalStateSyncPreview, forcePush: boolean): Promise<void> {
|
||||
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (localChange !== Change.None) {
|
||||
// update local
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`);
|
||||
await this.backupLocal(JSON.stringify(localUserData));
|
||||
@@ -187,11 +156,11 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`);
|
||||
}
|
||||
|
||||
if (hasRemoteChanged) {
|
||||
if (remoteChange !== Change.None) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`);
|
||||
const content = JSON.stringify(<IGlobalState>{ storage: remote });
|
||||
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
|
||||
}
|
||||
|
||||
@@ -203,16 +172,79 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
}
|
||||
}
|
||||
|
||||
private getResourcePreviews({ local, remote }: IMergeResult): IResourcePreview[] {
|
||||
const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0;
|
||||
const hasRemoteChanged = remote !== null;
|
||||
return [{
|
||||
hasLocalChanged,
|
||||
protected async updateResourcePreviewContent(resourcePreview: IGlobalStateResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IGlobalStateResourcePreview> {
|
||||
if (GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI, resource) {
|
||||
return this.getPushPreview(resourcePreview.remoteContent);
|
||||
}
|
||||
if (this.remotePreviewResource, 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 = GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI;
|
||||
const localContent = this.format(localGlobalState);
|
||||
const remoteResource = this.remotePreviewResource;
|
||||
const previewResource = this.localPreviewResource;
|
||||
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,
|
||||
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,
|
||||
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: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
|
||||
localContent: this.format(localUserData),
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent: null,
|
||||
local: { added: {}, removed: [], updated: {} },
|
||||
remote: localUserData.storage,
|
||||
localUserData,
|
||||
skippedStorageKeys: [],
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: false,
|
||||
hasRemoteChanged,
|
||||
localResouce: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
|
||||
remoteResource: this.remotePreviewResource
|
||||
}];
|
||||
};
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
@@ -225,6 +257,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
return this.format(localGlobalState);
|
||||
}
|
||||
|
||||
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { values, keys } from 'vs/base/common/map';
|
||||
import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { firstIndex as findFirstIndex, equals } from 'vs/base/common/arrays';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
@@ -57,14 +56,14 @@ export async function merge(localContent: string, remoteContent: string, baseCon
|
||||
const remoteByCommand = byCommand(remote);
|
||||
const baseByCommand = base ? byCommand(base) : null;
|
||||
const localToRemoteByCommand = compareByCommand(localByCommand, remoteByCommand, normalizedKeys);
|
||||
const baseToLocalByCommand = baseByCommand ? compareByCommand(baseByCommand, localByCommand, normalizedKeys) : { added: keys(localByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToRemoteByCommand = baseByCommand ? compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: keys(remoteByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToLocalByCommand = baseByCommand ? compareByCommand(baseByCommand, localByCommand, normalizedKeys) : { added: [...localByCommand.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToRemoteByCommand = baseByCommand ? compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: [...remoteByCommand.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
|
||||
const commandsMergeResult = computeMergeResult(localToRemoteByCommand, baseToLocalByCommand, baseToRemoteByCommand);
|
||||
let mergeContent = localContent;
|
||||
|
||||
// Removed commands in Remote
|
||||
for (const command of values(commandsMergeResult.removed)) {
|
||||
for (const command of commandsMergeResult.removed.values()) {
|
||||
if (commandsMergeResult.conflicts.has(command)) {
|
||||
continue;
|
||||
}
|
||||
@@ -72,7 +71,7 @@ export async function merge(localContent: string, remoteContent: string, baseCon
|
||||
}
|
||||
|
||||
// Added commands in remote
|
||||
for (const command of values(commandsMergeResult.added)) {
|
||||
for (const command of commandsMergeResult.added.values()) {
|
||||
if (commandsMergeResult.conflicts.has(command)) {
|
||||
continue;
|
||||
}
|
||||
@@ -86,7 +85,7 @@ export async function merge(localContent: string, remoteContent: string, baseCon
|
||||
}
|
||||
|
||||
// Updated commands in Remote
|
||||
for (const command of values(commandsMergeResult.updated)) {
|
||||
for (const command of commandsMergeResult.updated.values()) {
|
||||
if (commandsMergeResult.conflicts.has(command)) {
|
||||
continue;
|
||||
}
|
||||
@@ -109,7 +108,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
|
||||
const conflicts: Set<string> = new Set<string>();
|
||||
|
||||
// Removed keys in Local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// Got updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
@@ -117,7 +116,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
|
||||
}
|
||||
|
||||
// Removed keys in Remote
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -131,7 +130,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
|
||||
}
|
||||
|
||||
// Added keys in Local
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -145,7 +144,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
|
||||
}
|
||||
|
||||
// Added keys in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -161,7 +160,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
|
||||
}
|
||||
|
||||
// Updated keys in Local
|
||||
for (const key of values(baseToLocal.updated)) {
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -175,7 +174,7 @@ function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompare
|
||||
}
|
||||
|
||||
// Updated keys in Remote
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -204,13 +203,13 @@ function computeMergeResultByKeybinding(local: IUserFriendlyKeybinding[], remote
|
||||
return { hasLocalForwarded: false, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty };
|
||||
}
|
||||
|
||||
const baseToLocalByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, localByKeybinding) : { added: keys(localByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToLocalByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, localByKeybinding) : { added: [...localByKeybinding.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
if (baseToLocalByKeybinding.added.size === 0 && baseToLocalByKeybinding.removed.size === 0 && baseToLocalByKeybinding.updated.size === 0) {
|
||||
// Remote has moved forward and local has not.
|
||||
return { hasLocalForwarded: false, hasRemoteForwarded: true, added: empty, removed: empty, updated: empty, conflicts: empty };
|
||||
}
|
||||
|
||||
const baseToRemoteByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, remoteByKeybinding) : { added: keys(remoteByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToRemoteByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, remoteByKeybinding) : { added: [...remoteByKeybinding.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
if (baseToRemoteByKeybinding.added.size === 0 && baseToRemoteByKeybinding.removed.size === 0 && baseToRemoteByKeybinding.updated.size === 0) {
|
||||
return { hasLocalForwarded: true, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty };
|
||||
}
|
||||
@@ -250,8 +249,8 @@ function byCommand(keybindings: IUserFriendlyKeybinding[]): Map<string, IUserFri
|
||||
|
||||
|
||||
function compareByKeybinding(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>): ICompareResult {
|
||||
const fromKeys = keys(from);
|
||||
const toKeys = keys(to);
|
||||
const fromKeys = [...from.keys()];
|
||||
const toKeys = [...to.keys()];
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
@@ -271,8 +270,8 @@ function compareByKeybinding(from: Map<string, IUserFriendlyKeybinding[]>, to: M
|
||||
}
|
||||
|
||||
function compareByCommand(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>, normalizedKeys: IStringDictionary<string>): ICompareResult {
|
||||
const fromKeys = keys(from);
|
||||
const toKeys = keys(to);
|
||||
const fromKeys = [...from.keys()];
|
||||
const toKeys = [...to.keys()];
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo
|
||||
import {
|
||||
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource,
|
||||
IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle,
|
||||
IRemoteUserData, ISyncData, IResourcePreview
|
||||
IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
@@ -19,7 +19,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { OS, OperatingSystem } from 'vs/base/common/platform';
|
||||
import { isUndefined } from 'vs/base/common/types';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
|
||||
@@ -53,105 +53,69 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const content = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = false;
|
||||
const hasConflicts = false;
|
||||
const previewContent = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: previewContent,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const content: string | null = fileContent ? fileContent.value.toString() : null;
|
||||
const hasLocalChanged = false;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
const previewContent: string | null = fileContent ? fileContent.value.toString() : null;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const content = this.getKeybindingsContentFromSyncContent(syncData.content);
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
const previewContent = this.getKeybindingsContentFromSyncContent(syncData.content);
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
let lastSyncContent: string | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSyncContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
}
|
||||
} else {
|
||||
lastSyncContent = lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
|
||||
}
|
||||
const lastSyncContent: string | null = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
// Get file content last to get the latest
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
@@ -170,7 +134,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
|
||||
// Sync only if there are changes
|
||||
if (result.hasChanges) {
|
||||
content = result.mergeContent;
|
||||
previewContent = result.mergeContent;
|
||||
hasConflicts = result.hasConflicts;
|
||||
hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
|
||||
hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
|
||||
@@ -181,55 +145,49 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`);
|
||||
content = fileContent.value.toString();
|
||||
previewContent = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
}
|
||||
|
||||
if (content && !token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
|
||||
if (previewContent && !token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
|
||||
}
|
||||
|
||||
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
previewResource: this.localPreviewResource
|
||||
remoteContent,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
hasConflicts,
|
||||
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
|
||||
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
|
||||
}];
|
||||
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews };
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) {
|
||||
preview = { ...preview, content: conflictContent, hasConflicts: false };
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise<void> {
|
||||
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview;
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
|
||||
let { fileContent, previewContent: content, localChange, remoteChange } = resourcePreviews[0];
|
||||
|
||||
if (content !== null) {
|
||||
if (this.hasErrors(content)) {
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
|
||||
}
|
||||
await this.updateLocalFileContent(content, fileContent);
|
||||
await this.updateLocalFileContent(content, fileContent, force);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
|
||||
}
|
||||
|
||||
if (hasRemoteChanged) {
|
||||
if (remoteChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`);
|
||||
const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null);
|
||||
remoteUserData = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
|
||||
}
|
||||
|
||||
@@ -272,7 +230,11 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, uri)) {
|
||||
if (isEqual(this.file, uri)) {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
return fileContent ? fileContent.value.toString() : '';
|
||||
}
|
||||
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
let content = await super.resolveContent(uri);
|
||||
@@ -292,11 +254,6 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
const content = await super.resolvePreviewContent(conflictResource);
|
||||
return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null;
|
||||
}
|
||||
|
||||
getKeybindingsContentFromSyncContent(syncContent: string): string | null {
|
||||
try {
|
||||
const parsed = <ISyncContent>JSON.parse(syncContent);
|
||||
|
||||
@@ -6,14 +6,12 @@
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { parse, JSONVisitor, visit } from 'vs/base/common/json';
|
||||
import { setProperty, withFormatting, applyEdits } from 'vs/base/common/jsonEdit';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions, Edit, getEOL } from 'vs/base/common/jsonFormatter';
|
||||
import * as contentUtil from 'vs/platform/userDataSync/common/content';
|
||||
import { IConflictSetting, getDisallowedIgnoredSettings } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { firstIndex, distinct } from 'vs/base/common/arrays';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
|
||||
export interface IMergeResult {
|
||||
localContent: string | null;
|
||||
@@ -35,7 +33,7 @@ export function getIgnoredSettings(defaultIgnoredSettings: string[], configurati
|
||||
const added: string[] = [], removed: string[] = [...getDisallowedIgnoredSettings()];
|
||||
if (Array.isArray(value)) {
|
||||
for (const key of value) {
|
||||
if (startsWith(key, '-')) {
|
||||
if (key.startsWith('-')) {
|
||||
removed.push(key.substring(1));
|
||||
} else {
|
||||
added.push(key);
|
||||
@@ -130,7 +128,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
|
||||
};
|
||||
|
||||
// Removed settings in Local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// Conflict - Got updated in remote.
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
@@ -142,7 +140,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
|
||||
}
|
||||
|
||||
// Removed settings in Remote
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -157,7 +155,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
|
||||
}
|
||||
|
||||
// Updated settings in Local
|
||||
for (const key of values(baseToLocal.updated)) {
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -173,7 +171,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
|
||||
}
|
||||
|
||||
// Updated settings in Remote
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -189,7 +187,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
|
||||
}
|
||||
|
||||
// Added settings in Local
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -205,7 +203,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
|
||||
}
|
||||
|
||||
// Added settings in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -223,7 +221,7 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
|
||||
const hasConflicts = conflicts.size > 0 || !areSame(localContent, remoteContent, ignoredSettings);
|
||||
const hasLocalChanged = hasConflicts || !areSame(localContent, originalLocalContent, []);
|
||||
const hasRemoteChanged = hasConflicts || !areSame(remoteContent, originalRemoteContent, []);
|
||||
return { localContent: hasLocalChanged ? localContent : null, remoteContent: hasRemoteChanged ? remoteContent : null, conflictsSettings: values(conflicts), hasConflicts };
|
||||
return { localContent: hasLocalChanged ? localContent : null, remoteContent: hasRemoteChanged ? remoteContent : null, conflictsSettings: [...conflicts.values()], hasConflicts };
|
||||
}
|
||||
|
||||
export function areSame(localContent: string, remoteContent: string, ignoredSettings: string[]): boolean {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo
|
||||
import {
|
||||
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY,
|
||||
SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IUserDataSynchroniser,
|
||||
IRemoteUserData, ISyncData, IResourcePreview
|
||||
IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { localize } from 'vs/nls';
|
||||
@@ -17,7 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
@@ -58,133 +58,95 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
if (remoteSettingsSyncContent !== null) {
|
||||
// Update ignored settings from local file content
|
||||
content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
previewContent = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = false;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
if (fileContent !== null) {
|
||||
// Remove ignored settings
|
||||
content = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
|
||||
previewContent = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
const hasLocalChanged = false;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
|
||||
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
if (settingsSyncContent) {
|
||||
content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
previewContent = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
resourcePreviews,
|
||||
isLastSyncFromCurrentMachine: false
|
||||
};
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
let lastSettingsSyncContent: ISettingsSyncContent | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
}
|
||||
} else {
|
||||
lastSettingsSyncContent = this.getSettingsSyncContent(lastSyncUserData);
|
||||
}
|
||||
const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null;
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
|
||||
let content: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
@@ -193,9 +155,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
|
||||
this.validateContent(localContent);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, [], formattingOptions);
|
||||
content = result.localContent || result.remoteContent;
|
||||
previewContent = result.localContent || result.remoteContent;
|
||||
hasLocalChanged = result.localContent !== null;
|
||||
hasRemoteChanged = result.remoteContent !== null;
|
||||
hasConflicts = result.hasConflicts;
|
||||
@@ -204,64 +165,61 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`);
|
||||
content = fileContent.value.toString();
|
||||
previewContent = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
}
|
||||
|
||||
if (content && !token.isCancellationRequested) {
|
||||
if (previewContent && !token.isCancellationRequested) {
|
||||
// Remove the ignored settings from the preview.
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions);
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
|
||||
const content = updateIgnoredSettings(previewContent, '{}', ignoredSettings, formattingOptions);
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
|
||||
}
|
||||
|
||||
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
return [{
|
||||
localResource: this.file,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
previewResource: this.localPreviewResource
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.localPreviewResource,
|
||||
previewContent,
|
||||
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
|
||||
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
|
||||
hasConflicts,
|
||||
}];
|
||||
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews };
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Add ignored settings from local file content
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const content = updateIgnoredSettings(conflictContent, preview.fileContent ? preview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
preview = { ...preview, content, hasConflicts: false };
|
||||
}
|
||||
return preview;
|
||||
protected async updateResourcePreviewContent(resourcePreview: IFileResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IFileResourcePreview> {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Add ignored settings from local file content
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
previewContent = updateIgnoredSettings(previewContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
return super.updateResourcePreviewContent(resourcePreview, resource, previewContent, token) as Promise<IFileResourcePreview>;
|
||||
}
|
||||
|
||||
protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise<void> {
|
||||
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview;
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
|
||||
let { fileContent, previewContent: content, localChange, remoteChange } = resourcePreviews[0];
|
||||
|
||||
if (content !== null) {
|
||||
|
||||
this.validateContent(content);
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
|
||||
}
|
||||
await this.updateLocalFileContent(content, fileContent);
|
||||
await this.updateLocalFileContent(content, fileContent, force);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
|
||||
}
|
||||
if (hasRemoteChanged) {
|
||||
if (remoteChange !== Change.None) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Update ignored settings from remote
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
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)), forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
|
||||
}
|
||||
|
||||
@@ -302,7 +260,11 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, uri)) {
|
||||
if (isEqual(this.file, uri)) {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
return fileContent ? fileContent.value.toString() : '';
|
||||
}
|
||||
if (isEqual(this.remotePreviewResource, uri) || isEqual(this.localPreviewResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
let content = await super.resolveContent(uri);
|
||||
@@ -325,15 +287,11 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
let content = await super.resolvePreviewContent(conflictResource);
|
||||
if (content !== null) {
|
||||
const settingsSyncContent = this.parseSettingsSyncContent(content);
|
||||
content = settingsSyncContent ? settingsSyncContent.settings : null;
|
||||
}
|
||||
protected async resolvePreviewContent(resource: URI): Promise<string | null> {
|
||||
let content = await super.resolvePreviewContent(resource);
|
||||
if (content !== null) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// remove ignored settings from the remote content for preview
|
||||
// remove ignored settings from the preview content
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
content = updateIgnoredSettings(content, '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
@@ -3,30 +3,32 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
|
||||
export interface IMergeResult {
|
||||
added: IStringDictionary<string>;
|
||||
updated: IStringDictionary<string>;
|
||||
removed: string[];
|
||||
local: {
|
||||
added: IStringDictionary<string>;
|
||||
updated: IStringDictionary<string>;
|
||||
removed: string[];
|
||||
};
|
||||
remote: {
|
||||
added: IStringDictionary<string>;
|
||||
updated: IStringDictionary<string>;
|
||||
removed: string[];
|
||||
};
|
||||
conflicts: string[];
|
||||
remote: IStringDictionary<string> | null;
|
||||
}
|
||||
|
||||
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null, resolvedConflicts: IStringDictionary<string | null> = {}): IMergeResult {
|
||||
const added: IStringDictionary<string> = {};
|
||||
const updated: IStringDictionary<string> = {};
|
||||
const removed: Set<string> = new Set<string>();
|
||||
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null): IMergeResult {
|
||||
const localAdded: IStringDictionary<string> = {};
|
||||
const localUpdated: IStringDictionary<string> = {};
|
||||
const localRemoved: Set<string> = new Set<string>();
|
||||
|
||||
if (!remote) {
|
||||
return {
|
||||
added,
|
||||
removed: values(removed),
|
||||
updated,
|
||||
conflicts: [],
|
||||
remote: Object.keys(local).length > 0 ? local : null
|
||||
local: { added: localAdded, updated: localUpdated, removed: [...localRemoved.values()] },
|
||||
remote: { added: local, updated: {}, removed: [] },
|
||||
conflicts: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,145 +36,118 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar
|
||||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return {
|
||||
added,
|
||||
removed: values(removed),
|
||||
updated,
|
||||
conflicts: [],
|
||||
remote: null
|
||||
local: { added: localAdded, updated: localUpdated, removed: [...localRemoved.values()] },
|
||||
remote: { added: {}, updated: {}, removed: [] },
|
||||
conflicts: []
|
||||
};
|
||||
}
|
||||
|
||||
const baseToLocal = compare(base, local);
|
||||
const baseToRemote = compare(base, remote);
|
||||
const remoteContent: IStringDictionary<string> = deepClone(remote);
|
||||
|
||||
const remoteAdded: IStringDictionary<string> = {};
|
||||
const remoteUpdated: IStringDictionary<string> = {};
|
||||
const remoteRemoved: Set<string> = new Set<string>();
|
||||
|
||||
const conflicts: Set<string> = new Set<string>();
|
||||
const handledConflicts: Set<string> = new Set<string>();
|
||||
const handleConflict = (key: string): void => {
|
||||
if (handledConflicts.has(key)) {
|
||||
return;
|
||||
}
|
||||
handledConflicts.add(key);
|
||||
const conflictContent = resolvedConflicts[key];
|
||||
|
||||
// add to conflicts
|
||||
if (conflictContent === undefined) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
|
||||
// remove the snippet
|
||||
else if (conflictContent === null) {
|
||||
delete remote[key];
|
||||
if (local[key]) {
|
||||
removed.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// add/update the snippet
|
||||
else {
|
||||
if (local[key]) {
|
||||
if (local[key] !== conflictContent) {
|
||||
updated[key] = conflictContent;
|
||||
}
|
||||
} else {
|
||||
added[key] = conflictContent;
|
||||
}
|
||||
remoteContent[key] = conflictContent;
|
||||
}
|
||||
};
|
||||
|
||||
// Removed snippets in Local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// Conflict - Got updated in remote.
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Add to local
|
||||
added[key] = remote[key];
|
||||
localAdded[key] = remote[key];
|
||||
}
|
||||
// Remove it in remote
|
||||
else {
|
||||
delete remoteContent[key];
|
||||
remoteRemoved.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed snippets in Remote
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Conflict - Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
// Also remove in Local
|
||||
else {
|
||||
removed.add(key);
|
||||
localRemoved.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Updated snippets in Local
|
||||
for (const key of values(baseToLocal.updated)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent[key] = local[key];
|
||||
remoteUpdated[key] = local[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Updated snippets in Remote
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else if (local[key] !== undefined) {
|
||||
updated[key] = remote[key];
|
||||
localUpdated[key] = remote[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Added snippets in Local
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in remote
|
||||
if (baseToRemote.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent[key] = local[key];
|
||||
remoteAdded[key] = local[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Added snippets in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
added[key] = remote[key];
|
||||
localAdded[key] = remote[key];
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed: values(removed), updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent };
|
||||
return {
|
||||
local: { added: localAdded, removed: [...localRemoved.values()], updated: localUpdated },
|
||||
remote: { added: remoteAdded, removed: [...remoteRemoved.values()], updated: remoteUpdated },
|
||||
conflicts: [...conflicts.values()],
|
||||
};
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<string> | null, to: IStringDictionary<string> | null): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
@@ -196,7 +171,7 @@ function compare(from: IStringDictionary<string> | null, to: IStringDictionary<s
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
function areSame(a: IStringDictionary<string>, b: IStringDictionary<string>): boolean {
|
||||
export function areSame(a: IStringDictionary<string>, b: IStringDictionary<string>): boolean {
|
||||
const { added, removed, updated } = compare(a, b);
|
||||
return added.size === 0 && removed.size === 0 && updated.size === 0;
|
||||
}
|
||||
|
||||
@@ -5,30 +5,22 @@
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService,
|
||||
Conflict, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, IResourcePreview
|
||||
USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, UserDataSyncError, UserDataSyncErrorCode, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename, dirname } from 'vs/base/common/resources';
|
||||
import { joinPath, extname, relativePath, isEqualOrParent, basename, dirname } from 'vs/base/common/resources';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { merge, IMergeResult } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { merge, IMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
interface ISinppetsSyncPreview extends ISyncResourcePreview {
|
||||
readonly local: IStringDictionary<IFileContent>;
|
||||
readonly added: IStringDictionary<string>;
|
||||
readonly updated: IStringDictionary<string>;
|
||||
readonly removed: string[];
|
||||
readonly conflicts: Conflict[];
|
||||
readonly resolvedConflicts: IStringDictionary<string | null>;
|
||||
readonly remote: IStringDictionary<string> | null;
|
||||
}
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
@@ -60,83 +52,40 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
this.triggerLocalChange();
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISinppetsSyncPreview> {
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const resourcePreviews: IFileResourcePreview[] = [];
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, localSnippets);
|
||||
const { added, updated, remote, removed } = mergeResult;
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
added, removed, updated, remote, local,
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: this.getResourcePreviews(mergeResult)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
added: {}, removed: [], updated: {}, remote: null, local: {},
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: false,
|
||||
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: []
|
||||
};
|
||||
resourcePreviews.push(...this.getResourcePreviews(mergeResult, local, remoteSnippets));
|
||||
}
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISinppetsSyncPreview> {
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const mergeResult = merge(localSnippets, null, null);
|
||||
const { added, updated, remote, removed } = mergeResult;
|
||||
return {
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
hasConflicts: false,
|
||||
resourcePreviews: this.getResourcePreviews(mergeResult)
|
||||
};
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult, local, {});
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreview> {
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const snippets = this.parseSnippets(syncData);
|
||||
const mergeResult = merge(localSnippets, snippets, localSnippets);
|
||||
const { added, updated, removed } = mergeResult;
|
||||
return {
|
||||
added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, hasConflicts: false,
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: this.getResourcePreviews(mergeResult)
|
||||
};
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult, local, snippets);
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
return this.doGeneratePreview(local, remoteUserData, lastSyncUserData, {}, token);
|
||||
}
|
||||
|
||||
private async doGeneratePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null> = {}, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreview> {
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
|
||||
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
|
||||
|
||||
let lastSyncSnippets: IStringDictionary<string> | null = null;
|
||||
if (lastSyncUserData === null) {
|
||||
if (isLastSyncFromCurrentMachine) {
|
||||
lastSyncSnippets = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
|
||||
}
|
||||
} else {
|
||||
lastSyncSnippets = lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
|
||||
}
|
||||
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
|
||||
|
||||
if (remoteSnippets) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);
|
||||
@@ -144,79 +93,47 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`);
|
||||
}
|
||||
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts);
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult);
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets);
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult, local, remoteSnippets || {});
|
||||
|
||||
const conflicts: Conflict[] = [];
|
||||
for (const key of mergeResult.conflicts) {
|
||||
const localPreview = joinPath(this.syncPreviewFolder, key);
|
||||
conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) });
|
||||
const content = local[key];
|
||||
if (!token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(localPreview, content ? content.value : VSBuffer.fromString(''));
|
||||
}
|
||||
}
|
||||
|
||||
for (const conflict of this.conflicts) {
|
||||
// clear obsolete conflicts
|
||||
if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) {
|
||||
try {
|
||||
await this.fileService.del(conflict.local);
|
||||
} catch (error) {
|
||||
// Ignore & log
|
||||
this.logService.error(error);
|
||||
for (const resourcePreview of resourcePreviews) {
|
||||
if (resourcePreview.hasConflicts) {
|
||||
if (!token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(resourcePreview.previewResource!, VSBuffer.fromString(resourcePreview.previewContent || ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setConflicts(conflicts);
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async updateResourcePreviewContent(resourcePreview: IFileResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IFileResourcePreview> {
|
||||
return {
|
||||
remoteUserData, local,
|
||||
lastSyncUserData,
|
||||
added: mergeResult.added,
|
||||
removed: mergeResult.removed,
|
||||
updated: mergeResult.updated,
|
||||
conflicts,
|
||||
hasConflicts: conflicts.length > 0,
|
||||
remote: mergeResult.remote,
|
||||
resolvedConflicts,
|
||||
hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0,
|
||||
hasRemoteChanged: mergeResult.remote !== null,
|
||||
isLastSyncFromCurrentMachine,
|
||||
resourcePreviews
|
||||
...resourcePreview,
|
||||
previewContent: previewContent || null,
|
||||
hasConflicts: false,
|
||||
localChange: previewContent ? Change.Modified : Change.Deleted,
|
||||
remoteChange: previewContent ? Change.Modified : Change.Deleted,
|
||||
};
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: ISinppetsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISinppetsSyncPreview> {
|
||||
const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0];
|
||||
if (conflict) {
|
||||
const key = relativePath(this.syncPreviewFolder, conflict.local)!;
|
||||
preview.resolvedConflicts[key] = content || null;
|
||||
preview = await this.doGeneratePreview(preview.local, preview.remoteUserData, preview.lastSyncUserData, preview.resolvedConflicts, token);
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
|
||||
if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
|
||||
throw new UserDataSyncError(localize('unresolved conflicts', "Error while syncing {0}. Please resolve conflicts first.", this.syncResourceLogLabel), UserDataSyncErrorCode.UnresolvedConflicts, this.resource);
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
protected async applyPreview(preview: ISinppetsSyncPreview, forcePush: boolean): Promise<void> {
|
||||
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = preview;
|
||||
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
if (resourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
if (resourcePreviews.some(({ localChange }) => localChange !== Change.None)) {
|
||||
// back up all snippets
|
||||
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
|
||||
await this.updateLocalSnippets(added, removed, updated, local);
|
||||
await this.updateLocalBackup(resourcePreviews);
|
||||
await this.updateLocalSnippets(resourcePreviews, force);
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
|
||||
const content = JSON.stringify(remote);
|
||||
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
|
||||
if (resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) {
|
||||
remoteUserData = await this.updateRemoteSnippets(resourcePreviews, remoteUserData, force);
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
@@ -226,52 +143,149 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`);
|
||||
}
|
||||
|
||||
for (const { previewResource } of resourcePreviews) {
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(previewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private getResourcePreviews(mergeResult: IMergeResult): IResourcePreview[] {
|
||||
const resourcePreviews: IResourcePreview[] = [];
|
||||
for (const key of Object.keys(mergeResult.added)) {
|
||||
resourcePreviews.push({
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false
|
||||
});
|
||||
}
|
||||
for (const key of Object.keys(mergeResult.updated)) {
|
||||
resourcePreviews.push({
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
localResouce: joinPath(this.snippetsFolder, key),
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: true
|
||||
});
|
||||
}
|
||||
for (const key of mergeResult.removed) {
|
||||
resourcePreviews.push({
|
||||
localResouce: joinPath(this.snippetsFolder, key),
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false
|
||||
});
|
||||
}
|
||||
for (const key of mergeResult.conflicts) {
|
||||
resourcePreviews.push({
|
||||
localResouce: joinPath(this.snippetsFolder, key),
|
||||
private getResourcePreviews(mergeResult: IMergeResult, localFileContent: IStringDictionary<IFileContent>, remoteSnippets: IStringDictionary<string>): IFileResourcePreview[] {
|
||||
const resourcePreviews: Map<string, IFileResourcePreview> = new Map<string, IFileResourcePreview>();
|
||||
|
||||
/* Snippets added remotely -> add locally */
|
||||
for (const key of Object.keys(mergeResult.local.added)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: null,
|
||||
localContent: null,
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
hasConflicts: true,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: true
|
||||
previewContent: mergeResult.local.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Added,
|
||||
remoteChange: Change.None
|
||||
});
|
||||
}
|
||||
|
||||
return resourcePreviews;
|
||||
}
|
||||
/* Snippets updated remotely -> update locally */
|
||||
for (const key of Object.keys(mergeResult.local.updated)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.local.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.None
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.clearConflicts();
|
||||
return super.stop();
|
||||
/* Snippets removed remotely -> remove locally */
|
||||
for (const key of mergeResult.local.removed) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.Deleted,
|
||||
remoteChange: Change.None
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets added locally -> add remotely */
|
||||
for (const key of Object.keys(mergeResult.remote.added)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.remote.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Added
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets updated locally -> update remotely */
|
||||
for (const key of Object.keys(mergeResult.remote.updated)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.remote.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets removed locally -> remove remotely */
|
||||
for (const key of mergeResult.remote.removed) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: null,
|
||||
localContent: null,
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Deleted
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets with conflicts */
|
||||
for (const key of mergeResult.conflicts) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key] || null,
|
||||
localContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key] || null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: true,
|
||||
localChange: localFileContent[key] ? Change.Modified : Change.Added,
|
||||
remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added
|
||||
});
|
||||
}
|
||||
|
||||
/* Unmodified Snippets */
|
||||
for (const key of Object.keys(localFileContent)) {
|
||||
if (!resourcePreviews.has(key)) {
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.snippetsFolder, key),
|
||||
fileContent: localFileContent[key] || null,
|
||||
localContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
remoteContent: remoteSnippets[key] || null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...resourcePreviews.values()];
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
@@ -294,13 +308,25 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqualOrParent(uri.with({ scheme: this.syncFolder.scheme }), this.syncPreviewFolder)) {
|
||||
if (isEqualOrParent(uri, this.snippetsFolder)) {
|
||||
try {
|
||||
const content = await this.fileService.readFile(uri);
|
||||
return content ? content.value.toString() : null;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (isEqualOrParent(uri.with({ scheme: this.syncPreviewFolder.scheme }), this.syncPreviewFolder)
|
||||
|| isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME }))) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
content = await super.resolveContent(dirname(uri));
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
@@ -309,20 +335,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
return snippets[basename(uri)] || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
const syncPreview = await this.getSyncPreviewInProgress();
|
||||
if (syncPreview) {
|
||||
const key = relativePath(this.syncPreviewFolder, conflictResource.with({ scheme: this.syncPreviewFolder.scheme }))!;
|
||||
if (conflictResource.scheme === this.syncPreviewFolder.scheme) {
|
||||
return (syncPreview as ISinppetsSyncPreview).local[key] ? (syncPreview as ISinppetsSyncPreview).local[key].value.toString() : null;
|
||||
} else if (syncPreview.remoteUserData && syncPreview.remoteUserData.syncData) {
|
||||
const snippets = this.parseSnippets(syncPreview.remoteUserData.syncData);
|
||||
return snippets[key] || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -338,34 +351,78 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
return false;
|
||||
}
|
||||
|
||||
private async clearConflicts(): Promise<void> {
|
||||
if (this.conflicts.length) {
|
||||
await Promise.all(this.conflicts.map(({ local }) => this.fileService.del(local)));
|
||||
this.setConflicts([]);
|
||||
private async updateLocalBackup(resourcePreviews: IFileResourcePreview[]): Promise<void> {
|
||||
const local: IStringDictionary<IFileContent> = {};
|
||||
for (const resourcePreview of resourcePreviews) {
|
||||
if (resourcePreview.fileContent) {
|
||||
local[basename(resourcePreview.localResource!)] = resourcePreview.fileContent;
|
||||
}
|
||||
}
|
||||
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
|
||||
}
|
||||
|
||||
private async updateLocalSnippets(resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
|
||||
if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
|
||||
// Do not update if there are conflicts
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { fileContent, previewContent: content, localResource, remoteResource, localChange } of resourcePreviews) {
|
||||
if (localChange !== Change.None) {
|
||||
const key = remoteResource ? basename(remoteResource) : basename(localResource!);
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
|
||||
// Removed
|
||||
if (localChange === Change.Deleted) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource));
|
||||
await this.fileService.del(resource);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource));
|
||||
}
|
||||
|
||||
// Added
|
||||
else if (localChange === Change.Added) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource));
|
||||
await this.fileService.createFile(resource, VSBuffer.fromString(content!), { overwrite: 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!);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateLocalSnippets(added: IStringDictionary<string>, removed: string[], updated: IStringDictionary<string>, local: IStringDictionary<IFileContent>): Promise<void> {
|
||||
for (const key of removed) {
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource));
|
||||
await this.fileService.del(resource);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource));
|
||||
private async updateRemoteSnippets(resourcePreviews: IFileResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise<IRemoteUserData> {
|
||||
if (resourcePreviews.some(({ hasConflicts }) => hasConflicts)) {
|
||||
// Do not update if there are conflicts
|
||||
return remoteUserData;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(added)) {
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource));
|
||||
await this.fileService.createFile(resource, VSBuffer.fromString(added[key]), { overwrite: false });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource));
|
||||
const currentSnippets: IStringDictionary<string> = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : {};
|
||||
const newSnippets: IStringDictionary<string> = deepClone(currentSnippets);
|
||||
|
||||
for (const { previewContent: content, localResource, remoteResource, remoteChange } of resourcePreviews) {
|
||||
if (remoteChange !== Change.None) {
|
||||
const key = localResource ? basename(localResource) : basename(remoteResource!);
|
||||
if (remoteChange === Change.Deleted) {
|
||||
delete newSnippets[key];
|
||||
} else {
|
||||
newSnippets[key] = content!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(updated)) {
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource));
|
||||
await this.fileService.writeFile(resource, VSBuffer.fromString(updated[key]), local[key]);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource));
|
||||
if (!areSame(currentSnippets, newSnippets)) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
|
||||
remoteUserData = await this.updateRemoteUserData(JSON.stringify(newSnippets), forcePush ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
|
||||
}
|
||||
return remoteUserData;
|
||||
}
|
||||
|
||||
private parseSnippets(syncData: ISyncData): IStringDictionary<string> {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
@@ -43,7 +42,7 @@ export class StorageKeysSyncRegistryService extends Disposable implements IStora
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly _storageKeys = new Map<string, IStorageKey>();
|
||||
get storageKeys(): ReadonlyArray<IStorageKey> { return values(this._storageKeys); }
|
||||
get storageKeys(): ReadonlyArray<IStorageKey> { return [...this._storageKeys.values()]; }
|
||||
|
||||
private readonly _onDidChangeStorageKeys: Emitter<ReadonlyArray<IStorageKey>> = this._register(new Emitter<ReadonlyArray<IStorageKey>>());
|
||||
readonly onDidChangeStorageKeys = this._onDidChangeStorageKeys.event;
|
||||
|
||||
@@ -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 } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask } 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';
|
||||
@@ -81,12 +81,6 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
private readonly _onError: Emitter<UserDataSyncError> = this._register(new Emitter<UserDataSyncError>());
|
||||
readonly onError: Event<UserDataSyncError> = this._onError.event;
|
||||
|
||||
private readonly _onTurnOnSync: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onTurnOnSync: Event<void> = this._onTurnOnSync.event;
|
||||
|
||||
private readonly _onDidTurnOnSync: Emitter<UserDataSyncError | undefined> = this._register(new Emitter<UserDataSyncError | undefined>());
|
||||
readonly onDidTurnOnSync: Event<UserDataSyncError | undefined> = this._onDidTurnOnSync.event;
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@@ -145,24 +139,9 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
return { enabled: true };
|
||||
}
|
||||
|
||||
async turnOn(pullFirst: boolean): Promise<void> {
|
||||
this._onTurnOnSync.fire();
|
||||
|
||||
try {
|
||||
this.stopDisableMachineEventually();
|
||||
|
||||
if (pullFirst) {
|
||||
await this.userDataSyncService.pull();
|
||||
} else {
|
||||
await this.userDataSyncService.sync();
|
||||
}
|
||||
|
||||
this.setEnablement(true);
|
||||
this._onDidTurnOnSync.fire(undefined);
|
||||
} catch (error) {
|
||||
this._onDidTurnOnSync.fire(error);
|
||||
throw error;
|
||||
}
|
||||
async turnOn(): Promise<void> {
|
||||
this.stopDisableMachineEventually();
|
||||
this.setEnablement(true);
|
||||
}
|
||||
|
||||
async turnOff(everywhere: boolean, softTurnOffOnError?: boolean, donotRemoveMachine?: boolean): Promise<void> {
|
||||
@@ -219,8 +198,14 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
this.telemetryService.publicLog2<{ code: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code });
|
||||
}
|
||||
|
||||
// Turned off from another device or session got expired
|
||||
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff || userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
|
||||
// Session got expired
|
||||
if (userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */);
|
||||
this.logService.info('Auto Sync: Turned off sync because current session is expired');
|
||||
}
|
||||
|
||||
// Turned off from another device
|
||||
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');
|
||||
}
|
||||
@@ -233,6 +218,26 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
|
||||
}
|
||||
|
||||
// Upgrade Required or Gone
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.UpgradeRequired || userDataSyncError.code === UserDataSyncErrorCode.Gone) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */,
|
||||
true /* do not disable machine because disabling a machine makes request to server and can fail with upgrade required or gone */);
|
||||
this.disableMachineEventually();
|
||||
this.logService.info('Auto Sync: Turned off sync because current client is not compatible with server. Requires client upgrade.');
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
else {
|
||||
this.logService.error(userDataSyncError);
|
||||
this.successiveFailures++;
|
||||
@@ -310,6 +315,7 @@ class AutoSync extends Disposable {
|
||||
private readonly _onDidFinishSync = this._register(new Emitter<Error | undefined>());
|
||||
readonly onDidFinishSync = this._onDidFinishSync.event;
|
||||
|
||||
private syncTask: ISyncTask | undefined;
|
||||
private syncPromise: CancelablePromise<void> | undefined;
|
||||
|
||||
constructor(
|
||||
@@ -331,7 +337,9 @@ class AutoSync extends Disposable {
|
||||
this.logService.info('Auto sync: Canelled sync that is in progress');
|
||||
this.syncPromise = undefined;
|
||||
}
|
||||
this.userDataSyncService.stop();
|
||||
if (this.syncTask) {
|
||||
this.syncTask.stop();
|
||||
}
|
||||
this.logService.info('Auto Sync: Stopped');
|
||||
}));
|
||||
this.logService.info('Auto Sync: Started');
|
||||
@@ -368,8 +376,11 @@ class AutoSync extends Disposable {
|
||||
this._onDidStartSync.fire();
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
const syncTask = await this.userDataSyncService.createSyncTask();
|
||||
let manifest = syncTask.manifest;
|
||||
this.syncTask = await this.userDataSyncService.createSyncTask();
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
let manifest = this.syncTask.manifest;
|
||||
|
||||
// Server has no data but this machine was synced before
|
||||
if (manifest === null && await this.userDataSyncService.hasPreviouslySynced()) {
|
||||
@@ -396,7 +407,7 @@ class AutoSync extends Disposable {
|
||||
throw new UserDataAutoSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
|
||||
}
|
||||
|
||||
await syncTask.run(token);
|
||||
await this.syncTask.run();
|
||||
|
||||
// After syncing, get the manifest if it was not available before
|
||||
if (manifest === null) {
|
||||
|
||||
@@ -23,7 +23,6 @@ import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/com
|
||||
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';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
|
||||
|
||||
@@ -190,6 +189,13 @@ export interface IUserDataSyncBackupStoreService {
|
||||
|
||||
//#endregion
|
||||
|
||||
// #region User Data Sync Headers
|
||||
|
||||
export const HEADER_OPERATION_ID = 'x-operation-id';
|
||||
export const HEADER_EXECUTION_ID = 'X-Execution-Id';
|
||||
|
||||
//#endregion
|
||||
|
||||
// #region User Data Sync Error
|
||||
|
||||
export enum UserDataSyncErrorCode {
|
||||
@@ -211,34 +217,30 @@ export enum UserDataSyncErrorCode {
|
||||
LocalPreconditionFailed = 'LocalPreconditionFailed',
|
||||
LocalInvalidContent = 'LocalInvalidContent',
|
||||
LocalError = 'LocalError',
|
||||
Incompatible = 'Incompatible',
|
||||
IncompatibleLocalContent = 'IncompatibleLocalContent',
|
||||
IncompatibleRemoteContent = 'IncompatibleRemoteContent',
|
||||
UnresolvedConflicts = 'UnresolvedConflicts',
|
||||
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export class UserDataSyncError extends Error {
|
||||
|
||||
constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly resource?: SyncResource) {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly code: UserDataSyncErrorCode,
|
||||
readonly resource?: SyncResource,
|
||||
readonly operationId?: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = `${this.code} (UserDataSyncError) ${this.resource || ''}`;
|
||||
}
|
||||
|
||||
static toUserDataSyncError(error: Error): UserDataSyncError {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
return error;
|
||||
}
|
||||
const match = /^(.+) \(UserDataSyncError\) (.+)?$/.exec(error.name);
|
||||
if (match && match[1]) {
|
||||
return new UserDataSyncError(error.message, <UserDataSyncErrorCode>match[1], <SyncResource>match[2]);
|
||||
}
|
||||
return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown);
|
||||
this.name = `${this.code} (UserDataSyncError) syncResource:${this.resource || 'unknown'} operationId:${this.operationId || 'unknown'}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreError extends UserDataSyncError {
|
||||
constructor(message: string, code: UserDataSyncErrorCode) {
|
||||
super(message, code);
|
||||
constructor(message: string, code: UserDataSyncErrorCode, readonly operationId: string | undefined) {
|
||||
super(message, code, undefined, operationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +250,23 @@ export class UserDataAutoSyncError extends UserDataSyncError {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UserDataSyncError {
|
||||
|
||||
export function toUserDataSyncError(error: Error): UserDataSyncError {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
return error;
|
||||
}
|
||||
const match = /^(.+) \(UserDataSyncError\) syncResource:(.+) operationId:(.+)$/.exec(error.name);
|
||||
if (match && match[1]) {
|
||||
const syncResource = match[2] === 'unknown' ? undefined : match[2] as SyncResource;
|
||||
const operationId = match[3] === 'unknown' ? undefined : match[3];
|
||||
return new UserDataSyncError(error.message, <UserDataSyncErrorCode>match[1], syncResource, operationId);
|
||||
}
|
||||
return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
// #region User Data Synchroniser
|
||||
@@ -280,8 +299,6 @@ export interface ISyncResourceHandle {
|
||||
uri: URI;
|
||||
}
|
||||
|
||||
export type Conflict = { remote: URI, local: URI };
|
||||
|
||||
export interface IRemoteUserData {
|
||||
ref: string;
|
||||
syncData: ISyncData | null;
|
||||
@@ -293,20 +310,24 @@ export interface ISyncData {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const enum Change {
|
||||
None,
|
||||
Added,
|
||||
Modified,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
export interface IResourcePreview {
|
||||
readonly remoteResource?: URI;
|
||||
readonly localResouce?: URI;
|
||||
readonly previewResource?: URI;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
readonly remoteResource: URI;
|
||||
readonly localResource: URI;
|
||||
readonly previewResource: URI;
|
||||
readonly localChange: Change;
|
||||
readonly remoteChange: Change;
|
||||
readonly merged: boolean;
|
||||
}
|
||||
|
||||
export interface ISyncResourcePreview {
|
||||
readonly isLastSyncFromCurrentMachine: boolean;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
readonly resourcePreviews: IResourcePreview[];
|
||||
}
|
||||
|
||||
@@ -315,23 +336,26 @@ export interface IUserDataSynchroniser {
|
||||
readonly resource: SyncResource;
|
||||
readonly status: SyncStatus;
|
||||
readonly onDidChangeStatus: Event<SyncStatus>;
|
||||
readonly conflicts: Conflict[];
|
||||
readonly onDidChangeConflicts: Event<Conflict[]>;
|
||||
|
||||
readonly conflicts: IResourcePreview[];
|
||||
readonly onDidChangeConflicts: Event<IResourcePreview[]>;
|
||||
|
||||
readonly onDidChangeLocal: Event<void>;
|
||||
|
||||
pull(): Promise<void>;
|
||||
push(): Promise<void>;
|
||||
sync(manifest: IUserDataManifest | null, headers?: IHeaders): Promise<void>;
|
||||
sync(manifest: IUserDataManifest | null, headers: IHeaders): Promise<void>;
|
||||
preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise<ISyncResourcePreview | null>;
|
||||
replace(uri: URI): Promise<boolean>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
generateSyncPreview(): Promise<ISyncResourcePreview | null>
|
||||
hasPreviouslySynced(): Promise<boolean>
|
||||
hasPreviouslySynced(): Promise<boolean>;
|
||||
hasLocalData(): Promise<boolean>;
|
||||
resetLocal(): Promise<void>;
|
||||
|
||||
resolveContent(resource: URI): Promise<string | null>;
|
||||
acceptConflict(conflictResource: URI, content: string): Promise<void>;
|
||||
acceptPreviewContent(resource: URI, content: string, force: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null>;
|
||||
merge(resource: URI, force: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null>;
|
||||
|
||||
getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
|
||||
getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
|
||||
@@ -352,11 +376,22 @@ export interface IUserDataSyncResourceEnablementService {
|
||||
setResourceEnablement(resource: SyncResource, enabled: boolean): void;
|
||||
}
|
||||
|
||||
export type SyncResourceConflicts = { syncResource: SyncResource, conflicts: Conflict[] };
|
||||
|
||||
export interface ISyncTask {
|
||||
manifest: IUserDataManifest | null;
|
||||
run(token: CancellationToken): Promise<void>;
|
||||
readonly manifest: IUserDataManifest | null;
|
||||
run(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IManualSyncTask extends IDisposable {
|
||||
readonly id: string;
|
||||
readonly manifest: IUserDataManifest | null;
|
||||
readonly onSynchronizeResources: Event<[SyncResource, URI[]][]>;
|
||||
preview(): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
accept(uri: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
merge(uri?: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
pull(): Promise<void>;
|
||||
push(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
|
||||
@@ -365,10 +400,9 @@ export interface IUserDataSyncService {
|
||||
|
||||
readonly status: SyncStatus;
|
||||
readonly onDidChangeStatus: Event<SyncStatus>;
|
||||
readonly onSynchronizeResource: Event<SyncResource>;
|
||||
|
||||
readonly conflicts: SyncResourceConflicts[];
|
||||
readonly onDidChangeConflicts: Event<SyncResourceConflicts[]>;
|
||||
readonly conflicts: [SyncResource, IResourcePreview[]][];
|
||||
readonly onDidChangeConflicts: Event<[SyncResource, IResourcePreview[]][]>;
|
||||
|
||||
readonly onDidChangeLocal: Event<SyncResource>;
|
||||
readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]>;
|
||||
@@ -376,19 +410,19 @@ export interface IUserDataSyncService {
|
||||
readonly lastSyncTime: number | undefined;
|
||||
readonly onDidChangeLastSyncTime: Event<number>;
|
||||
|
||||
createSyncTask(): Promise<ISyncTask>;
|
||||
createManualSyncTask(): Promise<IManualSyncTask>;
|
||||
|
||||
pull(): Promise<void>;
|
||||
sync(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
replace(uri: URI): Promise<void>;
|
||||
reset(): Promise<void>;
|
||||
resetRemote(): Promise<void>;
|
||||
resetLocal(): Promise<void>;
|
||||
|
||||
createSyncTask(): Promise<ISyncTask>
|
||||
|
||||
isFirstTimeSyncingWithAnotherMachine(): Promise<boolean>;
|
||||
hasLocalData(): Promise<boolean>;
|
||||
hasPreviouslySynced(): Promise<boolean>;
|
||||
resolveContent(resource: URI): Promise<string | null>;
|
||||
acceptConflict(conflictResource: URI, content: string): Promise<void>;
|
||||
acceptPreviewContent(resource: SyncResource, conflictResource: URI, content: string): Promise<void>;
|
||||
|
||||
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
|
||||
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
|
||||
@@ -399,13 +433,11 @@ export interface IUserDataSyncService {
|
||||
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
|
||||
export interface IUserDataAutoSyncService {
|
||||
_serviceBrand: any;
|
||||
readonly onTurnOnSync: Event<void>
|
||||
readonly onDidTurnOnSync: Event<UserDataSyncError | undefined>
|
||||
readonly onError: Event<UserDataSyncError>;
|
||||
readonly onDidChangeEnablement: Event<boolean>;
|
||||
isEnabled(): boolean;
|
||||
canToggleEnablement(): boolean;
|
||||
turnOn(pullFirst: boolean): Promise<void>;
|
||||
turnOn(): Promise<void>;
|
||||
turnOff(everywhere: boolean): Promise<void>;
|
||||
triggerSync(sources: string[], hasToLimitSync: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IServerChannel, IChannel, IPCServer } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest } 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';
|
||||
@@ -17,12 +17,11 @@ import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/use
|
||||
|
||||
export class UserDataSyncChannel implements IServerChannel {
|
||||
|
||||
constructor(private readonly service: IUserDataSyncService, private readonly logService: ILogService) { }
|
||||
constructor(private server: IPCServer, private readonly service: IUserDataSyncService, private readonly logService: ILogService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChangeStatus': return this.service.onDidChangeStatus;
|
||||
case 'onSynchronizeResource': return this.service.onSynchronizeResource;
|
||||
case 'onDidChangeConflicts': return this.service.onDidChangeConflicts;
|
||||
case 'onDidChangeLocal': return this.service.onDidChangeLocal;
|
||||
case 'onDidChangeLastSyncTime': return this.service.onDidChangeLastSyncTime;
|
||||
@@ -44,15 +43,17 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
private _call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]);
|
||||
|
||||
case 'createManualSyncTask': return this.createManualSyncTask();
|
||||
|
||||
case 'pull': return this.service.pull();
|
||||
case 'sync': return this.service.sync();
|
||||
case 'stop': this.service.stop(); return Promise.resolve();
|
||||
case 'replace': return this.service.replace(URI.revive(args[0]));
|
||||
case 'reset': return this.service.reset();
|
||||
case 'resetRemote': return this.service.resetRemote();
|
||||
case 'resetLocal': return this.service.resetLocal();
|
||||
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
|
||||
case 'isFirstTimeSyncingWithAnotherMachine': return this.service.isFirstTimeSyncingWithAnotherMachine();
|
||||
case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]);
|
||||
case 'hasLocalData': return this.service.hasLocalData();
|
||||
case 'acceptPreviewContent': return this.service.acceptPreviewContent(args[0], URI.revive(args[1]), args[2]);
|
||||
case 'resolveContent': return this.service.resolveContent(URI.revive(args[0]));
|
||||
case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]);
|
||||
case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(args[0]);
|
||||
@@ -61,6 +62,39 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
|
||||
private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null }> {
|
||||
const manualSyncTask = await this.service.createManualSyncTask();
|
||||
const manualSyncTaskChannel = new ManualSyncTaskChannel(manualSyncTask);
|
||||
this.server.registerChannel(`manualSyncTask-${manualSyncTask.id}`, manualSyncTaskChannel);
|
||||
return { id: manualSyncTask.id, manifest: manualSyncTask.manifest };
|
||||
}
|
||||
}
|
||||
|
||||
class ManualSyncTaskChannel implements IServerChannel {
|
||||
|
||||
constructor(private readonly manualSyncTask: IManualSyncTask) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onSynchronizeResources': return this.manualSyncTask.onSynchronizeResources;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
async call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'preview': return this.manualSyncTask.preview();
|
||||
case 'accept': return this.manualSyncTask.accept(URI.revive(args[0]), args[1]);
|
||||
case 'merge': return this.manualSyncTask.merge(URI.revive(args[0]));
|
||||
case 'pull': return this.manualSyncTask.pull();
|
||||
case 'push': return this.manualSyncTask.push();
|
||||
case 'stop': return this.manualSyncTask.stop();
|
||||
case 'dispose': return this.manualSyncTask.dispose();
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataAutoSyncChannel implements IServerChannel {
|
||||
@@ -69,8 +103,6 @@ export class UserDataAutoSyncChannel implements IServerChannel {
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onTurnOnSync': return this.service.onTurnOnSync;
|
||||
case 'onDidTurnOnSync': return this.service.onDidTurnOnSync;
|
||||
case 'onError': return this.service.onError;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
@@ -79,7 +111,7 @@ export class UserDataAutoSyncChannel implements IServerChannel {
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'triggerSync': return this.service.triggerSync(args[0], args[1]);
|
||||
case 'turnOn': return this.service.turnOn(args[0]);
|
||||
case 'turnOn': return this.service.turnOn();
|
||||
case 'turnOff': return this.service.turnOff(args[0]);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import {
|
||||
IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode,
|
||||
UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
@@ -21,6 +24,8 @@ import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSy
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
|
||||
type SyncErrorClassification = {
|
||||
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
@@ -29,6 +34,12 @@ type SyncErrorClassification = {
|
||||
|
||||
const LAST_SYNC_TIME_KEY = 'sync.lastSyncTime';
|
||||
|
||||
function createSyncHeaders(executionId: string): IHeaders {
|
||||
const headers: IHeaders = {};
|
||||
headers[HEADER_EXECUTION_ID] = executionId;
|
||||
return headers;
|
||||
}
|
||||
|
||||
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
@@ -40,15 +51,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
private _onDidChangeStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangeStatus.event;
|
||||
|
||||
private _onSynchronizeResource: Emitter<SyncResource> = this._register(new Emitter<SyncResource>());
|
||||
readonly onSynchronizeResource: Event<SyncResource> = this._onSynchronizeResource.event;
|
||||
|
||||
readonly onDidChangeLocal: Event<SyncResource>;
|
||||
|
||||
private _conflicts: SyncResourceConflicts[] = [];
|
||||
get conflicts(): SyncResourceConflicts[] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<SyncResourceConflicts[]> = this._register(new Emitter<SyncResourceConflicts[]>());
|
||||
readonly onDidChangeConflicts: Event<SyncResourceConflicts[]> = this._onDidChangeConflicts.event;
|
||||
private _conflicts: [SyncResource, IResourcePreview[]][] = [];
|
||||
get conflicts(): [SyncResource, IResourcePreview[]][] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<[SyncResource, IResourcePreview[]][]> = this._register(new Emitter<[SyncResource, IResourcePreview[]][]>());
|
||||
readonly onDidChangeConflicts: Event<[SyncResource, IResourcePreview[]][]> = this._onDidChangeConflicts.event;
|
||||
|
||||
private _syncErrors: [SyncResource, UserDataSyncError][] = [];
|
||||
private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>());
|
||||
@@ -95,7 +103,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
try {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
this._onSynchronizeResource.fire(synchroniser.resource);
|
||||
await synchroniser.pull();
|
||||
} catch (e) {
|
||||
this.handleSynchronizerError(e, synchroniser.resource);
|
||||
@@ -129,18 +136,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
}
|
||||
|
||||
private recoveredSettings: boolean = false;
|
||||
async sync(): Promise<void> {
|
||||
const syncTask = await this.createSyncTask();
|
||||
return syncTask.run(CancellationToken.None);
|
||||
}
|
||||
|
||||
async createSyncTask(): Promise<ISyncTask> {
|
||||
this.telemetryService.publicLog2('sync/getmanifest');
|
||||
await this.checkEnablement();
|
||||
|
||||
const executionId = generateUuid();
|
||||
let manifest: IUserDataManifest | null;
|
||||
try {
|
||||
manifest = await this.userDataSyncStoreService.manifest({ 'X-Execution-Id': executionId });
|
||||
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 });
|
||||
@@ -150,20 +152,46 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
let executed = false;
|
||||
const that = this;
|
||||
let cancellablePromise: CancelablePromise<void> | undefined;
|
||||
return {
|
||||
manifest,
|
||||
run(token: CancellationToken): Promise<void> {
|
||||
run(): Promise<void> {
|
||||
if (executed) {
|
||||
throw new Error('Can run a task only once');
|
||||
}
|
||||
return that.doSync(manifest, executionId, token);
|
||||
cancellablePromise = createCancelablePromise(token => that.sync(manifest, executionId, token));
|
||||
return cancellablePromise.finally(() => cancellablePromise = undefined);
|
||||
},
|
||||
async stop(): Promise<void> {
|
||||
if (cancellablePromise) {
|
||||
cancellablePromise.cancel();
|
||||
return that.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async doSync(manifest: IUserDataManifest | null, executionId: string, token: CancellationToken): Promise<void> {
|
||||
async createManualSyncTask(): Promise<IManualSyncTask> {
|
||||
await this.checkEnablement();
|
||||
|
||||
const executionId = generateUuid();
|
||||
const syncHeaders = createSyncHeaders(executionId);
|
||||
|
||||
let manifest: IUserDataManifest | null;
|
||||
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 });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return new ManualSyncTask(executionId, manifest, syncHeaders, this.synchronisers, this.logService);
|
||||
}
|
||||
|
||||
private recoveredSettings: boolean = false;
|
||||
private async sync(manifest: IUserDataManifest | null, executionId: string, token: CancellationToken): Promise<void> {
|
||||
if (!this.recoveredSettings) {
|
||||
await this.settingsSynchroniser.recoverSettings();
|
||||
this.recoveredSettings = true;
|
||||
@@ -182,7 +210,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
}
|
||||
|
||||
const syncHeaders: IHeaders = { 'X-Execution-Id': executionId };
|
||||
const syncHeaders = createSyncHeaders(executionId);
|
||||
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
// Return if cancellation is requested
|
||||
@@ -190,7 +218,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._onSynchronizeResource.fire(synchroniser.resource);
|
||||
await synchroniser.sync(manifest, syncHeaders);
|
||||
} catch (e) {
|
||||
this.handleSynchronizerError(e, synchroniser.resource);
|
||||
@@ -211,18 +238,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
}
|
||||
|
||||
async replace(uri: URI): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.replace(uri)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
|
||||
private async stop(): Promise<void> {
|
||||
if (this.status === SyncStatus.Idle) {
|
||||
return;
|
||||
}
|
||||
@@ -239,15 +255,21 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
}
|
||||
|
||||
async acceptConflict(conflict: URI, content: string): Promise<void> {
|
||||
async replace(uri: URI): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
const syncResourceConflict = this.conflicts.filter(({ conflicts }) => conflicts.some(({ local, remote }) => isEqual(conflict, local) || isEqual(conflict, remote)))[0];
|
||||
if (syncResourceConflict) {
|
||||
const synchroniser = this.getSynchroniser(syncResourceConflict.syncResource);
|
||||
await synchroniser.acceptConflict(conflict, content);
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.replace(uri)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async acceptPreviewContent(syncResource: SyncResource, resource: URI, content: string, executionId: string = generateUuid()): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
const synchroniser = this.getSynchroniser(syncResource);
|
||||
await synchroniser.acceptPreviewContent(resource, content, false, createSyncHeaders(executionId));
|
||||
}
|
||||
|
||||
async resolveContent(resource: URI): Promise<string | null> {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
const content = await synchroniser.resolveContent(resource);
|
||||
@@ -274,35 +296,14 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return this.getSynchroniser(resource).getMachineId(syncResourceHandle);
|
||||
}
|
||||
|
||||
async isFirstTimeSyncingWithAnotherMachine(): Promise<boolean> {
|
||||
await this.checkEnablement();
|
||||
|
||||
if (!await this.userDataSyncStoreService.manifest()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
// skip global state synchronizer
|
||||
const synchronizers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser];
|
||||
|
||||
let hasLocalData: boolean = false;
|
||||
for (const synchroniser of synchronizers) {
|
||||
if (await synchroniser.hasLocalData()) {
|
||||
hasLocalData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasLocalData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const synchroniser of synchronizers) {
|
||||
const preview = await synchroniser.generateSyncPreview();
|
||||
if (preview && !preview.isLastSyncFromCurrentMachine && (preview.hasLocalChanged || preview.hasRemoteChanged)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -312,6 +313,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
await this.resetLocal();
|
||||
}
|
||||
|
||||
async resetRemote(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
try {
|
||||
await this.userDataSyncStoreService.clear();
|
||||
this.logService.info('Cleared data on server');
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL);
|
||||
@@ -335,16 +346,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return false;
|
||||
}
|
||||
|
||||
private async resetRemote(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
try {
|
||||
await this.userDataSyncStoreService.clear();
|
||||
this.logService.info('Cleared data on server');
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private setStatus(status: SyncStatus): void {
|
||||
const oldStatus = this._status;
|
||||
if (this._status !== status) {
|
||||
@@ -364,7 +365,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
private updateConflicts(): void {
|
||||
const conflicts = this.computeConflicts();
|
||||
if (!equals(this._conflicts, conflicts, (a, b) => a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.local, b.local) && isEqual(a.remote, b.remote)))) {
|
||||
if (!equals(this._conflicts, conflicts, ([syncResourceA, conflictsA], [syncResourceB, conflictsB]) => syncResourceA === syncResourceA && equals(conflictsA, conflictsB, (a, b) => isEqual(a.previewResource, b.previewResource)))) {
|
||||
this._conflicts = this.computeConflicts();
|
||||
this._onDidChangeConflicts.fire(conflicts);
|
||||
}
|
||||
@@ -401,7 +402,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
case UserDataSyncErrorCode.LocalTooManyRequests:
|
||||
case UserDataSyncErrorCode.Gone:
|
||||
case UserDataSyncErrorCode.UpgradeRequired:
|
||||
case UserDataSyncErrorCode.Incompatible:
|
||||
case UserDataSyncErrorCode.IncompatibleRemoteContent:
|
||||
case UserDataSyncErrorCode.IncompatibleLocalContent:
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -409,13 +411,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.logService.error(`${source}: ${toErrorMessage(e)}`);
|
||||
}
|
||||
|
||||
private computeConflicts(): SyncResourceConflicts[] {
|
||||
private computeConflicts(): [SyncResource, IResourcePreview[]][] {
|
||||
return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)
|
||||
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts }));
|
||||
.map(s => ([s.resource, s.conflicts.map(toStrictResourcePreview)]));
|
||||
}
|
||||
|
||||
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
|
||||
return this.synchronisers.filter(s => s.resource === source)[0];
|
||||
return this.synchronisers.find(s => s.resource === source)!;
|
||||
}
|
||||
|
||||
private async checkEnablement(): Promise<void> {
|
||||
@@ -425,3 +427,222 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ManualSyncTask extends Disposable implements IManualSyncTask {
|
||||
|
||||
private previewsPromise: CancelablePromise<[SyncResource, ISyncResourcePreview][]> | undefined;
|
||||
private previews: [SyncResource, ISyncResourcePreview][] | undefined;
|
||||
|
||||
private synchronizingResources: [SyncResource, URI[]][] = [];
|
||||
private _onSynchronizeResources = this._register(new Emitter<[SyncResource, URI[]][]>());
|
||||
readonly onSynchronizeResources = this._onSynchronizeResources.event;
|
||||
|
||||
private isDisposed: boolean = false;
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly manifest: IUserDataManifest | null,
|
||||
private readonly syncHeaders: IHeaders,
|
||||
private readonly synchronisers: IUserDataSynchroniser[],
|
||||
private readonly logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async preview(): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (this.isDisposed) {
|
||||
throw new Error('Disposed');
|
||||
}
|
||||
if (!this.previewsPromise) {
|
||||
this.previewsPromise = createCancelablePromise(token => this.getPreviews(token));
|
||||
}
|
||||
this.previews = await this.previewsPromise;
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
async accept(resource: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
return this.mergeOrAccept(resource, (sychronizer, force) => sychronizer.acceptPreviewContent(resource, content, force, this.syncHeaders));
|
||||
}
|
||||
|
||||
async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (resource) {
|
||||
return this.mergeOrAccept(resource, (sychronizer, force) => sychronizer.merge(resource, force, this.syncHeaders));
|
||||
} else {
|
||||
return this.mergeAll();
|
||||
}
|
||||
}
|
||||
|
||||
private async mergeOrAccept(resource: URI, mergeOrAccept: (synchroniser: IUserDataSynchroniser, force: boolean) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (!this.previews) {
|
||||
throw new Error('You need to create preview before merging or accepting');
|
||||
}
|
||||
|
||||
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])!;
|
||||
/* force only if the resource is local or remote resource */
|
||||
const force = isEqual(resource, resourcePreview.localResource) || isEqual(resource, resourcePreview.remoteResource);
|
||||
const preview = await mergeOrAccept(synchroniser, force);
|
||||
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');
|
||||
}
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot merge 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)!;
|
||||
let syncResourcePreview = null;
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
syncResourcePreview = await synchroniser.merge(resourcePreview.remoteResource, false, this.syncHeaders);
|
||||
}
|
||||
if (syncResourcePreview) {
|
||||
previews.push([syncResource, syncResourcePreview]);
|
||||
}
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
this.previews = previews;
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.previews) {
|
||||
throw new Error('You need to create preview before applying');
|
||||
}
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot pull while synchronizing resources');
|
||||
}
|
||||
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)!;
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
const content = await synchroniser.resolveContent(resourcePreview.remoteResource) || '';
|
||||
await synchroniser.acceptPreviewContent(resourcePreview.remoteResource, content, true, this.syncHeaders);
|
||||
}
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
this.previews = [];
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
if (!this.previews) {
|
||||
throw new Error('You need to create preview before applying');
|
||||
}
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot pull while synchronizing resources');
|
||||
}
|
||||
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)!;
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
const content = await synchroniser.resolveContent(resourcePreview.localResource) || '';
|
||||
await synchroniser.acceptPreviewContent(resourcePreview.localResource, content, true, this.syncHeaders);
|
||||
}
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
this.previews = [];
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.stop();
|
||||
} catch (error) {
|
||||
if (!isPromiseCanceledError(error)) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private async getPreviews(token: CancellationToken): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
const result: [SyncResource, ISyncResourcePreview][] = [];
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
const preview = await synchroniser.preview(this.manifest, this.syncHeaders);
|
||||
if (preview) {
|
||||
result.push(this.toSyncResourcePreview(synchroniser.resource, preview));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private toSyncResourcePreview(syncResource: SyncResource, preview: ISyncResourcePreview): [SyncResource, ISyncResourcePreview] {
|
||||
return [
|
||||
syncResource,
|
||||
{
|
||||
isLastSyncFromCurrentMachine: preview.isLastSyncFromCurrentMachine,
|
||||
resourcePreviews: preview.resourcePreviews.map(toStrictResourcePreview)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
if (this.previewsPromise) {
|
||||
this.previewsPromise.cancel();
|
||||
this.previewsPromise = undefined;
|
||||
}
|
||||
this.previews = undefined;
|
||||
this.synchronizingResources = [];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.reset();
|
||||
this.isDisposed = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toStrictResourcePreview(resourcePreview: IResourcePreview): IResourcePreview {
|
||||
return {
|
||||
localResource: resourcePreview.localResource,
|
||||
previewResource: resourcePreview.previewResource,
|
||||
remoteResource: resourcePreview.remoteResource,
|
||||
localChange: resourcePreview.localChange,
|
||||
remoteChange: resourcePreview.remoteChange,
|
||||
merged: resourcePreview.merged,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
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 { joinPath, relativePath } from 'vs/base/common/resources';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
@@ -65,7 +65,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
});
|
||||
|
||||
/* A requests session that limits requests per sessions */
|
||||
this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService);
|
||||
this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService, this.logService);
|
||||
}
|
||||
|
||||
setAuthToken(token: string, type: string): void {
|
||||
@@ -83,7 +83,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
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);
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
|
||||
const result = await asJson<{ url: string, created: number }[]>(context) || [];
|
||||
@@ -102,7 +102,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
|
||||
const content = await asText(context);
|
||||
@@ -120,7 +120,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,12 +145,12 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
|
||||
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);
|
||||
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
const content = await asText(context);
|
||||
return { ref, content };
|
||||
@@ -171,12 +171,12 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
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);
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
|
||||
const newRef = context.res.headers['etag'];
|
||||
if (!newRef) {
|
||||
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef);
|
||||
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
return newRef;
|
||||
}
|
||||
@@ -192,7 +192,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
|
||||
const manifest = await asJson<IUserDataManifest>(context);
|
||||
@@ -227,7 +227,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
|
||||
// clear cached session.
|
||||
@@ -241,7 +241,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
|
||||
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
if (!this.authToken) {
|
||||
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized);
|
||||
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, undefined);
|
||||
}
|
||||
|
||||
const commonHeaders = await this.commonHeadersPromise;
|
||||
@@ -258,40 +258,48 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
let context;
|
||||
try {
|
||||
context = await this.session.request(options, token);
|
||||
this.logService.trace('Request finished', { url: options.url, status: context.res.statusCode });
|
||||
} catch (e) {
|
||||
if (!(e instanceof UserDataSyncStoreError)) {
|
||||
e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused);
|
||||
e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, undefined);
|
||||
}
|
||||
this.logService.info('Request failed', options.url);
|
||||
throw e;
|
||||
}
|
||||
|
||||
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)) {
|
||||
this.logService.trace('Request succeeded', requestInfo);
|
||||
} else {
|
||||
this.logService.info('Request failed', requestInfo);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 401) {
|
||||
this.authToken = undefined;
|
||||
this._onTokenFailed.fire();
|
||||
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized);
|
||||
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, operationId);
|
||||
}
|
||||
|
||||
this._onTokenSucceed.fire();
|
||||
|
||||
if (context.res.statusCode === 410) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because the requested resource is not longer available (410).`, UserDataSyncErrorCode.Gone);
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because the requested resource is not longer available (410).`, UserDataSyncErrorCode.Gone, operationId);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 412) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed);
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed, operationId);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 413) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge);
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge, operationId);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 426) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed with status Upgrade Required (426). Please upgrade the client and try again.`, UserDataSyncErrorCode.UpgradeRequired);
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed with status Upgrade Required (426). Please upgrade the client and try again.`, UserDataSyncErrorCode.UpgradeRequired, operationId);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 429) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests);
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests, operationId);
|
||||
}
|
||||
|
||||
return context;
|
||||
@@ -315,13 +323,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
|
||||
export class RequestsSession {
|
||||
|
||||
private count: number = 0;
|
||||
private requests: string[] = [];
|
||||
private startTime: Date | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
private readonly limit: number,
|
||||
private readonly interval: number, /* in ms */
|
||||
private readonly requestService: IRequestService,
|
||||
private readonly logService: IUserDataSyncLogService,
|
||||
) { }
|
||||
|
||||
request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
@@ -329,12 +338,13 @@ export class RequestsSession {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
if (this.count >= this.limit) {
|
||||
throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests);
|
||||
if (this.requests.length >= this.limit) {
|
||||
this.logService.info('Too many requests', ...this.requests);
|
||||
throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests, undefined);
|
||||
}
|
||||
|
||||
this.startTime = this.startTime || new Date();
|
||||
this.count++;
|
||||
this.requests.push(options.url!);
|
||||
|
||||
return this.requestService.request(options, token);
|
||||
}
|
||||
@@ -344,7 +354,7 @@ export class RequestsSession {
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
this.count = 0;
|
||||
this.requests = [];
|
||||
this.startTime = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -116,11 +116,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries', async () => {
|
||||
@@ -129,11 +131,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries in different order', async () => {
|
||||
@@ -142,11 +146,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with different base content', async () => {
|
||||
@@ -156,11 +162,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote', async () => {
|
||||
@@ -169,11 +177,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to remote', async () => {
|
||||
@@ -182,11 +192,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, remote);
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, remote);
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when new entry is added to remote from base and local has not changed', async () => {
|
||||
@@ -195,11 +207,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from remote from base and local has not changed', async () => {
|
||||
@@ -208,11 +222,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when all entries are removed from base and local has not changed', async () => {
|
||||
@@ -221,11 +237,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in remote from base and local has not changed', async () => {
|
||||
@@ -234,11 +252,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.equal(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => {
|
||||
@@ -247,11 +267,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.local.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.local.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when a new entries are added to local', async () => {
|
||||
@@ -260,11 +282,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
|
||||
@@ -273,11 +297,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1, 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.added, { 'html.json': htmlSnippet1, 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from local from base and remote has not changed', async () => {
|
||||
@@ -286,11 +312,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, ['typescript.json']);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in local from base and remote has not changed', async () => {
|
||||
@@ -299,11 +327,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => {
|
||||
@@ -312,11 +342,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.remote.removed, ['typescript.json']);
|
||||
});
|
||||
|
||||
test('merge when local and remote with one entry but different value', async () => {
|
||||
@@ -325,11 +357,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
|
||||
@@ -339,11 +373,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge with single entry and local is empty', async () => {
|
||||
@@ -353,11 +389,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with conflicts', async () => {
|
||||
@@ -367,41 +405,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet });
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with resolved conflicts - update', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet2 };
|
||||
const resolvedConflicts = { 'html.json': htmlSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, base, resolvedConflicts);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'html.json': htmlSnippet2, 'c.json': cSnippet });
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with resolved conflicts - remove', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet2 };
|
||||
const resolvedConflicts = { 'html.json': null };
|
||||
|
||||
const actual = merge(local, remote, base, resolvedConflicts);
|
||||
|
||||
assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, ['html.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with multiple conflicts', async () => {
|
||||
@@ -411,26 +421,13 @@ suite('SnippetsMerge', () => {
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, {});
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with multiple conflicts and resolving one conflict', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'c.json': cSnippet };
|
||||
const resolvedConflicts = { 'html.json': htmlSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, base, resolvedConflicts);
|
||||
|
||||
assert.deepEqual(actual.added, {});
|
||||
assert.deepEqual(actual.updated, { 'html.json': htmlSnippet1 });
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['typescript.json']);
|
||||
assert.deepEqual(actual.remote, { 'c.json': cSnippet, 'html.json': htmlSnippet1 });
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncData } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, PREVIEW_DIR_NAME, ISyncData, IResourcePreview } 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 { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
@@ -12,8 +12,9 @@ import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { joinPath, dirname } from 'vs/base/common/resources';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
const tsSnippet1 = `{
|
||||
|
||||
@@ -276,7 +277,7 @@ suite('SnippetsSync', () => {
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
|
||||
assertPreviews(testObject.conflicts, [local]);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has conflicts and accept conflicts', async () => {
|
||||
@@ -286,12 +287,10 @@ suite('SnippetsSync', () => {
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
const conflicts = testObject.conflicts;
|
||||
await testObject.acceptConflict(conflicts[0].local, htmlSnippet1);
|
||||
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet1, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
assert.ok(!await fileService.exists(conflicts[0].local));
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
@@ -315,10 +314,7 @@ suite('SnippetsSync', () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local1 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
const local2 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
|
||||
assertConflicts(testObject.conflicts, [
|
||||
{ local: local1, remote: local1.with({ scheme: USER_DATA_SYNC_SCHEME }) },
|
||||
{ local: local2, remote: local2.with({ scheme: USER_DATA_SYNC_SCHEME }) }
|
||||
]);
|
||||
assertPreviews(testObject.conflicts, [local1, local2]);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has multiple conflicts and accept one conflict', async () => {
|
||||
@@ -331,15 +327,13 @@ suite('SnippetsSync', () => {
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
let conflicts = testObject.conflicts;
|
||||
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
assert.ok(!await fileService.exists(conflicts[0].local));
|
||||
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2, false);
|
||||
|
||||
conflicts = testObject.conflicts;
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
|
||||
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
|
||||
assertPreviews(testObject.conflicts, [local]);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has multiple conflicts and accept all conflicts', async () => {
|
||||
@@ -352,14 +346,11 @@ suite('SnippetsSync', () => {
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
const conflicts = testObject.conflicts;
|
||||
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
|
||||
await testObject.acceptConflict(conflicts[1].local, tsSnippet1);
|
||||
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2, false);
|
||||
await testObject.acceptPreviewContent(conflicts[1].previewResource, tsSnippet1, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
assert.ok(!await fileService.exists(conflicts[0].local));
|
||||
assert.ok(!await fileService.exists(conflicts[1].local));
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet2);
|
||||
@@ -457,7 +448,7 @@ suite('SnippetsSync', () => {
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
|
||||
assertPreviews(testObject.conflicts, [local]);
|
||||
});
|
||||
|
||||
test('sync updating a snippet - resolve conflict', async () => {
|
||||
@@ -470,7 +461,7 @@ suite('SnippetsSync', () => {
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet3, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2);
|
||||
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, htmlSnippet2, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
@@ -560,7 +551,7 @@ suite('SnippetsSync', () => {
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]);
|
||||
assertPreviews(testObject.conflicts, [local]);
|
||||
});
|
||||
|
||||
test('sync removing a snippet - resolve conflict', async () => {
|
||||
@@ -574,7 +565,7 @@ suite('SnippetsSync', () => {
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3);
|
||||
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, htmlSnippet3, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
@@ -601,7 +592,7 @@ suite('SnippetsSync', () => {
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
await testObject.acceptConflict(testObject.conflicts[0].local, '');
|
||||
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, '', false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
@@ -689,6 +680,220 @@ suite('SnippetsSync', () => {
|
||||
assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'global.code-snippets': globalSnippet });
|
||||
});
|
||||
|
||||
test('previews are reset after all conflicts resolved', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
let conflicts = testObject.conflicts;
|
||||
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2, false);
|
||||
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
assert.ok(!await fileService.exists(dirname(conflicts[0].previewResource)));
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets and only one snippet is merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].localResource, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets and all snippets are merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].localResource, false);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[1].localResource, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets and one snippet has no changes and one snippet is merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].localResource, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets with conflicts and only one snippet is merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assertPreviews(testObject.conflicts,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets with conflicts and all snippets are merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[1].previewResource, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assertPreviews(testObject.conflicts,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('accept when there are multiple snippets with conflicts and only one snippet is accepted', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, htmlSnippet2, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('accept when there are multiple snippets with conflicts and all snippets are accepted', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, htmlSnippet2, false);
|
||||
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[1].previewResource, tsSnippet2, false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
function parseSnippets(content: string): IStringDictionary<string> {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
return JSON.parse(syncData.content);
|
||||
@@ -719,8 +924,8 @@ suite('SnippetsSync', () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
function assertConflicts(actual: Conflict[], expected: Conflict[]) {
|
||||
assert.deepEqual(actual.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })), expected.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })));
|
||||
function assertPreviews(actual: IResourcePreview[], expected: URI[]) {
|
||||
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -4,31 +4,37 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData, Change, USER_DATA_SYNC_SCHEME, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
interface ITestSyncPreview extends ISyncResourcePreview {
|
||||
ref?: string;
|
||||
}
|
||||
const resource = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'testResource', path: `/current.json` });
|
||||
|
||||
class TestSynchroniser extends AbstractSynchroniser {
|
||||
|
||||
syncBarrier: Barrier = new Barrier();
|
||||
syncResult: { hasConflicts: boolean, hasError: boolean } = { hasConflicts: false, hasError: false };
|
||||
onDoSyncCall: Emitter<void> = this._register(new Emitter<void>());
|
||||
failWhenGettingLatestRemoteUserData: boolean = false;
|
||||
|
||||
readonly resource: SyncResource = SyncResource.Settings;
|
||||
protected readonly version: number = 1;
|
||||
|
||||
private cancelled: boolean = false;
|
||||
|
||||
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
protected getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
|
||||
if (this.failWhenGettingLatestRemoteUserData) {
|
||||
throw new Error();
|
||||
}
|
||||
return super.getLatestRemoteUserData(manifest, lastSyncUserData);
|
||||
}
|
||||
|
||||
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
|
||||
this.cancelled = false;
|
||||
this.onDoSyncCall.fire();
|
||||
await this.syncBarrier.wait();
|
||||
@@ -37,39 +43,35 @@ class TestSynchroniser extends AbstractSynchroniser {
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
return super.doSync(remoteUserData, lastSyncUserData);
|
||||
return super.doSync(remoteUserData, lastSyncUserData, apply);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: remoteUserData.ref, previewResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: remoteUserData.ref, previewResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ITestSyncPreview> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IResourcePreview[]> {
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: remoteUserData.ref, previewResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
|
||||
if (this.syncResult.hasError) {
|
||||
throw new Error('failed');
|
||||
}
|
||||
return { ref: remoteUserData.ref, hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, previewContent: remoteUserData.ref, previewResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, conflictContent: string): Promise<ISyncResourcePreview> {
|
||||
return preview;
|
||||
}
|
||||
|
||||
protected async applyPreview({ ref }: ITestSyncPreview, forcePush: boolean): Promise<void> {
|
||||
if (ref) {
|
||||
await this.apply(ref);
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, preview: IResourcePreview[], forcePush: boolean): Promise<void> {
|
||||
if (preview[0]?.previewContent) {
|
||||
await this.applyRef(preview[0].previewContent);
|
||||
}
|
||||
}
|
||||
|
||||
async apply(ref: string): Promise<void> {
|
||||
async applyRef(ref: string): Promise<void> {
|
||||
const remoteUserData = await this.updateRemoteUserData('', ref);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
}
|
||||
@@ -137,19 +139,6 @@ suite('TestSynchronizer', () => {
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
});
|
||||
|
||||
test('status is set correctly when sync has conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
const actual: SyncStatus[] = [];
|
||||
disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status)));
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.HasConflicts]);
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
});
|
||||
|
||||
test('status is set correctly when sync has errors', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasError: true, hasConflicts: false };
|
||||
@@ -167,6 +156,17 @@ suite('TestSynchronizer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('status is set to hasConflicts when asked to sync if there are 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);
|
||||
assertConflicts(testObject.conflicts, [resource]);
|
||||
});
|
||||
|
||||
test('sync should not run if syncing already', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
const promise = Event.toPromise(testObject.onDoSyncCall.event);
|
||||
@@ -181,7 +181,7 @@ suite('TestSynchronizer', () => {
|
||||
assert.deepEqual(actual, []);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
|
||||
testObject.stop();
|
||||
await testObject.stop();
|
||||
});
|
||||
|
||||
test('sync should not run if disabled', async () => {
|
||||
@@ -221,7 +221,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.apply(ref);
|
||||
await testObject.applyRef(ref);
|
||||
server.reset();
|
||||
testObject.syncBarrier.open();
|
||||
});
|
||||
@@ -251,5 +251,115 @@ suite('TestSynchronizer', () => {
|
||||
assert.deepEqual(server.requests, []);
|
||||
});
|
||||
|
||||
test('status is reset when getting latest remote data fails', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.failWhenGettingLatestRemoteUserData = true;
|
||||
|
||||
try {
|
||||
await testObject.sync(await client.manifest());
|
||||
assert.fail('Should throw an error');
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
});
|
||||
|
||||
test('preview: status is set to syncing when asked for preview if there are no conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
const preview = await testObject.preview(await client.manifest());
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to idle after merging if there are no conflicts', 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, false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to idle and sync is applied after accepting when there are no conflicts before merging', 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.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!, false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to syncing when asked for preview if there are conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
const preview = await testObject.preview(await client.manifest());
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to hasConflicts after merging', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
const preview = await testObject.preview(await client.manifest());
|
||||
await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].previewResource]);
|
||||
});
|
||||
|
||||
test('preview: status is set to idle and sync is applied after accepting when there are conflicts', 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, false);
|
||||
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!, false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to idle and sync is applied after accepting when there are conflicts before merging', 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.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!, false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
function assertConflicts(actual: IResourcePreview[], expected: URI[]) {
|
||||
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
|
||||
}
|
||||
|
||||
function assertPreviews(actual: IResourcePreview[], expected: URI[]) {
|
||||
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ suite('UserDataAutoSyncService', () => {
|
||||
await client.setUp();
|
||||
|
||||
// Sync once and reset requests
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
@@ -59,7 +59,7 @@ suite('UserDataAutoSyncService', () => {
|
||||
await client.setUp();
|
||||
|
||||
// Sync once and reset requests
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
@@ -85,7 +85,7 @@ suite('UserDataAutoSyncService', () => {
|
||||
await client.setUp();
|
||||
|
||||
// Sync once and reset requests
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
@@ -107,7 +107,7 @@ suite('UserDataAutoSyncService', () => {
|
||||
await client.setUp();
|
||||
|
||||
// Sync once and reset requests
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
@@ -245,7 +245,7 @@ suite('UserDataAutoSyncService', () => {
|
||||
// Set up and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
@@ -334,7 +334,7 @@ suite('UserDataAutoSyncService', () => {
|
||||
// Set up and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
@@ -346,7 +346,7 @@ suite('UserDataAutoSyncService', () => {
|
||||
await client.instantiationService.get(IUserDataSyncService).reset();
|
||||
|
||||
// Sync again from the first client to create new session
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Sync from the test client
|
||||
target.reset();
|
||||
@@ -383,5 +383,4 @@ suite('UserDataAutoSyncService', () => {
|
||||
assert.deepEqual((<UserDataSyncStoreError>e).code, UserDataSyncErrorCode.TooManyRequests);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -120,8 +120,8 @@ export class UserDataSyncClient extends Disposable {
|
||||
await configurationService.reloadConfiguration();
|
||||
}
|
||||
|
||||
sync(): Promise<void> {
|
||||
return this.instantiationService.get(IUserDataSyncService).sync();
|
||||
async sync(): Promise<void> {
|
||||
await (await this.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
}
|
||||
|
||||
read(resource: SyncResource): Promise<IUserData> {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing tests
|
||||
|
||||
@@ -27,7 +26,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync for first time
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
@@ -58,7 +57,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync for first time
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
@@ -83,7 +82,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
@@ -92,17 +91,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
|
||||
// Sync (pull) from the test client
|
||||
target.reset();
|
||||
await testObject.isFirstTimeSyncingWithAnotherMachine();
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ 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/extensions/latest`, headers: {} },
|
||||
/* pull */
|
||||
{ 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: {} },
|
||||
@@ -118,7 +109,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client with changes
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
@@ -132,14 +123,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
|
||||
// Sync (pull) from the test client
|
||||
target.reset();
|
||||
await testObject.isFirstTimeSyncingWithAnotherMachine();
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
/* pull */
|
||||
{ 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: {} },
|
||||
@@ -155,7 +141,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
@@ -164,17 +150,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
|
||||
// Sync (merge) from the test client
|
||||
target.reset();
|
||||
await testObject.isFirstTimeSyncingWithAnotherMachine();
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ 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/extensions/latest`, headers: {} },
|
||||
/* sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
@@ -191,7 +169,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client with changes
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
@@ -206,15 +184,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
|
||||
// Sync (merge) from the test client
|
||||
target.reset();
|
||||
await testObject.isFirstTimeSyncingWithAnotherMachine();
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
|
||||
@@ -235,11 +207,11 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// sync from the client again
|
||||
target.reset();
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
@@ -254,7 +226,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
// Do changes in the client
|
||||
@@ -266,7 +238,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
|
||||
// Sync from the client
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
@@ -288,13 +260,13 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
// Sync from first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Sync from test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// Do changes in first client and sync
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
@@ -303,11 +275,11 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{ "a": "changed" }`));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Sync from test client
|
||||
target.reset();
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
@@ -331,7 +303,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// Reset from the client
|
||||
target.reset();
|
||||
@@ -351,14 +323,14 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// Reset from the client
|
||||
await testObject.reset();
|
||||
|
||||
// Sync again
|
||||
target.reset();
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
@@ -392,7 +364,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
// sync from the client
|
||||
const actualStatuses: SyncStatus[] = [];
|
||||
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
disposable.dispose();
|
||||
assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]);
|
||||
@@ -407,7 +379,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
let fileService = client.instantiationService.get(IFileService);
|
||||
let environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
@@ -418,10 +390,10 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// sync from the client
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
assert.deepEqual(testObject.conflicts.map(({ syncResource }) => syncResource), [SyncResource.Settings]);
|
||||
assert.deepEqual(testObject.conflicts.map(([syncResource]) => syncResource), [SyncResource.Settings]);
|
||||
});
|
||||
|
||||
test('test sync will sync other non conflicted areas', async () => {
|
||||
@@ -433,7 +405,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
let fileService = client.instantiationService.get(IFileService);
|
||||
let environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client and get conflicts in settings
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
@@ -442,17 +414,17 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
let testEnvironmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await testFileService.writeFile(testEnvironmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// sync from the first client with changes in keybindings
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// sync from the test client
|
||||
target.reset();
|
||||
const actualStatuses: SyncStatus[] = [];
|
||||
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
disposable.dispose();
|
||||
assert.deepEqual(actualStatuses, []);
|
||||
@@ -475,7 +447,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
let fileService = client.instantiationService.get(IFileService);
|
||||
let environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
@@ -484,10 +456,11 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// sync from the client
|
||||
await testObject.stop();
|
||||
|
||||
const syncTask = (await testObject.createSyncTask());
|
||||
syncTask.run();
|
||||
await syncTask.stop();
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
@@ -500,7 +473,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
await testObject.sync();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
for (const request of target.requestsWithAllHeaders) {
|
||||
const hasExecutionIdHeader = request.headers && request.headers['X-Execution-Id'] && request.headers['X-Execution-Id'].length > 0;
|
||||
@@ -517,10 +490,10 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
const syncTask = await testObject.createSyncTask();
|
||||
await syncTask.run(CancellationToken.None);
|
||||
await syncTask.run();
|
||||
|
||||
try {
|
||||
await syncTask.run(CancellationToken.None);
|
||||
await syncTask.run();
|
||||
assert.fail('Should fail running the task again');
|
||||
} catch (error) {
|
||||
/* expected */
|
||||
|
||||
@@ -14,6 +14,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { newWriteableBufferStream } from 'vs/base/common/buffer';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
|
||||
suite('UserDataSyncStoreService', () => {
|
||||
|
||||
@@ -332,7 +333,7 @@ suite('UserDataSyncRequestsSession', () => {
|
||||
};
|
||||
|
||||
test('too many requests are thrown when limit exceeded', async () => {
|
||||
const testObject = new RequestsSession(1, 500, requestService);
|
||||
const testObject = new RequestsSession(1, 500, requestService, new NullLogService());
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
|
||||
try {
|
||||
@@ -346,14 +347,14 @@ suite('UserDataSyncRequestsSession', () => {
|
||||
});
|
||||
|
||||
test('requests are handled after session is expired', async () => {
|
||||
const testObject = new RequestsSession(1, 500, requestService);
|
||||
const testObject = new RequestsSession(1, 500, requestService, new NullLogService());
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
await timeout(600);
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
});
|
||||
|
||||
test('too many requests are thrown after session is expired', async () => {
|
||||
const testObject = new RequestsSession(1, 500, requestService);
|
||||
const testObject = new RequestsSession(1, 500, requestService, new NullLogService());
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
await timeout(600);
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
|
||||
Reference in New Issue
Block a user