Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support syncing profiles manifest #160907

Merged
merged 1 commit into from Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/vs/base/common/product.ts
Expand Up @@ -156,6 +156,9 @@ export interface IProductConfiguration {
readonly 'editSessions.store'?: Omit<ConfigurationSyncStore, 'insidersUrl' | 'stableUrl'>;

readonly darwinUniversalAssetId?: string;

// experimental
readonly enableSyncingProfiles?: boolean;
}

export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean; whenNotInstalled?: string[] };
Expand Down
10 changes: 10 additions & 0 deletions src/vs/platform/userDataProfile/common/userDataProfile.ts
Expand Up @@ -539,3 +539,13 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf
protected getStoredProfileAssociations(): StoredProfileAssociations { return {}; }
protected saveStoredProfileAssociations(storedProfileAssociations: StoredProfileAssociations): void { throw new Error('not implemented'); }
}

export class InMemoryUserDataProfilesService extends UserDataProfilesService {
private storedProfiles: StoredUserDataProfile[] = [];
protected override getStoredProfiles(): StoredUserDataProfile[] { return this.storedProfiles; }
protected override saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void { this.storedProfiles = storedProfiles; }

private storedProfileAssociations: StoredProfileAssociations = {};
protected override getStoredProfileAssociations(): StoredProfileAssociations { return this.storedProfileAssociations; }
protected override saveStoredProfileAssociations(storedProfileAssociations: StoredProfileAssociations): void { this.storedProfileAssociations = storedProfileAssociations; }
}
Expand Up @@ -13,7 +13,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { AbstractNativeEnvironmentService } from 'vs/platform/environment/common/environmentService';
import product from 'vs/platform/product/common/product';
import { StoredProfileAssociations, StoredUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { InMemoryUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService';

const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' });
Expand All @@ -25,17 +25,6 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService {
override get userRoamingDataHome() { return this._appSettingsHome.with({ scheme: Schemas.vscodeUserData }); }
}

class TestUserDataProfilesService extends UserDataProfilesService {

private storedProfiles: StoredUserDataProfile[] = [];
protected override getStoredProfiles(): StoredUserDataProfile[] { return this.storedProfiles; }
protected override saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void { this.storedProfiles = storedProfiles; }

private storedProfileAssociations: StoredProfileAssociations = {};
protected override getStoredProfileAssociations(): StoredProfileAssociations { return this.storedProfileAssociations; }
protected override saveStoredProfileAssociations(storedProfileAssociations: StoredProfileAssociations): void { this.storedProfileAssociations = storedProfileAssociations; }
}

suite('UserDataProfileService (Common)', () => {

const disposables = new DisposableStore();
Expand All @@ -50,7 +39,7 @@ suite('UserDataProfileService (Common)', () => {
disposables.add(fileService.registerProvider(Schemas.vscodeUserData, fileSystemProvider));

environmentService = new TestEnvironmentService(joinPath(ROOT, 'User'));
testObject = new TestUserDataProfilesService(environmentService, fileService, new UriIdentityService(fileService), logService);
testObject = new InMemoryUserDataProfilesService(environmentService, fileService, new UriIdentityService(fileService), logService);
testObject.setEnablement(true);
});

Expand Down
Expand Up @@ -121,7 +121,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
private hasSyncResourceStateVersionChanged: boolean = false;
protected readonly syncResourceLogLabel: string;

private syncHeaders: IHeaders = {};
protected syncHeaders: IHeaders = {};

constructor(
readonly resource: SyncResource,
Expand Down
135 changes: 135 additions & 0 deletions src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts
@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile';
import { ISyncUserDataProfile } from 'vs/platform/userDataSync/common/userDataSync';

interface IRelaxedMergeResult {
local: { added: ISyncUserDataProfile[]; removed: IUserDataProfile[]; updated: ISyncUserDataProfile[] };
remote: { added: IUserDataProfile[]; removed: ISyncUserDataProfile[]; updated: IUserDataProfile[] } | null;
}

export type IMergeResult = Required<IRelaxedMergeResult>;

interface IUserDataProfileInfo {
readonly id: string;
readonly name: string;
}

export function merge(local: IUserDataProfile[], remote: ISyncUserDataProfile[] | null, lastSync: ISyncUserDataProfile[] | null, ignored: string[]): IMergeResult {
const result: IRelaxedMergeResult = {
local: {
added: [],
removed: [],
updated: [],
}, remote: {
added: [],
removed: [],
updated: [],
}
};

if (!remote) {
const added = local.filter(({ id }) => !ignored.includes(id));
if (added.length) {
result.remote!.added = added;
} else {
result.remote = null;
}
return result;
}

const localToRemote = compare(local, remote, ignored);
if (localToRemote.added.length > 0 || localToRemote.removed.length > 0 || localToRemote.updated.length > 0) {

const baseToLocal = compare(lastSync, local, ignored);
const baseToRemote = compare(lastSync, remote, ignored);

// Remotely removed profiles
for (const id of baseToRemote.removed) {
const e = local.find(profile => profile.id === id);
if (e) {
result.local.removed.push(e);
}
}

// Remotely added profiles
for (const id of baseToRemote.added) {
const remoteProfile = remote.find(profile => profile.id === id)!;
// Got added in local
if (baseToLocal.added.includes(id)) {
// Is different from local to remote
if (localToRemote.updated.includes(id)) {
// Remote wins always
result.local.updated.push(remoteProfile);
}
} else {
result.local.added.push(remoteProfile);
}
}

// Remotely updated profiles
for (const id of baseToRemote.updated) {
// Remote wins always
result.local.updated.push(remote.find(profile => profile.id === id)!);
}

// Locally added profiles
for (const id of baseToLocal.added) {
// Not there in remote
if (!baseToRemote.added.includes(id)) {
result.remote!.added.push(local.find(profile => profile.id === id)!);
}
}

// Locally updated profiles
for (const id of baseToLocal.updated) {
// If removed in remote
if (baseToRemote.removed.includes(id)) {
continue;
}

// If not updated in remote
if (!baseToRemote.updated.includes(id)) {
result.remote!.updated.push(local.find(profile => profile.id === id)!);
}
}

// Locally removed profiles
for (const id of baseToLocal.removed) {
result.remote!.removed.push(remote.find(profile => profile.id === id)!);
}
}

if (result.remote!.added.length === 0 && result.remote!.removed.length === 0 && result.remote!.updated.length === 0) {
result.remote = null;
}

return result;
}

function compare(from: IUserDataProfileInfo[] | null, to: IUserDataProfileInfo[], ignoredProfiles: string[]): { added: string[]; removed: string[]; updated: string[] } {
from = from ? from.filter(({ id }) => !ignoredProfiles.includes(id)) : [];
to = to.filter(({ id }) => !ignoredProfiles.includes(id));
const fromKeys = from.map(({ id }) => id);
const toKeys = to.map(({ id }) => id);
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1);
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1);
const updated: string[] = [];

for (const { id, name } of from) {
if (removed.includes(id)) {
continue;
}
const toProfile = to.find(p => p.id === id);
if (!toProfile
|| toProfile.name !== name
) {
updated.push(id);
}
}

return { added, removed, updated };
}