Skip to content

Commit

Permalink
#86678 add merge tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 committed Mar 19, 2020
1 parent 9dca561 commit 6a9b88c
Show file tree
Hide file tree
Showing 3 changed files with 512 additions and 39 deletions.
67 changes: 50 additions & 17 deletions src/vs/platform/userDataSync/common/snippetsMerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ export interface IMergeResult {
remote: IStringDictionary<string> | null;
}

export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null): IMergeResult {
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null, resolvedConflicts: IStringDictionary<string | null> = {}): IMergeResult {
const added: IStringDictionary<string> = {};
const updated: IStringDictionary<string> = {};
const removed: string[] = [];
const removed: Set<string> = new Set<string>();

if (!remote) {
return {
added,
removed,
removed: values(removed),
updated,
conflicts: [],
remote: local
Expand All @@ -35,7 +35,7 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar
// No changes found between local and remote.
return {
added,
removed,
removed: values(removed),
updated,
conflicts: [],
remote: null
Expand All @@ -44,8 +44,41 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar

const baseToLocal = compare(base, local);
const baseToRemote = compare(base, remote);
const remoteContent: IStringDictionary<string> = deepClone(remote);
const conflicts: Set<string> = new Set<string>();
let remoteContent: IStringDictionary<string> = deepClone(remote);
const handledConflicts: Set<string> = new Set<string>();
const handleConflict = (key: string): void => {
if (handledConflicts.has(key)) {
return;
}
handledConflicts.add(key);
const conflictContent = resolvedConflicts[key];

// add to conflicts
if (conflictContent === undefined) {
conflicts.add(key);
}

// remove the snippet
else if (conflictContent === null) {
delete remote[key];
if (local[key]) {
removed.add(key);
}
}

// add/update the snippet
else {
if (local[key]) {
if (local[key] !== conflictContent) {
updated[key] = conflictContent;
}
} else {
added[key] = conflictContent;
}
remoteContent[key] = conflictContent;
}
};

// Removed snippets in Local
for (const key of values(baseToLocal.removed)) {
Expand All @@ -62,29 +95,29 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar

// Removed snippets in Remote
for (const key of values(baseToRemote.removed)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Conflict - Got updated in local
if (baseToLocal.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
// Also remove in Local
else {
removed.push(key);
removed.add(key);
}
}

// Updated snippets in Local
for (const key of values(baseToLocal.updated)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Got updated in remote
if (baseToRemote.updated.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
} else {
remoteContent[key] = local[key];
Expand All @@ -93,14 +126,14 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar

// Updated snippets in Remote
for (const key of values(baseToRemote.updated)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Got updated in local
if (baseToLocal.updated.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
} else if (local[key] !== undefined) {
updated[key] = remote[key];
Expand All @@ -109,14 +142,14 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar

// Added snippets in Local
for (const key of values(baseToLocal.added)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Got added in remote
if (baseToRemote.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
} else {
remoteContent[key] = local[key];
Expand All @@ -125,21 +158,21 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar

// Added snippets in remote
for (const key of values(baseToRemote.added)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Got added in local
if (baseToLocal.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
} else {
added[key] = remote[key];
}
}

return { added, removed, updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent };
return { added, removed: values(removed), updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent };
}

function compare(from: IStringDictionary<string> | null, to: IStringDictionary<string> | null): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
Expand Down
48 changes: 26 additions & 22 deletions src/vs/platform/userDataSync/common/snippetsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,18 +196,13 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0];
if (this.status === SyncStatus.HasConflicts && conflict) {
const key = relativePath(this.snippetsPreviewFolder, conflict.local)!;
const result = await this.syncPreviewResultPromise!;
if (content) {
if (result.local[key]) {
result.updated[key] = content;
} else {
result.added[key] = content;
}
} else {
result.removed.push(key);
}
await this.fileService.del(conflict.local);
this.setConflicts(this.conflicts.filter(c => c !== conflict));
const previewResult = await this.syncPreviewResultPromise!;
this.cancel();
const resolvedConflicts: IStringDictionary<string | null> = {};
resolvedConflicts[key] = content || null;
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, resolvedConflicts, token));
await this.syncPreviewResultPromise;
this.setConflicts(previewResult.conflicts);
if (!this.conflicts.length) {
await this.apply();
this.setStatus(SyncStatus.Idle);
Expand All @@ -230,7 +225,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
try {
const previewResult = await this.getPreview(remoteUserData, lastSyncUserData);
if (previewResult.conflicts.length) {
this.setConflicts(previewResult.conflicts);
if (this.conflicts.length) {
return SyncStatus.HasConflicts;
}
await this.apply();
Expand All @@ -251,7 +247,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD

private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
this.syncPreviewResultPromise = createCancelablePromise(token => this.getSnippetsFileContents()
.then(local => this.generatePreview(local, remoteUserData, lastSyncUserData, {}, token)));
}
return this.syncPreviewResultPromise;
}
Expand All @@ -270,22 +267,19 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
}

private async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncPreviewResult> {
await this.clearConflicts();

private async generatePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null>, token: CancellationToken): Promise<ISyncPreviewResult> {
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null;

const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);

if (remoteSnippets) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`);
}

const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets);
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts);

const conflicts: Conflict[] = [];
for (const key of mergeResult.conflicts) {
const localPreview = joinPath(this.snippetsPreviewFolder, key);
Expand All @@ -296,7 +290,17 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
}

this.setConflicts(!token.isCancellationRequested ? conflicts : []);
for (const conflict of this.conflicts) {
// clear obsolete conflicts
if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) {
try {
await this.fileService.del(conflict.local);
} catch (error) {
// Ignore & log
this.logService.error(error);
}
}
}

return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote };
}
Expand Down
Loading

0 comments on commit 6a9b88c

Please sign in to comment.