webgme-engine
Version:
WebGME server and Client API without a GUI
439 lines (394 loc) • 16.8 kB
JavaScript
/*globals define*/
/*eslint-env node, browser*/
/**
* @author kecso / https://github.com/kecso
*/
define([
'common/regexp',
'common/core/coreQ',
'common/storage/constants',
'q',
'common/core/users/getroot',
'common/util/diff'
], function (REGEXP,
Core,
CONSTANTS,
Q,
getRoot,
DIFF) {
'use strict';
function save(parameters, callback) {
var persisted = parameters.core.persist(parameters.root),
newRootHash;
if (Object.hasOwn(persisted, 'objects') === false || Object.keys(persisted.objects).length === 0) {
parameters.logger.warn('empty patch was inserted - not making commit');
return Q({
hash: parameters.parents[0], //if there is no change, we return the first parent!!!
branchName: parameters.branchName
})
.nodeify(callback);
}
newRootHash = parameters.core.getHash(parameters.root);
//must use ASYNC version of makeCommit to allow usability from different project sources
return parameters.project.makeCommit(
parameters.branchName || null,
parameters.parents,
newRootHash,
persisted.objects,
parameters.msg)
.then(function (saveResult) {
saveResult.root = parameters.root;
saveResult.rootHash = newRootHash;
return saveResult;
});
}
function _diff(parameters) {
var deferred = Q.defer();
parameters.core.generateTreeDiff(parameters.sourceRoot, parameters.targetRoot, function (err, diff) {
if (err) {
deferred.reject(err);
return;
}
deferred.resolve(diff);
});
return deferred.promise;
}
/**
*
* @param {object} parameters
* @param {object} parameters.gmeConfig
* @param {object} parameters.logger
* @param {object} parameters.project
* @param {object} parameters.branchOrCommitA - the source
* @param {string} parameters.branchOrCommitB - the target
* @param callback
* @param callback.error
* @param callback.diff - The diff/patch object that when applied to source will generate the target.
* @returns {Promise}
*/
function diff(parameters, callback) {
var deferred = Q.defer(),
core = new Core(parameters.project, {
globConf: parameters.gmeConfig,
logger: parameters.logger.fork('core')
});
Q.all([
getRoot({project: parameters.project, core: core, id: parameters.branchOrCommitA}),
getRoot({project: parameters.project, core: core, id: parameters.branchOrCommitB})
])
.then(function (results) {
return _diff({core: core, sourceRoot: results[0].root, targetRoot: results[1].root});
})
.then(deferred.resolve)
.catch(deferred.reject);
return deferred.promise.nodeify(callback);
}
/**
*
* @param {object} parameters
* @param {object} parameters.gmeConfig
* @param {object} parameters.logger
* @param {object} parameters.project
* @param {object} parameters.patch
* @param {string} parameters.branchOrCommit
* @param {string} [parameters.branchName]
* @param {boolean} [parameters.noUpdate=false]
* @param {string} [parameters.msg]
* @param callback.error
* @param callback.diff - The diff/patch object that when applied to source will generate the target.
* @returns {Promise}
*/
function apply(parameters, callback) {
var core = new Core(parameters.project, {
globConf: parameters.gmeConfig,
logger: parameters.logger.fork('core')
}),
rootsResult,
branchName = parameters.branchName;
return getRoot({project: parameters.project, core: core, id: parameters.branchOrCommit})
.then(function (result) {
rootsResult = result;
return core.applyTreeDiff(rootsResult.root, parameters.patch);
})
.then(function () {
if (rootsResult.branchName && !parameters.noUpdate) {
branchName = branchName || rootsResult.branchName;
}
return save({
project: parameters.project,
logger: parameters.logger,
core: core,
root: rootsResult.root,
parents: parameters.parents || [rootsResult.commitHash],
branchName: branchName,
msg: parameters.msg || 'applying patch'
});
})
.nodeify(callback);
}
/**
*
* @param parameters
* @param callback
* @returns {*}
*/
function merge(parameters, callback) {
var deferred = Q.defer(),
result = {},
theirRoot = null,
myRoot = null,
baseRoot = null,
core = new Core(parameters.project, {
globConf: parameters.gmeConfig,
logger: parameters.logger.fork('core')
}),
startTime = Date.now(),
branchName;
// Allow to tie down commit and still update branch.
branchName = parameters.branchName ||
REGEXP.HASH.test(parameters.theirBranchOrCommit) ? null : parameters.theirBranchOrCommit;
function updateBranch() {
return parameters.project.setBranchHash(
branchName,
result.finalCommitHash,
result.theirCommitHash)
.then(function (commitResult) {
if (commitResult.status !== CONSTANTS.FORKED) {
// Branch was updated
result.updatedBranch = branchName;
} else {
parameters.logger.debug('merged commit forked', commitResult);
}
});
}
function addBaseValues() {
var conflictNodePaths = {},
addPath = function (path, index) {
var pathObject = DIFF.pathToObject(path),
nodePath = pathObject.node,
subPath;
if (typeof nodePath === 'string') {
subPath = path.substr(nodePath.length);
if (Object.hasOwn(conflictNodePaths, nodePath)) {
conflictNodePaths[nodePath][subPath] = index;
return null;
} else {
conflictNodePaths[nodePath] = {};
conflictNodePaths[nodePath][subPath] = index;
return nodePath;
}
}
return null;
},
processNode = function (nodePath) {
var deferred = Q.defer();
Q.ninvoke(core, 'loadByPath', baseRoot, nodePath)
.then(function (node) {
var subPath,
newItem;
for (subPath in conflictNodePaths[nodePath]) {
newItem = result.conflict.items[conflictNodePaths[nodePath][subPath]];
newItem.other = {
path: newItem.mine.path,
info: newItem.mine.info,
value: DIFF.getValueFromNode(core, node, subPath)
};
}
deferred.resolve();
})
.catch(function (err) {
parameters.logger.error('Ignore during base value collection ignored:', err);
deferred.resolve();
});
return deferred.promise;
},
item,
processes = [],
nodePathToCheck,
i;
for (i = 0; i < result.conflict.items.length; i += 1) {
item = result.conflict.items[i];
if (item.mine.path === item.theirs.path &&
item.mine.value !== CONSTANTS.TO_DELETE_STRING &&
item.theirs.value !== CONSTANTS.TO_DELETE_STRING &&
JSON.stringify(item.mine.value) !== JSON.stringify(item.theirs.value)) {
// We only able to provide a third option if the conflict is not a result of removal
nodePathToCheck = addPath(item.mine.path, i);
if (nodePathToCheck !== null) {
processes.push(processNode(nodePathToCheck));
}
}
}
return Q.all(processes);
}
function doMerge() {
var mergeDeferred = Q.defer(),
noApply = false;
getRoot({project: parameters.project, core: core, id: result.baseCommitHash})
.then(function (_result) {
baseRoot = _result.root;
return Q.allSettled([
_diff({
core: core,
sourceRoot: baseRoot,
targetRoot: myRoot
}),
_diff({
core: core,
sourceRoot: baseRoot,
targetRoot: theirRoot
})
]);
})
.then(function (diffs) {
if (diffs[0].state === 'rejected') {
parameters.logger.error('Initial diff generation failed (base->mine)', diffs[0].reason);
throw diffs[0].reason;
}
if (diffs[1].state === 'rejected') {
parameters.logger.error('Initial diff generation failed (base->theirs)', diffs[1].reason);
throw diffs[1].reason;
}
result.diff = {
mine: diffs[0].value,
theirs: diffs[1].value
};
result.conflict = core.tryToConcatChanges(result.diff.mine, result.diff.theirs);
if (!parameters.auto) {
noApply = true;
return;
}
if (!result.conflict) {
parameters.logger.error('Initial diff concatenation failed');
throw new Error('error during merged patch calculation');
}
if (result.conflict.items.length > 0) {
//the user will find out that there were no update done
if (branchName) {
result.targetBranchName = branchName;
}
result.projectId = parameters.project.projectId;
noApply = true;
return addBaseValues();
}
return apply({
gmeConfig: parameters.gmeConfig,
logger: parameters.logger,
project: parameters.project,
branchOrCommit: result.baseCommitHash,
patch: result.conflict.merge,
parents: [result.theirCommitHash, result.myCommitHash]
});
})
.then(function (applyResult) {
if (noApply) {
return;
}
//we made the commit, but now we also have try to update the branch of necessary
result.finalCommitHash = applyResult.hash;
if (branchName) {
result.targetBranchName = branchName;
return updateBranch();
}
})
.then(mergeDeferred.resolve)
.catch(err => {
let message = 'Merge reproduction info [base:' + result.baseCommitHash +
', mine:' + result.myCommitHash + ', theirs:' + result.theirCommitHash + '] \n';
message += 'original error message:' + err.message;
err.messasge = message;
mergeDeferred.reject(err);
});
return mergeDeferred.promise;
}
Q.allSettled([
getRoot({project: parameters.project, core: core, id: parameters.myBranchOrCommit}),
getRoot({project: parameters.project, core: core, id: parameters.theirBranchOrCommit})
])
.then(function (results) {
myRoot = results[0].value.root;
theirRoot = results[1].value.root;
result.myCommitHash = results[0].value.commitHash;
result.theirCommitHash = results[1].value.commitHash;
return Q.nfcall(parameters.project.getCommonAncestorCommit,
result.myCommitHash, result.theirCommitHash);
})
.then(function (commitHash) {
result.baseCommitHash = commitHash;
//no change
if (result.myCommitHash === result.baseCommitHash) {
if (branchName) {
result.updatedBranch = branchName;
}
result.finalCommitHash = result.theirCommitHash;
return;
}
//check fast-forward
if (result.theirCommitHash === result.baseCommitHash) {
result.finalCommitHash = result.myCommitHash;
if (branchName) {
return updateBranch();
}
return;
}
return doMerge();
})
.then(function () {
var ms = Date.now() - startTime,
min = Math.floor(ms / 1000 / 60),
sec = (ms / 1000) % 60;
parameters.logger.debug('Merge exec time', min, 'min', sec, 'sec');
deferred.resolve(result);
})
.catch(deferred.reject);
return deferred.promise.nodeify(callback);
}
function resolve(parameters, callback) {
var deferred = Q.defer(),
core = new Core(parameters.project, {
globConf: parameters.gmeConfig,
logger: parameters.logger.fork('core')
}),
finalPatch = core.applyResolution(parameters.partial.conflict);
//TODO error handling should be checked - can the applyResolution fail???
apply({
gmeConfig: parameters.gmeConfig,
logger: parameters.logger,
project: parameters.project,
branchOrCommit: parameters.partial.baseCommitHash,
noUpdate: true,
patch: finalPatch,
parents: [parameters.partial.theirCommitHash, parameters.partial.myCommitHash],
msg: 'merge with resolved conflicts'
})
.then(function (applyResult) {
var result = {
hash: applyResult.hash
};
//we made the commit, but now we also have try to update the branch of necessary
if (!parameters.partial.targetBranchName) {
deferred.resolve(applyResult.hash);
return;
}
parameters.project.setBranchHash(
parameters.partial.targetBranchName,
applyResult.hash,
parameters.partial.theirCommitHash,
function (err) {
if (!err) {
result.updatedBranch = parameters.partial.targetBranchName;
}
deferred.resolve(result);
}
);
})
.catch(deferred.reject);
return deferred.promise.nodeify(callback);
}
return {
merge: merge,
diff: diff,
apply: apply,
resolve: resolve
};
});