提交 9e457a6c 编写于 作者: S Sandeep Somavarapu

#100346 introduce merge state on a resource. Do not apply automatically.

上级 80a33109
......@@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri';
import {
SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview,
IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change
IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change, MergeState
} from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
......@@ -58,10 +58,10 @@ export interface IMergableResourcePreview extends IBaseResourcePreview {
readonly localContent: string | null;
readonly previewContent: string | null;
readonly hasConflicts: boolean;
merged: boolean;
mergeState: MergeState;
}
export type IResourcePreview = Omit<IMergableResourcePreview, 'merged'>;
export type IResourcePreview = Omit<IMergableResourcePreview, 'mergeState'>;
export interface ISyncResourcePreview extends IBaseSyncResourcePreview {
readonly remoteUserData: IRemoteUserData;
......@@ -217,6 +217,19 @@ export abstract class AbstractSynchroniser extends Disposable {
return this._sync(manifest, false, headers);
}
async apply(force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
try {
this.syncHeaders = { ...headers };
const status = await this.doApply(force);
this.setStatus(status);
return this.syncPreviewPromise;
} finally {
this.syncHeaders = {};
}
}
private async _sync(manifest: IUserDataManifest | null, apply: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null> {
try {
this.syncHeaders = { ...headers };
......@@ -350,14 +363,18 @@ export abstract class AbstractSynchroniser extends Disposable {
this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, apply, token));
}
const preview = await this.syncPreviewPromise;
this.updateConflicts(preview.resourcePreviews);
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
return SyncStatus.HasConflicts;
}
if (apply) {
const preview = await this.syncPreviewPromise;
const newConflicts = preview.resourcePreviews.filter(({ hasConflicts }) => hasConflicts);
return await this.updateConflictsAndApply(newConflicts, false);
} else {
return SyncStatus.Syncing;
return await this.doApply(false);
}
return SyncStatus.Syncing;
} catch (error) {
// reset preview on error
......@@ -367,76 +384,63 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
async acceptPreviewContent(resource: URI, content: string, force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
async accept(resource: URI, content: string): 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 = {};
let preview = await this.syncPreviewPromise;
this.syncPreviewPromise = createCancelablePromise(token => this.updateSyncResourcePreviewContent(preview, resource, content, token));
preview = await this.syncPreviewPromise;
this.updateConflicts(preview.resourcePreviews);
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
this.setStatus(SyncStatus.HasConflicts);
} else {
this.setStatus(SyncStatus.Syncing);
}
return preview;
}
async merge(resource: URI, force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
async merge(resource: URI): 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;
}
/* mark merged */
resourcePreview.merged = true;
/* 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);
}
}
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;
}
const status = await this.updateConflictsAndApply(newConflicts, force);
this.setStatus(status);
return this.syncPreviewPromise;
resourcePreview.mergeState = resourcePreview.hasConflicts ? MergeState.Conflict : MergeState.Accepted;
} finally {
this.syncHeaders = {};
this.updateConflicts(preview.resourcePreviews);
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
this.setStatus(SyncStatus.HasConflicts);
} else {
this.setStatus(SyncStatus.Syncing);
}
return preview;
}
private async updateConflictsAndApply(conflicts: IMergableResourcePreview[], force: boolean): Promise<SyncStatus> {
private async doApply(force: boolean): Promise<SyncStatus> {
if (!this.syncPreviewPromise) {
return SyncStatus.Idle;
}
const preview = await this.syncPreviewPromise;
// update conflicts
this.updateConflicts(conflicts);
if (this._conflicts.length) {
// check for conflicts
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
return SyncStatus.HasConflicts;
}
// check if all are merged
if (preview.resourcePreviews.some(r => !r.merged)) {
// check if all are accepted
if (preview.resourcePreviews.some(({ mergeState }) => mergeState !== MergeState.Accepted)) {
return SyncStatus.Syncing;
}
......@@ -459,7 +463,7 @@ export abstract class AbstractSynchroniser extends Disposable {
if (index !== -1) {
const resourcePreviews = [...preview.resourcePreviews];
const resourcePreview = await this.updateResourcePreviewContent(resourcePreviews[index], resource, previewContent, token);
resourcePreviews[index] = { ...resourcePreview, merged: resourcePreviews[index].merged };
resourcePreviews[index] = { ...resourcePreview, mergeState: MergeState.Accepted };
preview = {
...preview,
resourcePreviews
......@@ -484,7 +488,8 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (error) { /* Ignore */ }
}
private updateConflicts(conflicts: IMergableResourcePreview[]): void {
private updateConflicts(previews: IMergableResourcePreview[]): void {
const conflicts = previews.filter(p => p.mergeState === MergeState.Conflict);
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) {
this._conflicts = conflicts;
this._onDidChangeConflicts.fire(conflicts);
......@@ -562,7 +567,7 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (e) { /* ignore */ }
}
private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, merge: boolean, token: CancellationToken): Promise<ISyncResourcePreview> {
private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, token: CancellationToken): Promise<ISyncResourcePreview> {
const machineId = await this.currentMachineIdPromise;
const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
......@@ -571,9 +576,12 @@ export abstract class AbstractSynchroniser extends Disposable {
const resourcePreviews = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
// Mark merge
const mergableResourcePreviews = resourcePreviews.map(r => ({
const mergableResourcePreviews = resourcePreviews.map<IMergableResourcePreview>(r => ({
...r,
merged: merge || (r.localChange === Change.None && r.remoteChange === Change.None) /* Mark previews with no changes as merged */
mergeState: r.localChange === Change.None && r.remoteChange === Change.None ? MergeState.Accepted /* Mark previews with no changes as merged */
: apply ? (r.hasConflicts ? MergeState.Conflict : MergeState.Accepted)
: MergeState.Preview
}));
return { remoteUserData, lastSyncUserData, resourcePreviews: mergableResourcePreviews, isLastSyncFromCurrentMachine };
......
......@@ -317,13 +317,19 @@ export const enum Change {
Deleted,
}
export const enum MergeState {
Preview,
Conflict,
Accepted,
}
export interface IResourcePreview {
readonly remoteResource: URI;
readonly localResource: URI;
readonly previewResource: URI;
readonly localChange: Change;
readonly remoteChange: Change;
readonly merged: boolean;
readonly mergeState: MergeState;
}
export interface ISyncResourcePreview {
......@@ -345,18 +351,19 @@ export interface IUserDataSynchroniser {
pull(): Promise<void>;
push(): 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>;
preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise<ISyncResourcePreview | null>;
accept(resource: URI, content: string): Promise<ISyncResourcePreview | null>;
merge(resource: URI): Promise<ISyncResourcePreview | null>;
apply(force: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null>;
hasPreviouslySynced(): Promise<boolean>;
hasLocalData(): Promise<boolean>;
resetLocal(): Promise<void>;
resolveContent(resource: URI): Promise<string | null>;
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[]>;
getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
......@@ -388,7 +395,8 @@ export interface IManualSyncTask extends IDisposable {
readonly onSynchronizeResources: Event<[SyncResource, URI[]][]>;
preview(): Promise<[SyncResource, ISyncResourcePreview][]>;
accept(uri: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]>;
merge(uri?: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
merge(uri: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
apply(): Promise<[SyncResource, ISyncResourcePreview][]>;
pull(): Promise<void>;
push(): Promise<void>;
stop(): Promise<void>;
......@@ -422,7 +430,7 @@ export interface IUserDataSyncService {
hasLocalData(): Promise<boolean>;
hasPreviouslySynced(): Promise<boolean>;
resolveContent(resource: URI): Promise<string | null>;
acceptPreviewContent(resource: SyncResource, conflictResource: URI, content: string): Promise<void>;
accept(resource: SyncResource, conflictResource: URI, content: string, apply: boolean): Promise<void>;
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
......
......@@ -53,7 +53,7 @@ export class UserDataSyncChannel implements IServerChannel {
case 'resetLocal': return this.service.resetLocal();
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
case 'hasLocalData': return this.service.hasLocalData();
case 'acceptPreviewContent': return this.service.acceptPreviewContent(args[0], URI.revive(args[1]), args[2]);
case 'accept': return this.service.accept(args[0], URI.revive(args[1]), args[2], args[3]);
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]);
......@@ -87,6 +87,7 @@ class ManualSyncTaskChannel implements IServerChannel {
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 'apply': return this.manualSyncTask.apply();
case 'pull': return this.manualSyncTask.pull();
case 'push': return this.manualSyncTask.push();
case 'stop': return this.manualSyncTask.stop();
......
......@@ -264,10 +264,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
async acceptPreviewContent(syncResource: SyncResource, resource: URI, content: string, executionId: string = generateUuid()): Promise<void> {
async accept(syncResource: SyncResource, resource: URI, content: string, apply: boolean): Promise<void> {
await this.checkEnablement();
const synchroniser = this.getSynchroniser(syncResource);
await synchroniser.acceptPreviewContent(resource, content, false, createSyncHeaders(executionId));
await synchroniser.accept(resource, content);
if (apply) {
await synchroniser.apply(false, createSyncHeaders(generateUuid()));
}
}
async resolveContent(resource: URI): Promise<string | null> {
......@@ -461,18 +464,14 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
}
async accept(resource: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]> {
return this.mergeOrAccept(resource, (sychronizer, force) => sychronizer.acceptPreviewContent(resource, content, force, this.syncHeaders));
return this.mergeOrAccept(resource, sychronizer => sychronizer.accept(resource, content));
}
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();
}
async merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
return this.mergeOrAccept(resource, sychronizer => sychronizer.merge(resource));
}
private async mergeOrAccept(resource: URI, mergeOrAccept: (synchroniser: IUserDataSynchroniser, force: boolean) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
private async mergeOrAccept(resource: URI, mergeOrAccept: (synchroniser: IUserDataSynchroniser) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
if (!this.previews) {
throw new Error('You need to create preview before merging or accepting');
}
......@@ -500,9 +499,7 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
}
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);
const preview = await mergeOrAccept(synchroniser);
preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1);
const i = this.synchronizingResources.findIndex(s => s[0] === syncResource);
......@@ -515,24 +512,21 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
return this.previews;
}
private async mergeAll(): Promise<[SyncResource, ISyncResourcePreview][]> {
async apply(): Promise<[SyncResource, ISyncResourcePreview][]> {
if (!this.previews) {
throw new Error('You need to create preview before merging');
throw new Error('You need to create preview before applying');
}
if (this.synchronizingResources.length) {
throw new Error('Cannot merge while synchronizing resources');
throw new Error('Cannot pull 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]);
const newPreview = await synchroniser.apply(false, this.syncHeaders);
if (newPreview) {
previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview));
}
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
......@@ -554,8 +548,9 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
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);
await synchroniser.accept(resourcePreview.remoteResource, content);
}
await synchroniser.apply(true, this.syncHeaders);
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
......@@ -575,8 +570,9 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
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);
await synchroniser.accept(resourcePreview.localResource, content);
}
await synchroniser.apply(true, this.syncHeaders);
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
this._onSynchronizeResources.fire(this.synchronizingResources);
}
......@@ -643,6 +639,6 @@ function toStrictResourcePreview(resourcePreview: IResourcePreview): IResourcePr
remoteResource: resourcePreview.remoteResource,
localChange: resourcePreview.localChange,
remoteChange: resourcePreview.remoteChange,
merged: resourcePreview.merged,
mergeState: resourcePreview.mergeState,
};
}
......@@ -287,7 +287,8 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
const conflicts = testObject.conflicts;
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet1, false);
await testObject.accept(conflicts[0].previewResource, htmlSnippet1);
await testObject.apply(false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
......@@ -327,7 +328,7 @@ suite('SnippetsSync', () => {
await testObject.sync(await testClient.manifest());
let conflicts = testObject.conflicts;
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2, false);
await testObject.accept(conflicts[0].previewResource, htmlSnippet2);
conflicts = testObject.conflicts;
assert.equal(testObject.status, SyncStatus.HasConflicts);
......@@ -346,8 +347,9 @@ suite('SnippetsSync', () => {
await testObject.sync(await testClient.manifest());
const conflicts = testObject.conflicts;
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2, false);
await testObject.acceptPreviewContent(conflicts[1].previewResource, tsSnippet1, false);
await testObject.accept(conflicts[0].previewResource, htmlSnippet2);
await testObject.accept(conflicts[1].previewResource, tsSnippet1);
await testObject.apply(false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
......@@ -461,7 +463,8 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet3, testClient);
await testObject.sync(await testClient.manifest());
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, htmlSnippet2, false);
await testObject.accept(testObject.conflicts[0].previewResource, htmlSnippet2);
await testObject.apply(false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
......@@ -565,7 +568,8 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, htmlSnippet3, false);
await testObject.accept(testObject.conflicts[0].previewResource, htmlSnippet3);
await testObject.apply(false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
......@@ -592,7 +596,8 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync(await testClient.manifest());
await testObject.acceptPreviewContent(testObject.conflicts[0].previewResource, '', false);
await testObject.accept(testObject.conflicts[0].previewResource, '');
await testObject.apply(false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
......@@ -689,7 +694,8 @@ suite('SnippetsSync', () => {
await testObject.sync(await testClient.manifest());
let conflicts = testObject.conflicts;
await testObject.acceptPreviewContent(conflicts[0].previewResource, htmlSnippet2, false);
await testObject.accept(conflicts[0].previewResource, htmlSnippet2);
await testObject.apply(false);
const fileService = testClient.instantiationService.get(IFileService);
assert.ok(!await fileService.exists(dirname(conflicts[0].previewResource)));
......@@ -710,7 +716,7 @@ suite('SnippetsSync', () => {
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource, false);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
......@@ -736,8 +742,36 @@ suite('SnippetsSync', () => {
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource, false);
preview = await testObject.merge(preview!.resourcePreviews[1].localResource, false);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
preview = await testObject.merge(preview!.resourcePreviews[1].localResource);
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 and applied', 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);
preview = await testObject.merge(preview!.resourcePreviews[1].localResource);
preview = await testObject.apply(false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
......@@ -762,7 +796,37 @@ suite('SnippetsSync', () => {
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource, false);
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
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, []);
});
test('merge when there are multiple snippets and one snippet has no changes and one snippet is merged and applied', 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);
preview = await testObject.apply(false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
......@@ -788,7 +852,7 @@ suite('SnippetsSync', () => {
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
assert.equal(testObject.status, SyncStatus.HasConflicts);
assertPreviews(preview!.resourcePreviews,
......@@ -821,8 +885,8 @@ suite('SnippetsSync', () => {
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
preview = await testObject.merge(preview!.resourcePreviews[1].previewResource, false);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.merge(preview!.resourcePreviews[1].previewResource);
assert.equal(testObject.status, SyncStatus.HasConflicts);
assertPreviews(preview!.resourcePreviews,
......@@ -856,7 +920,7 @@ suite('SnippetsSync', () => {
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, htmlSnippet2, false);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2);
assert.equal(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews,
......@@ -886,8 +950,40 @@ suite('SnippetsSync', () => {
]);
assert.deepEqual(testObject.conflicts, []);
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[0].previewResource, htmlSnippet2, false);
preview = await testObject.acceptPreviewContent(preview!.resourcePreviews[1].previewResource, tsSnippet2, false);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2);
preview = await testObject.accept(preview!.resourcePreviews[1].previewResource, tsSnippet2);
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 and applied', 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.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2);
preview = await testObject.accept(preview!.resourcePreviews[1].previewResource, tsSnippet2);
preview = await testObject.apply(false);
assert.equal(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
......
......@@ -276,26 +276,54 @@ suite('TestSynchronizer', () => {
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to idle after merging if there are no conflicts', async () => {
test('preview: status is syncing 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);
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to idle after merging and applying 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);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is syncing after accepting when 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.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
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);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
......@@ -320,34 +348,63 @@ suite('TestSynchronizer', () => {
testObject.syncBarrier.open();
const preview = await testObject.preview(await client.manifest());
await testObject.merge(preview!.resourcePreviews[0].previewResource, false);
await testObject.merge(preview!.resourcePreviews[0].previewResource);
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assertPreviews(preview!.resourcePreviews, [resource]);
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].previewResource]);
});
test('preview: status is syncing 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);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
assert.deepEqual(testObject.conflicts, []);
});
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);
await testObject.merge(preview!.resourcePreviews[0].previewResource);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
assertConflicts(testObject.conflicts, []);
});
test('preview: status is set to syncing 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.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
assertPreviews(preview!.resourcePreviews, [resource]);
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);
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].previewContent!);
preview = await testObject.apply(false);
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.equal(preview, null);
......
......@@ -240,7 +240,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
for (const conflict of conflicts) {
const modelRef = await this.textModelResolverService.createModelReference(conflict.remoteResource);
try {
await this.userDataSyncService.acceptPreviewContent(syncResource, conflict.remoteResource, modelRef.object.textEditorModel.getValue());
await this.userDataSyncService.accept(syncResource, conflict.remoteResource, modelRef.object.textEditorModel.getValue(), true);
} finally {
modelRef.dispose();
}
......@@ -255,7 +255,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
for (const conflict of conflicts) {
const modelRef = await this.textModelResolverService.createModelReference(conflict.previewResource);
try {
await this.userDataSyncService.acceptPreviewContent(syncResource, conflict.previewResource, modelRef.object.textEditorModel.getValue());
await this.userDataSyncService.accept(syncResource, conflict.previewResource, modelRef.object.textEditorModel.getValue(), true);
} finally {
modelRef.dispose();
}
......@@ -1195,7 +1195,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
});
if (result.confirmed) {
try {
await this.userDataSyncService.acceptPreviewContent(syncResource, model.uri, model.getValue());
await this.userDataSyncService.accept(syncResource, model.uri, model.getValue(), true);
} catch (e) {
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) {
const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(syncResourceCoflicts => syncResourceCoflicts[0] === syncResource)[0];
......
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask, MergeState } from 'vs/platform/userDataSync/common/userDataSync';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResourceGroup, CONTEXT_ENABLE_MANUAL_SYNC_VIEW, MANUAL_SYNC_VIEW_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID } from 'vs/workbench/services/userDataSync/common/userDataSync';
......@@ -270,7 +270,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
synchronizingResources.length ? progress.report({ message: localize('syncing resource', "Syncing {0}...", getSyncAreaLabel(synchronizingResources[0][0])) }) : undefined);
try {
switch (action) {
case 'merge': return await manualSyncTask.merge();
case 'merge': return await manualSyncTask.apply();
case 'pull': return await manualSyncTask.pull();
case 'push': return await manualSyncTask.push();
case 'manual': return;
......@@ -343,7 +343,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
}
/* Merge to sync globalState changes */
await task.merge();
await task.apply();
this.userDataSyncPreview.unsetManualSyncPreview();
......@@ -599,11 +599,11 @@ class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview {
const syncPreview = await this.manualSync.task.accept(resource, content);
this.updatePreview(syncPreview);
} else {
await this.userDataSyncService.acceptPreviewContent(syncResource, resource, content);
await this.userDataSyncService.accept(syncResource, resource, content, false);
}
}
async merge(resource?: URI): Promise<void> {
async merge(resource: URI): Promise<void> {
if (!this.manualSync) {
throw new Error('Can merge only while syncing manually');
}
......@@ -650,10 +650,9 @@ class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview {
.map(([syncResource, syncResourcePreview]) =>
([
syncResource,
/* remove merged previews and conflicts and with no changes and conflicts */
/* remove accepted previews and conflicts */
syncResourcePreview.resourcePreviews.filter(r =>
!r.merged
&& (r.localChange !== Change.None || r.remoteChange !== Change.None)
r.mergeState !== MergeState.Accepted
&& !this._conflicts.some(c => c.syncResource === syncResource && isEqual(c.local, r.localResource)))
]))
);
......
......@@ -102,8 +102,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.channel.call('hasLocalData');
}
acceptPreviewContent(syncResource: SyncResource, resource: URI, content: string): Promise<void> {
return this.channel.call('acceptPreviewContent', [syncResource, resource, content]);
accept(syncResource: SyncResource, resource: URI, content: string, apply: boolean): Promise<void> {
return this.channel.call('accept', [syncResource, resource, content, apply]);
}
resolveContent(resource: URI): Promise<string | null> {
......@@ -191,11 +191,16 @@ class ManualSyncTask implements IManualSyncTask {
return this.deserializePreviews(previews);
}
async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
async merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('merge', [resource]);
return this.deserializePreviews(previews);
}
async apply(): Promise<[SyncResource, ISyncResourcePreview][]> {
const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('apply');
return this.deserializePreviews(previews);
}
pull(): Promise<void> {
return this.channel.call('pull');
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册