Implement merge in sync providers

This commit is contained in:
benweet 2014-03-28 00:49:49 +00:00
parent 533558945b
commit 0e5f198270
11 changed files with 236 additions and 83 deletions

View File

@ -88,7 +88,7 @@ define([
});
Object.defineProperty(this, 'discussionListJSON', {
get: function() {
return storage[this.fileIndex + ".discussionList"];
return storage[this.fileIndex + ".discussionList"] || '{}';
},
set: function(discussionList) {
this._discussionList = JSON.parse(discussionList);

View File

@ -1,10 +1,126 @@
define(function() {
define([
'underscore',
'utils',
'settings',
'eventMgr',
'fileMgr',
'diff_match_patch_uncompressed',
'jsondiffpatch',
], function(_, utils, settings, eventMgr, fileMgr, diff_match_patch, jsondiffpatch) {
function Provider(providerId, providerName) {
this.providerId = providerId;
this.providerName = providerName;
this.isPublishEnabled = true;
}
// Parse and check a JSON discussion list
Provider.prototype.parseDiscussionList = function(discussionListJSON) {
try {
var discussionList = JSON.parse(discussionListJSON);
_.each(discussionList, function(discussion, discussionIndex) {
if(
(discussion.discussionIndex != discussionIndex) ||
(!_.isNumber(discussion.selectionStart)) ||
(!_.isNumber(discussion.selectionEnd))
) {
throw 'invalid';
}
discussion.commentList.forEach(function(comment) {
if(
(!_.isString(comment.author)) ||
(!_.isString(comment.content))
) {
throw 'invalid';
}
});
});
return discussionList;
}
catch(e) {
}
};
Provider.prototype.serializeContent = function(content, discussionList) {
if(_.size(discussionList) !== 0) {
return content + '<!--se_discussion_list:' + discussionList + '-->';
}
return content;
};
Provider.prototype.parseSerializedContent = function(content) {
var discussionList = '{}';
var discussionExtractor = /<!--se_discussion_list:([\s\S]+)-->$/.exec(content);
if(discussionExtractor && this.parseDiscussionList(discussionExtractor[1])) {
content = content.substring(0, discussionExtractor.index);
discussionList = discussionExtractor[1];
}
return {
content: content,
discussionList: discussionList
};
};
var diffMatchPatch = new diff_match_patch();
var jsonDiffPatch = jsondiffpatch.create({
objectHash: function(obj) {
return JSON.stringify(obj);
},
arrays: {
detectMove: false,
},
textDiff: {
minLength: 9999999
}
});
var merge = settings.conflictMode == 'merge';
Provider.prototype.merge = function(localContent, remoteContent, localTitle, remoteTitle, localDiscussionList, remoteDiscussionList, syncAttributes) {
// Local/Remote CRCs
var localContentCRC = utils.crc32(localContent);
var localTitleCRC = utils.crc32(localTitle);
var localDiscussionListCRC = utils.crc32(localDiscussionList);
var remoteContentCRC = utils.crc32(remoteContent);
var remoteTitleCRC = utils.crc32(remoteTitle);
var remoteDiscussionListCRC = utils.crc32(remoteDiscussionList);
// Check content
var contentChanged = localContent != remoteContent;
var localContentChanged = syncAttributes.contentCRC != localContentCRC;
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
var contentConflict = contentChanged && localContentChanged && remoteContentChanged;
// Check title
syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox
var titleChanged = localTitle != remoteTitle;
var localTitleChanged = syncAttributes.titleCRC != localTitleCRC;
var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC;
var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged;
// Check discussionList
var discussionListChanged = localDiscussionList != remoteDiscussionList;
var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC;
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged;
// Conflict detection
if(
(!merge && (contentConflict || titleConflict || discussionListConflict)) ||
(contentConflict && syncAttributes.content === undefined) ||
(titleConflict && syncAttributes.title === undefined) ||
(discussionListConflict && syncAttributes.discussionList === undefined)
) {
fileMgr.createFile(localTitle + " (backup)", localContent);
eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
}
else {
if(contentConflict === true) {
var patch = diffMatchPatch.patch_make(syncAttributes.content, localContent);
}
}
};
return Provider;
});
});

View File

@ -11,18 +11,6 @@ define([
'MutationObservers',
'libs/prism-markdown'
], function ($, _, settings, eventMgr, Prism, diff_match_patch, jsondiffpatch, crel) {
var diffMatchPatch = new diff_match_patch();
var jsonDiffPatch = jsondiffpatch.create({
objectHash: function(obj) {
return JSON.stringify(obj);
},
arrays: {
detectMove: false,
},
textDiff: {
minLength: 9999999
}
});
function strSplice(str, i, remove, add) {
remove = +remove || 0;
@ -86,6 +74,19 @@ define([
});
}
var diffMatchPatch = new diff_match_patch();
var jsonDiffPatch = jsondiffpatch.create({
objectHash: function(obj) {
return JSON.stringify(obj);
},
arrays: {
detectMove: false,
},
textDiff: {
minLength: 9999999
}
});
var previousTextContent;
var currentMode;
editor.undoManager = (function() {

View File

@ -111,7 +111,7 @@ define([
refreshDiscussions();
};
comments.onContentChanged = function(fileDesc, content) {
comments.onContentChanged = function(fileDesc) {
currentFileDesc === fileDesc && refreshDiscussions();
};
@ -359,7 +359,7 @@ define([
// Focus on textarea
context.$contentInputElt.focus().val(previousContent);
}).on('hide.bs.popover', '#wmd-input > .editor-margin', function(evt) {
}).on('hide.bs.popover', '#wmd-input > .editor-margin', function() {
if(!currentContext) {
return;
}

View File

@ -50,7 +50,7 @@ define([
core.initEditor(fileDesc);
};
fileMgr.createFile = function(title, content, syncLocations, isTemporary) {
fileMgr.createFile = function(title, content, discussionListJSON, syncLocations, isTemporary) {
content = content !== undefined ? content : settings.defaultContent;
if(!title) {
// Create a file title
@ -86,6 +86,7 @@ define([
// Create the file descriptor
var fileDesc = new FileDescriptor(fileIndex, title, syncLocations);
discussionListJSON && (fileDesc.discussionListJSON = discussionListJSON);
// Add the index to the file list
if(!isTemporary) {

View File

@ -2,11 +2,12 @@ define([
"underscore",
"utils",
"storage",
"settings",
"classes/Provider",
"eventMgr",
"fileMgr",
"helpers/dropboxHelper"
], function(_, utils, storage, Provider, eventMgr, fileMgr, dropboxHelper) {
], function(_, utils, storage, settings, Provider, eventMgr, fileMgr, dropboxHelper) {
var PROVIDER_DROPBOX = "dropbox";
@ -55,7 +56,8 @@ define([
var syncAttributes = createSyncAttributes(file.path, file.versionTag, file.content);
var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
var fileDesc = fileMgr.createFile(file.name, file.content, syncLocations);
var parsingResult = dropboxProvider.parseSerializedContent(file.content);
var fileDesc = fileMgr.createFile(file.name, parsingResult.content, parsingResult.discussionList, syncLocations);
fileMgr.selectFile(fileDesc);
fileDescList.push(fileDesc);
});
@ -76,8 +78,7 @@ define([
var syncIndex = createSyncIndex(path);
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
if(fileDesc !== undefined) {
eventMgr.onError('"' + fileDesc.title + '" was already imported.');
return;
return eventMgr.onError('"' + fileDesc.title + '" was already imported.');
}
importPaths.push(path);
});
@ -89,8 +90,7 @@ define([
var path = utils.getInputTextValue("#input-sync-export-dropbox-path", event);
path = checkPath(path);
if(path === undefined) {
callback(true);
return;
return callback(true);
}
// Check that file is not synchronized with another one
var syncIndex = createSyncIndex(path);
@ -98,33 +98,39 @@ define([
if(fileDesc !== undefined) {
var existingTitle = fileDesc.title;
eventMgr.onError('File path is already synchronized with "' + existingTitle + '".');
callback(true);
return;
return callback(true);
}
dropboxHelper.upload(path, content, function(error, result) {
if(error) {
callback(error);
return;
return callback(error);
}
var syncAttributes = createSyncAttributes(result.path, result.versionTag, content);
callback(undefined, syncAttributes);
});
};
dropboxProvider.syncUp = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) {
var syncContentCRC = syncAttributes.contentCRC;
// Skip if CRC has not changed
if(uploadContentCRC == syncContentCRC) {
callback(undefined, false);
return;
var merge = settings.conflictMode == 'merge';
dropboxProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) {
if(
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
(syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed
) {
return callback(undefined, false);
}
dropboxHelper.upload(syncAttributes.path, uploadContent, function(error, result) {
var uploadedContent = dropboxProvider.serializeContent(content, discussionList);
dropboxHelper.upload(syncAttributes.path, uploadedContent, function(error, result) {
if(error) {
callback(error, true);
return;
return callback(error, true);
}
syncAttributes.version = result.versionTag;
syncAttributes.contentCRC = uploadContentCRC;
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = content;
syncAttributes.discussionList = discussionList;
}
syncAttributes.contentCRC = contentCRC;
syncAttributes.discussionListCRC = discussionListCRC;
callback(undefined, true);
});
};
@ -133,8 +139,7 @@ define([
var lastChangeId = storage[PROVIDER_DROPBOX + ".lastChangeId"];
dropboxHelper.checkChanges(lastChangeId, function(error, changes, newChangeId) {
if(error) {
callback(error);
return;
return callback(error);
}
var interestingChanges = [];
_.each(changes, function(change) {
@ -164,8 +169,7 @@ define([
var syncAttributes = change.syncAttributes;
var syncIndex = syncAttributes.syncIndex;
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
// No file corresponding (file may have been deleted
// locally)
// No file corresponding (file may have been deleted locally)
if(fileDesc === undefined) {
return;
}
@ -174,15 +178,22 @@ define([
if(change.wasRemoved === true) {
eventMgr.onError('"' + localTitle + '" has been removed from Dropbox.');
fileDesc.removeSyncLocation(syncAttributes);
eventMgr.onSyncRemoved(fileDesc, syncAttributes);
return;
return eventMgr.onSyncRemoved(fileDesc, syncAttributes);
}
var localContent = fileDesc.content;
var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent);
var localDiscussionList = fileDesc.discussionListJSON;
var localDiscussionListChanged = syncAttributes.discussionListCRC != utils.crc32(localDiscussionList);
var file = change.stat;
var remoteContentCRC = utils.crc32(file.content);
var parsingResult = dropboxProvider.parseSerializedContent(file.content);
var remoteContent = parsingResult.content;
var remoteDiscussionList = parsingResult.discussionList;
var remoteContentCRC = utils.crc32(remoteContent);
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
var fileContentChanged = localContent != file.content;
var remoteDiscussionListCRC = utils.crc32(remoteDiscussionList);
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
var fileContentChanged = localContent != remoteContent;
var fileDiscussionListChanged = localDiscussionList != remoteDiscussionList;
// Conflict detection
if(fileContentChanged === true && localContentChanged === true && remoteContentChanged === true) {
fileMgr.createFile(localTitle + " (backup)", localContent);
@ -211,8 +222,7 @@ define([
dropboxProvider.publish = function(publishAttributes, frontMatter, title, content, callback) {
var path = checkPath(publishAttributes.path);
if(path === undefined) {
callback(true);
return;
return callback(true);
}
dropboxHelper.upload(path, content, callback);
};

View File

@ -56,7 +56,8 @@ define([
syncAttributes.isRealtime = file.isRealtime;
var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
fileDesc = fileMgr.createFile(file.title, file.content, syncLocations);
var parsingResult = gdriveProvider.parseSerializedContent(file.content);
fileDesc = fileMgr.createFile(file.title, parsingResult.content, parsingResult.discussionList, syncLocations);
fileDescList.push(fileDesc);
});
if(fileDesc !== undefined) {
@ -122,37 +123,48 @@ define([
});
};
gdriveProvider.syncUp = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) {
// Skip if CRC has not changed
if(uploadContentCRC == syncAttributes.contentCRC && uploadTitleCRC == syncAttributes.titleCRC) {
callback(undefined, false);
return;
var merge = settings.conflictMode == 'merge';
gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) {
if(
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
(syncAttributes.titleCRC == titleCRC) && // Content CRC hasn't changed
(syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed
) {
return callback(undefined, false);
}
googleHelper.upload(syncAttributes.id, undefined, uploadTitle, uploadContent, undefined, syncAttributes.etag, accountId, function(error, result) {
var uploadedContent = gdriveProvider.serializeContent(content, discussionList);
googleHelper.upload(syncAttributes.id, undefined, title, uploadedContent, undefined, syncAttributes.etag, accountId, function(error, result) {
if(error) {
callback(error, true);
return;
}
syncAttributes.etag = result.etag;
syncAttributes.contentCRC = uploadContentCRC;
syncAttributes.titleCRC = uploadTitleCRC;
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = content;
syncAttributes.title = title;
syncAttributes.discussionList = discussionList;
}
syncAttributes.contentCRC = contentCRC;
syncAttributes.titleCRC = titleCRC;
syncAttributes.discussionListCRC = discussionListCRC;
callback(undefined, true);
});
};
gdriveProvider.syncUpRealtime = function(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, callback) {
gdriveProvider.syncUpRealtime = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) {
// Skip if title CRC has not changed
if(uploadTitleCRC == syncAttributes.titleCRC) {
if(titleCRC == syncAttributes.titleCRC) {
callback(undefined, false);
return;
}
googleHelper.rename(syncAttributes.id, uploadTitle, accountId, function(error, result) {
googleHelper.rename(syncAttributes.id, title, accountId, function(error, result) {
if(error) {
callback(error, true);
return;
}
syncAttributes.etag = result.etag;
syncAttributes.titleCRC = uploadTitleCRC;
syncAttributes.titleCRC = titleCRC;
callback(undefined, true);
});
};
@ -646,7 +658,7 @@ define([
var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title);
var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
var fileDesc = fileMgr.createFile(file.title, file.content, syncLocations);
var fileDesc = fileMgr.createFile(file.title, file.content, undefined, syncLocations);
fileMgr.selectFile(fileDesc);
eventMgr.onMessage('"' + file.title + '" created successfully on ' + providerName + '.');
});

View File

@ -13,6 +13,7 @@ define([
maxWidth: 960,
defaultContent: "\n\n\n> Written with [StackEdit](" + constants.MAIN_URL + ").",
commitMsg: "Published with " + constants.MAIN_URL,
conflictMode: 'merge',
gdriveMultiAccount: 1,
gdriveFullAccess: true,
dropboxFullAccess: true,

View File

@ -126,10 +126,10 @@ define([
if(error) {
return;
}
var fileDesc = fileMgr.createFile(title, content, undefined, true);
var fileDesc = fileMgr.createFile(title, content, undefined, undefined, true);
fileMgr.selectFile(fileDesc);
});
});
return sharing;
});
});

View File

@ -1041,7 +1041,6 @@ a {
font-family: "PT Sans", sans-serif;
line-height: @editor-line-weight;
letter-spacing: normal;
font-size: @font-size-base;
border-radius: 0;
color: @tertiary-color-dark;
.box-shadow(none);

View File

@ -47,12 +47,12 @@ define([
}
});
});
// AutoSync configuration
_.each(providerMap, function(provider) {
provider.autosyncConfig = utils.retrieveIgnoreError(provider.providerId + ".autosyncConfig") || {};
});
// Returns true if at least one file has synchronized location
synchronizer.hasSync = function(provider) {
return _.some(fileSystem, function(fileDesc) {
@ -72,6 +72,8 @@ define([
var uploadContentCRC;
var uploadTitle;
var uploadTitleCRC;
var uploadDiscussionList;
var uploadDiscussionListCRC;
function locationUp(callback) {
// No more synchronized location for this document
@ -90,21 +92,30 @@ define([
}
// Use the specified provider to perform the upload
providerSyncUpFunction(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, function(error, uploadFlag) {
if(uploadFlag === true) {
// If uploadFlag is true, request another upload cycle
uploadCycle = true;
providerSyncUpFunction(
uploadContent,
uploadContentCRC,
uploadTitle,
uploadTitleCRC,
uploadDiscussionList,
uploadTitleCRC,
uploadDiscussionListCRC,
function(error, uploadFlag) {
if(uploadFlag === true) {
// If uploadFlag is true, request another upload cycle
uploadCycle = true;
}
if(error) {
callback(error);
return;
}
if(uploadFlag) {
// Update syncAttributes in storage
utils.storeAttributes(syncAttributes);
}
locationUp(callback);
}
if(error) {
callback(error);
return;
}
if(uploadFlag) {
// Update syncAttributes in storage
utils.storeAttributes(syncAttributes);
}
locationUp(callback);
});
);
}
// Recursive function to upload multiple files
@ -130,6 +141,8 @@ define([
uploadContentCRC = utils.crc32(uploadContent);
uploadTitle = fileDesc.title;
uploadTitleCRC = utils.crc32(uploadTitle);
uploadDiscussionList = fileDesc.discussionListJSON;
uploadDiscussionListCRC = utils.crc32(uploadDiscussionList);
locationUp(callback);
}