mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 17:22:59 -05:00
Merge from vscode 61d5f2b82f17bf9f99f56405204caab88a7e8747
This commit is contained in:
@@ -7,9 +7,9 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, dirname } from 'vs/base/common/resources';
|
||||
import { joinPath, dirname, isEqual } from 'vs/base/common/resources';
|
||||
import { CancelablePromise } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
@@ -20,6 +20,7 @@ import { localize } from 'vs/nls';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { uppercaseFirstLetter } from 'vs/base/common/strings';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
|
||||
type SyncSourceClassification = {
|
||||
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
@@ -51,6 +52,11 @@ 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;
|
||||
|
||||
protected readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
|
||||
|
||||
@@ -87,6 +93,16 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +170,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return !!lastSyncData;
|
||||
}
|
||||
|
||||
async getRemoteContentFromPreview(): Promise<string | null> {
|
||||
async getConflictContent(conflictResource: URI): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -285,15 +301,22 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
this.cancel();
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
|
||||
try {
|
||||
await this.fileService.del(this.conflictsPreviewResource);
|
||||
await this.fileService.del(this.localPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
async getRemoteContentFromPreview(): Promise<string | null> {
|
||||
if (this.syncPreviewResultPromise) {
|
||||
const result = await this.syncPreviewResultPromise;
|
||||
return result.remoteUserData && result.remoteUserData.syncData ? result.remoteUserData.syncData.content : null;
|
||||
async getConflictContent(conflictResource: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) {
|
||||
if (this.syncPreviewResultPromise) {
|
||||
const result = await this.syncPreviewResultPromise;
|
||||
if (isEqual(this.remotePreviewResource, conflictResource)) {
|
||||
return result.remoteUserData && result.remoteUserData.syncData ? result.remoteUserData.syncData.content : null;
|
||||
}
|
||||
if (isEqual(this.localPreviewResource, conflictResource)) {
|
||||
return result.fileContent ? result.fileContent.value.toString() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -356,7 +379,8 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract readonly conflictsPreviewResource: URI;
|
||||
protected abstract readonly localPreviewResource: URI;
|
||||
protected abstract readonly remotePreviewResource: URI;
|
||||
}
|
||||
|
||||
export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
|
||||
|
||||
@@ -11,11 +11,11 @@ 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 { localize } from 'vs/nls';
|
||||
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly localExtensions: ISyncExtension[];
|
||||
@@ -147,7 +147,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
return null;
|
||||
}
|
||||
|
||||
accept(content: string): Promise<void> {
|
||||
async acceptConflict(conflict: URI, content: string): Promise<void> {
|
||||
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
|
||||
}
|
||||
|
||||
@@ -230,9 +230,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const extensionsToRemove = installedExtensions.filter(({ identifier }) => removed.some(r => areSameExtensions(identifier, r)));
|
||||
await Promise.all(extensionsToRemove.map(async extensionToRemove => {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Uninstalling local extension...', extensionToRemove.identifier.i`);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Uninstalling local extension...`, extensionToRemove.identifier.id);
|
||||
await this.extensionManagementService.uninstall(extensionToRemove);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Uninstalled local extension.', extensionToRemove.identifier.i`);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Uninstalled local extension.`, extensionToRemove.identifier.id);
|
||||
removeFromSkipped.push(extensionToRemove.identifier);
|
||||
}));
|
||||
}
|
||||
@@ -245,13 +245,13 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
// Builtin Extension: Sync only enablement state
|
||||
if (installedExtension && installedExtension.type === ExtensionType.System) {
|
||||
if (e.disabled) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...', e.identifier.i`);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id);
|
||||
await this.extensionEnablementService.disableExtension(e.identifier);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension', e.identifier.i`);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id);
|
||||
} else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...', e.identifier.i`);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id);
|
||||
await this.extensionEnablementService.enableExtension(e.identifier);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension', e.identifier.i`);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id);
|
||||
}
|
||||
removeFromSkipped.push(e.identifier);
|
||||
return;
|
||||
@@ -261,25 +261,25 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
if (extension) {
|
||||
try {
|
||||
if (e.disabled) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...', e.identifier.id, extension.versio`);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id, extension.version);
|
||||
await this.extensionEnablementService.disableExtension(extension.identifier);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension', e.identifier.id, extension.versio`);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id, extension.version);
|
||||
} else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...', e.identifier.id, extension.versio`);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id, extension.version);
|
||||
await this.extensionEnablementService.enableExtension(extension.identifier);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension', e.identifier.id, extension.versio`);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id, extension.version);
|
||||
}
|
||||
// Install only if the extension does not exist
|
||||
if (!installedExtension || installedExtension.manifest.version !== extension.version) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...', e.identifier.id, extension.versio`);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...`, e.identifier.id, extension.version);
|
||||
await this.extensionManagementService.installFromGallery(extension);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Installed extension.', e.identifier.id, extension.versio`);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Installed extension.`, e.identifier.id, extension.version);
|
||||
removeFromSkipped.push(extension.identifier);
|
||||
}
|
||||
} catch (error) {
|
||||
addToSkipped.push(e);
|
||||
this.logService.error(error);
|
||||
this.logService.info(localize('skip extension', "Skipped synchronizing extension {0}", extension.displayName || extension.identifier.id));
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension`, extension.displayName || extension.identifier.id);
|
||||
}
|
||||
} else {
|
||||
addToSkipped.push(e);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { parse } from 'vs/base/common/json';
|
||||
import { AbstractSynchroniser, IRemoteUserData } 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';
|
||||
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
@@ -131,7 +132,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
return null;
|
||||
}
|
||||
|
||||
accept(content: string): Promise<void> {
|
||||
async acceptConflict(conflict: URI, content: string): Promise<void> {
|
||||
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
@@ -19,6 +19,7 @@ import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, isEqual } from 'vs/base/common/resources';
|
||||
|
||||
interface ISyncContent {
|
||||
mac?: string;
|
||||
@@ -29,8 +30,9 @@ interface ISyncContent {
|
||||
|
||||
export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
protected get conflictsPreviewResource(): URI { return this.environmentService.keybindingsSyncPreviewResource; }
|
||||
protected readonly version: number = 1;
|
||||
protected readonly localPreviewResource: URI = joinPath(this.syncFolder, PREVIEW_DIR_NAME, 'keybindings.json');
|
||||
protected readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME });
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@@ -39,7 +41,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
) {
|
||||
@@ -129,8 +131,10 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
|
||||
}
|
||||
|
||||
async accept(content: string): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
async acceptConflict(conflict: URI, content: string): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts
|
||||
&& (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict))
|
||||
) {
|
||||
const preview = await this.syncPreviewResultPromise!;
|
||||
this.cancel();
|
||||
this.syncPreviewResultPromise = createCancelablePromise(async () => ({ ...preview, content }));
|
||||
@@ -156,8 +160,8 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
return false;
|
||||
}
|
||||
|
||||
async getRemoteContentFromPreview(): Promise<string | null> {
|
||||
const content = await super.getRemoteContentFromPreview();
|
||||
async getConflictContent(conflictResource: URI): Promise<string | null> {
|
||||
const content = await super.getConflictContent(conflictResource);
|
||||
return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null;
|
||||
}
|
||||
|
||||
@@ -224,7 +228,9 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
|
||||
if (hasLocalChanged) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
|
||||
await this.backupLocal(this.toSyncContent(content, null));
|
||||
if (fileContent) {
|
||||
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
|
||||
}
|
||||
await this.updateLocalFileContent(content, fileContent);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
|
||||
}
|
||||
@@ -238,7 +244,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(this.conflictsPreviewResource);
|
||||
await this.fileService.del(this.localPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`);
|
||||
@@ -303,9 +309,11 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
}
|
||||
|
||||
if (content && !token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(content));
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
|
||||
}
|
||||
|
||||
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
|
||||
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
|
||||
@@ -4,24 +4,23 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { createCancelablePromise } from 'vs/base/common/async';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { updateIgnoredSettings, merge, getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { isEmptyObject } from 'vs/base/common/types';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } 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';
|
||||
import { joinPath, isEqual } from 'vs/base/common/resources';
|
||||
|
||||
export interface ISettingsSyncContent {
|
||||
settings: string;
|
||||
@@ -38,16 +37,12 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
_serviceBrand: any;
|
||||
|
||||
protected readonly version: number = 1;
|
||||
protected get conflictsPreviewResource(): URI { return this.environmentService.settingsSyncPreviewResource; }
|
||||
|
||||
private _conflicts: IConflictSetting[] = [];
|
||||
get conflicts(): IConflictSetting[] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<IConflictSetting[]> = this._register(new Emitter<IConflictSetting[]>());
|
||||
readonly onDidChangeConflicts: Event<IConflictSetting[]> = this._onDidChangeConflicts.event;
|
||||
protected readonly localPreviewResource: URI = joinPath(this.syncFolder, PREVIEW_DIR_NAME, 'settings.json');
|
||||
protected readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME });
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@@ -67,15 +62,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
}
|
||||
}
|
||||
|
||||
private setConflicts(conflicts: IConflictSetting[]): void {
|
||||
if (!arrays.equals(this.conflicts, conflicts,
|
||||
(a, b) => a.key === b.key && objects.equals(a.localValue, b.localValue) && objects.equals(a.remoteValue, b.remoteValue))
|
||||
) {
|
||||
this._conflicts = conflicts;
|
||||
this._onDidChangeConflicts.fire(conflicts);
|
||||
}
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.isEnabled()) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling settings as it is disabled.`);
|
||||
@@ -187,8 +173,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
return false;
|
||||
}
|
||||
|
||||
async getRemoteContentFromPreview(): Promise<string | null> {
|
||||
let content = await super.getRemoteContentFromPreview();
|
||||
async getConflictContent(conflictResource: URI): Promise<string | null> {
|
||||
let content = await super.getConflictContent(conflictResource);
|
||||
if (content !== null) {
|
||||
const settingsSyncContent = this.parseSettingsSyncContent(content);
|
||||
content = settingsSyncContent ? settingsSyncContent.settings : null;
|
||||
@@ -232,8 +218,10 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
return null;
|
||||
}
|
||||
|
||||
async accept(content: string): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
async acceptConflict(conflict: URI, content: string): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts
|
||||
&& (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict))
|
||||
) {
|
||||
const preview = await this.syncPreviewResultPromise!;
|
||||
this.cancel();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
@@ -289,7 +277,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
|
||||
if (hasLocalChanged) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
|
||||
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(content)));
|
||||
if (fileContent) {
|
||||
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
|
||||
}
|
||||
await this.updateLocalFileContent(content, fileContent);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
|
||||
}
|
||||
@@ -306,7 +296,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(this.conflictsPreviewResource);
|
||||
await this.fileService.del(this.localPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`);
|
||||
@@ -338,7 +328,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
let conflictSettings: IConflictSetting[] = [];
|
||||
|
||||
if (remoteSettingsSyncContent) {
|
||||
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
|
||||
@@ -350,7 +339,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
hasLocalChanged = result.localContent !== null;
|
||||
hasRemoteChanged = result.remoteContent !== null;
|
||||
hasConflicts = result.hasConflicts;
|
||||
conflictSettings = result.conflictsSettings;
|
||||
}
|
||||
|
||||
// First time syncing to remote
|
||||
@@ -364,10 +352,11 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
// Remove the ignored settings from the preview.
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions);
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent));
|
||||
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
|
||||
}
|
||||
|
||||
this.setConflicts(conflictSettings);
|
||||
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
|
||||
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isEqual, joinPath, dirname, basename } from 'vs/base/common/resources';
|
||||
import { joinPath, dirname, basename, isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
@@ -242,11 +242,15 @@ export const enum SyncStatus {
|
||||
HasConflicts = 'hasConflicts',
|
||||
}
|
||||
|
||||
export type Conflict = { remote: URI, local: URI };
|
||||
|
||||
export interface IUserDataSynchroniser {
|
||||
|
||||
readonly resource: SyncResource;
|
||||
readonly status: SyncStatus;
|
||||
readonly onDidChangeStatus: Event<SyncStatus>;
|
||||
readonly conflicts: Conflict[];
|
||||
readonly onDidChangeConflicts: Event<Conflict[]>;
|
||||
readonly onDidChangeLocal: Event<void>;
|
||||
|
||||
pull(): Promise<void>;
|
||||
@@ -258,10 +262,11 @@ export interface IUserDataSynchroniser {
|
||||
hasLocalData(): Promise<boolean>;
|
||||
resetLocal(): Promise<void>;
|
||||
|
||||
getRemoteContentFromPreview(): Promise<string | null>;
|
||||
getConflictContent(conflictResource: URI): Promise<string | null>;
|
||||
acceptConflict(conflictResource: URI, content: string): Promise<void>;
|
||||
|
||||
getRemoteContent(ref?: string, fragment?: string): Promise<string | null>;
|
||||
getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null>;
|
||||
accept(content: string): Promise<void>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@@ -282,6 +287,8 @@ export interface IUserDataSyncEnablementService {
|
||||
setResourceEnablement(resource: SyncResource, enabled: boolean): void;
|
||||
}
|
||||
|
||||
export type SyncResourceConflicts = { syncResource: SyncResource, conflicts: Conflict[] };
|
||||
|
||||
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
|
||||
export interface IUserDataSyncService {
|
||||
_serviceBrand: any;
|
||||
@@ -289,8 +296,8 @@ export interface IUserDataSyncService {
|
||||
readonly status: SyncStatus;
|
||||
readonly onDidChangeStatus: Event<SyncStatus>;
|
||||
|
||||
readonly conflictsSources: SyncResource[];
|
||||
readonly onDidChangeConflicts: Event<SyncResource[]>;
|
||||
readonly conflicts: SyncResourceConflicts[];
|
||||
readonly onDidChangeConflicts: Event<SyncResourceConflicts[]>;
|
||||
|
||||
readonly onDidChangeLocal: Event<SyncResource>;
|
||||
readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]>;
|
||||
@@ -306,7 +313,7 @@ export interface IUserDataSyncService {
|
||||
|
||||
isFirstTimeSyncWithMerge(): Promise<boolean>;
|
||||
resolveContent(resource: URI): Promise<string | null>;
|
||||
accept(source: SyncResource, content: string): Promise<void>;
|
||||
acceptConflict(conflictResource: URI, content: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
|
||||
@@ -335,36 +342,41 @@ export interface IConflictSetting {
|
||||
|
||||
//#endregion
|
||||
|
||||
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
|
||||
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
|
||||
export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false);
|
||||
|
||||
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
|
||||
export const PREVIEW_QUERY = 'preview=true';
|
||||
export function toRemoteSyncResource(resource: SyncResource, ref?: string): URI {
|
||||
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote', path: `/${resource}/${ref ? ref : 'latest'}` });
|
||||
export function toRemoteBackupSyncResource(resource: SyncResource, ref?: string): URI {
|
||||
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote-backup', path: `/${resource}/${ref ? ref : 'latest'}` });
|
||||
}
|
||||
export function toLocalBackupSyncResource(resource: SyncResource, ref?: string): URI {
|
||||
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${resource}/${ref ? ref : 'latest'}` });
|
||||
}
|
||||
|
||||
export function resolveSyncResource(resource: URI): { remote: boolean, resource: SyncResource, ref?: string } | null {
|
||||
if (resource.scheme === USER_DATA_SYNC_SCHEME) {
|
||||
const remote = resource.authority === 'remote';
|
||||
export function resolveBackupSyncResource(resource: URI): { remote: boolean, resource: SyncResource, path: string } | null {
|
||||
if (resource.scheme === USER_DATA_SYNC_SCHEME
|
||||
&& resource.authority === 'remote-backup' || resource.authority === 'local-backup') {
|
||||
const resourceKey: SyncResource = basename(dirname(resource)) as SyncResource;
|
||||
const ref = basename(resource);
|
||||
if (resourceKey && ref) {
|
||||
return { remote, resource: resourceKey, ref: ref !== 'latest' ? ref : undefined };
|
||||
const path = resource.path.substring(resourceKey.length + 1);
|
||||
if (resourceKey && path) {
|
||||
const remote = resource.authority === 'remote-backup';
|
||||
return { remote, resource: resourceKey, path };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getSyncSourceFromPreviewResource(uri: URI, environmentService: IEnvironmentService): SyncResource | undefined {
|
||||
if (isEqual(uri, environmentService.settingsSyncPreviewResource)) {
|
||||
return SyncResource.Settings;
|
||||
export const PREVIEW_DIR_NAME = 'preview';
|
||||
export function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined {
|
||||
if (localPreview.scheme === USER_DATA_SYNC_SCHEME) {
|
||||
return undefined;
|
||||
}
|
||||
if (isEqual(uri, environmentService.keybindingsSyncPreviewResource)) {
|
||||
return SyncResource.Keybindings;
|
||||
}
|
||||
return undefined;
|
||||
localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme });
|
||||
return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0];
|
||||
}
|
||||
export function getSyncResourceFromRemotePreview(remotePreview: URI, environmentService: IEnvironmentService): SyncResource | undefined {
|
||||
if (remotePreview.scheme !== USER_DATA_SYNC_SCHEME) {
|
||||
return undefined;
|
||||
}
|
||||
remotePreview = remotePreview.with({ scheme: environmentService.userDataSyncHome.scheme });
|
||||
return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(remotePreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0];
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflictsSources, this.service.lastSyncTime]);
|
||||
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]);
|
||||
case 'sync': return this.service.sync();
|
||||
case 'accept': return this.service.accept(args[0], args[1]);
|
||||
case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]);
|
||||
case 'pull': return this.service.pull();
|
||||
case 'stop': this.service.stop(); return Promise.resolve();
|
||||
case 'reset': return this.service.reset();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, resolveSyncResource, PREVIEW_QUERY } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, resolveBackupSyncResource, SyncResourceConflicts } 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';
|
||||
@@ -17,6 +17,7 @@ import { localize } from 'vs/nls';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
type SyncErrorClassification = {
|
||||
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
@@ -38,10 +39,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
readonly onDidChangeLocal: Event<SyncResource>;
|
||||
|
||||
private _conflictsSources: SyncResource[] = [];
|
||||
get conflictsSources(): SyncResource[] { return this._conflictsSources; }
|
||||
private _onDidChangeConflicts: Emitter<SyncResource[]> = this._register(new Emitter<SyncResource[]>());
|
||||
readonly onDidChangeConflicts: Event<SyncResource[]> = this._onDidChangeConflicts.event;
|
||||
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 _syncErrors: [SyncResource, UserDataSyncError][] = [];
|
||||
private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>());
|
||||
@@ -62,7 +63,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IStorageService private readonly storageService: IStorageService
|
||||
) {
|
||||
super();
|
||||
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
|
||||
@@ -74,6 +75,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
if (this.userDataSyncStoreService.userDataSyncStore) {
|
||||
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
|
||||
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeConflicts, () => undefined)))(() => this.updateConflicts()));
|
||||
}
|
||||
|
||||
this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL, undefined);
|
||||
@@ -173,23 +175,32 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
}
|
||||
|
||||
async accept(source: SyncResource, content: string): Promise<void> {
|
||||
async acceptConflict(conflict: URI, content: string): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
const synchroniser = this.getSynchroniser(source);
|
||||
await synchroniser.accept(content);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async resolveContent(resource: URI): Promise<string | null> {
|
||||
const result = resolveSyncResource(resource);
|
||||
const result = resolveBackupSyncResource(resource);
|
||||
if (result) {
|
||||
const synchronizer = this.synchronisers.filter(s => s.resource === result.resource)[0];
|
||||
if (synchronizer) {
|
||||
if (PREVIEW_QUERY === resource.query) {
|
||||
return result.remote ? synchronizer.getRemoteContentFromPreview() : null;
|
||||
}
|
||||
return result.remote ? synchronizer.getRemoteContent(result.ref, resource.fragment) : synchronizer.getLocalBackupContent(result.ref, resource.fragment);
|
||||
const ref = result.path !== 'latest' ? result.path : undefined;
|
||||
return result.remote ? synchronizer.getRemoteContent(ref, resource.fragment) : synchronizer.getLocalBackupContent(ref, resource.fragment);
|
||||
}
|
||||
}
|
||||
|
||||
for (const synchronizer of this.synchronisers) {
|
||||
const content = await synchronizer.getConflictContent(resource);
|
||||
if (content !== null) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -263,15 +274,19 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
|
||||
private updateStatus(): void {
|
||||
const conflictsSources = this.computeConflictsSources();
|
||||
if (!equals(this._conflictsSources, conflictsSources)) {
|
||||
this._conflictsSources = this.computeConflictsSources();
|
||||
this._onDidChangeConflicts.fire(conflictsSources);
|
||||
}
|
||||
this.updateConflicts();
|
||||
const status = this.computeStatus();
|
||||
this.setStatus(status);
|
||||
}
|
||||
|
||||
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)))) {
|
||||
this._conflicts = this.computeConflicts();
|
||||
this._onDidChangeConflicts.fire(conflicts);
|
||||
}
|
||||
}
|
||||
|
||||
private computeStatus(): SyncStatus {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
return SyncStatus.Uninitialized;
|
||||
@@ -305,8 +320,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.logService.error(`${source}: ${toErrorMessage(e)}`);
|
||||
}
|
||||
|
||||
private computeConflictsSources(): SyncResource[] {
|
||||
return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts).map(s => s.resource);
|
||||
private computeConflicts(): SyncResourceConflicts[] {
|
||||
return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)
|
||||
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts }));
|
||||
}
|
||||
|
||||
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
|
||||
|
||||
@@ -52,9 +52,7 @@ export class UserDataSyncClient extends Disposable {
|
||||
const environmentService = this.instantiationService.stub(IEnvironmentService, <Partial<IEnvironmentService>>{
|
||||
userDataSyncHome,
|
||||
settingsResource: joinPath(userDataDirectory, 'settings.json'),
|
||||
settingsSyncPreviewResource: joinPath(userDataSyncHome, 'settings.json'),
|
||||
keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'),
|
||||
keybindingsSyncPreviewResource: joinPath(userDataSyncHome, 'keybindings.json'),
|
||||
argvResource: joinPath(userDataDirectory, 'argv.json'),
|
||||
args: {}
|
||||
});
|
||||
|
||||
@@ -480,7 +480,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
assert.deepEqual(testObject.conflictsSources, [SyncResource.Settings]);
|
||||
assert.deepEqual(testObject.conflicts.map(({ syncResource }) => syncResource), [SyncResource.Settings]);
|
||||
});
|
||||
|
||||
test('test sync will sync other non conflicted areas', async () => {
|
||||
@@ -549,7 +549,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.stop();
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflictsSources, []);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user