pcottle.learnGitBranching/src/js/graph/treeCompare.js
2021-04-19 10:52:05 +07:00

465 lines
15 KiB
JavaScript

var _ = require('underscore');
// static class...
var TreeCompare = {};
TreeCompare.dispatchFromLevel = function(levelBlob, treeToCompare) {
var goalTreeString = levelBlob.goalTreeString;
if (typeof treeToCompare !== 'string') {
console.warn('NEED to pass in string!! gah');
}
return TreeCompare.dispatch(levelBlob, goalTreeString, treeToCompare);
};
TreeCompare.onlyMainCompared = function(levelBlob) {
var getAroundLintTrue = true;
switch (getAroundLintTrue) {
case !!levelBlob.compareOnlyMain:
case !!levelBlob.compareOnlyMainHashAgnostic:
case !!levelBlob.compareOnlyMainHashAgnosticWithAsserts:
return true;
default:
return false;
}
};
TreeCompare.dispatch = function(levelBlob, goalTreeString, treeToCompare) {
var goalTree = this.convertTreeSafe(goalTreeString);
treeToCompare = this.convertTreeSafe(treeToCompare);
if (typeof goalTree.originTree !== typeof treeToCompare.originTree) {
// origin status does not match
return false;
}
var shallowResult = this.dispatchShallow(
levelBlob, goalTree, treeToCompare
);
if (!shallowResult || !goalTree.originTree) {
// we only have one level (or failed on shallow), punt
return shallowResult;
}
var originBlob = (levelBlob.originCompare) ?
levelBlob.originCompare : levelBlob;
// compare origin trees
return shallowResult && this.dispatchShallow(
originBlob, goalTree.originTree, treeToCompare.originTree
);
};
TreeCompare.dispatchShallow = function(levelBlob, goalTreeString, treeToCompare) {
var getAroundLintTrue = true;
// i actually prefer this to else if
switch (getAroundLintTrue) {
case !!levelBlob.compareOnlyMain:
return TreeCompare.compareBranchWithinTrees(
treeToCompare, goalTreeString, 'main'
);
case !!levelBlob.compareAllBranchesAndEnforceBranchCleanup:
return TreeCompare.compareAllBranchesAndEnforceBranchCleanup(
treeToCompare, goalTreeString
);
case !!levelBlob.compareOnlyBranches:
return TreeCompare.compareAllBranchesWithinTrees(
treeToCompare, goalTreeString
);
case !!levelBlob.compareAllBranchesHashAgnostic:
return TreeCompare.compareAllBranchesWithinTreesHashAgnostic(
treeToCompare, goalTreeString
);
case !!levelBlob.compareOnlyMainHashAgnostic:
return TreeCompare.compareBranchesWithinTreesHashAgnostic(
treeToCompare, goalTreeString, ['main']
);
case !!levelBlob.compareOnlyMainHashAgnosticWithAsserts:
return TreeCompare.compareBranchesWithinTreesHashAgnostic(
treeToCompare, goalTreeString, ['main']
) && TreeCompare.evalAsserts(treeToCompare, levelBlob.goalAsserts);
default:
return TreeCompare.compareAllBranchesWithinTreesAndHEAD(
treeToCompare, goalTreeString
);
}
};
// would love to have copy properties here.. :(
TreeCompare.compareAllBranchesWithinTreesAndHEAD = function(treeToCompare, goalTree) {
treeToCompare = this.convertTreeSafe(treeToCompare);
goalTree = this.convertTreeSafe(goalTree);
// also compare tags!! for just one level
return treeToCompare.HEAD.target === goalTree.HEAD.target &&
this.compareAllBranchesWithinTrees(treeToCompare, goalTree) &&
this.compareAllTagsWithinTrees(treeToCompare, goalTree);
};
TreeCompare.compareAllBranchesAndEnforceBranchCleanup = function(treeToCompare, goalTree) {
treeToCompare = this.convertTreeSafe(treeToCompare);
goalTree = this.convertTreeSafe(goalTree);
// Unlike compareAllBranchesWithinTrees, here we consider both the branches
// in the goalTree and the branches in the treeToCompare. This means that
// we enforce that you clean up any branches that you have locally that
// the goal does not have. this is helpful when we want to verify that you
// have deleted branch, for instance.
var allBranches = Object.assign(
{},
treeToCompare.branches,
goalTree.branches
);
return Object.keys(allBranches).every(function(branch) {
return this.compareBranchWithinTrees(treeToCompare, goalTree, branch);
}.bind(this));
};
TreeCompare.compareAllBranchesWithinTrees = function(treeToCompare, goalTree) {
treeToCompare = this.convertTreeSafe(treeToCompare);
goalTree = this.convertTreeSafe(goalTree);
/**
* Disclaimer / reminder!! We only care about branches in the goal tree;
* if you have extra branches in your source tree thats ok. but that means
* the arguments here are important -- always call this function with
* goalTree being the latter argument, since we will discard extra branches
* from treeToCompare (the first argument).
*/
return Object.keys(goalTree.branches).every(function(branch) {
return this.compareBranchWithinTrees(treeToCompare, goalTree, branch);
}.bind(this));
};
TreeCompare.compareAllTagsWithinTrees = function(treeToCompare, goalTree) {
treeToCompare = this.convertTreeSafe(treeToCompare);
goalTree = this.convertTreeSafe(goalTree);
this.reduceTreeFields([treeToCompare, goalTree]);
return _.isEqual(treeToCompare.tags, goalTree.tags);
};
TreeCompare.compareBranchesWithinTrees = function(treeToCompare, goalTree, branches) {
var result = true;
branches.forEach(function(branchName) {
result = result && this.compareBranchWithinTrees(treeToCompare, goalTree, branchName);
}, this);
return result;
};
TreeCompare.compareBranchWithinTrees = function(treeToCompare, goalTree, branchName) {
treeToCompare = this.convertTreeSafe(treeToCompare);
goalTree = this.convertTreeSafe(goalTree);
this.reduceTreeFields([treeToCompare, goalTree]);
var recurseCompare = this.getRecurseCompare(treeToCompare, goalTree);
var branchA = treeToCompare.branches[branchName];
var branchB = goalTree.branches[branchName];
return _.isEqual(branchA, branchB) &&
recurseCompare(treeToCompare.commits[branchA.target], goalTree.commits[branchB.target]);
};
TreeCompare.compareAllBranchesWithinTreesHashAgnostic = function(treeToCompare, goalTree) {
treeToCompare = this.convertTreeSafe(treeToCompare);
goalTree = this.convertTreeSafe(goalTree);
this.reduceTreeFields([treeToCompare, goalTree]);
var allBranches = Object.assign(
{},
treeToCompare.branches,
goalTree.branches
);
var branchNames = Object.keys(allBranches || {});
return this.compareBranchesWithinTreesHashAgnostic(treeToCompare, goalTree, branchNames);
};
TreeCompare.compareBranchesWithinTreesHashAgnostic = function(treeToCompare, goalTree, branches) {
// we can't DRY unfortunately here because we need a special _.isEqual function
// for both the recursive compare and the branch compare
treeToCompare = this.convertTreeSafe(treeToCompare);
goalTree = this.convertTreeSafe(goalTree);
this.reduceTreeFields([treeToCompare, goalTree]);
// get a function to compare branch objects without hashes
var compareBranchObjs = function(branchA, branchB) {
if (!branchA || !branchB) {
return false;
}
// don't mess up the rest of comparison
branchA = Object.assign({}, branchA);
branchB = Object.assign({}, branchB);
branchA.target = this.getBaseRef(branchA.target);
branchB.target = this.getBaseRef(branchB.target);
return _.isEqual(branchA, branchB);
}.bind(this);
// and a function to compare recursively without worrying about hashes
var recurseCompare = this.getRecurseCompareHashAgnostic(treeToCompare, goalTree);
var result = true;
branches.forEach(function(branchName) {
var branchA = treeToCompare.branches[branchName];
var branchB = goalTree.branches[branchName];
result = result && compareBranchObjs(branchA, branchB) &&
recurseCompare(treeToCompare.commits[branchA.target], goalTree.commits[branchB.target]);
}, this);
return result;
};
TreeCompare.evalAsserts = function(tree, assertsPerBranch) {
var result = true;
Object.keys(assertsPerBranch).forEach(function(branchName) {
var asserts = assertsPerBranch[branchName];
result = result && this.evalAssertsOnBranch(tree, branchName, asserts);
}, this);
return result;
};
TreeCompare.evalAssertsOnBranch = function(tree, branchName, asserts) {
tree = this.convertTreeSafe(tree);
// here is the outline:
// * make a data object
// * go to the branch given by the key
// * traverse upwards, storing the amount of hashes on each in the data object
// * then come back and perform functions on data
if (!tree.branches[branchName]) {
return false;
}
var branch = tree.branches[branchName];
var queue = [branch.target];
var data = {};
while (queue.length) {
var commitRef = queue.pop();
data[this.getBaseRef(commitRef)] = this.getNumHashes(commitRef);
queue = queue.concat(tree.commits[commitRef].parents);
}
var result = true;
asserts.forEach(function(assert) {
try {
result = result && assert(data);
} catch (err) {
console.warn('error during assert', err);
console.log(err);
result = false;
}
});
return result;
};
TreeCompare.getNumHashes = function(ref) {
var regexMap = [
[/^C(\d+)([']{0,3})$/, function(bits) {
if (!bits[2]) {
return 0;
}
return bits[2].length;
}],
[/^C(\d+)['][\^](\d+)$/, function(bits) {
return Number(bits[2]);
}]
];
for (var i = 0; i < regexMap.length; i++) {
var regex = regexMap[i][0];
var func = regexMap[i][1];
var results = regex.exec(ref);
if (results) {
return func(results);
}
}
throw new Error('couldn\'t parse ref ' + ref);
};
TreeCompare.getBaseRef = function(ref) {
var idRegex = /^C(\d+)/;
var bits = idRegex.exec(ref);
if (!bits) { throw new Error('no regex matchy for ' + ref); }
// no matter what hash this is (aka C1', C1'', C1'^3, etc) we
// return C1
return 'C' + bits[1];
};
TreeCompare.getRecurseCompareHashAgnostic = function(treeToCompare, goalTree) {
// here we pass in a special comparison function to pass into the base
// recursive compare.
// some buildup functions
var getStrippedCommitCopy = function(commit) {
if (!commit) { return {}; }
return Object.assign(
{},
commit,
{
id: this.getBaseRef(commit.id),
parents: null
}
);
}.bind(this);
var isEqual = function(commitA, commitB) {
return _.isEqual(
getStrippedCommitCopy(commitA),
getStrippedCommitCopy(commitB)
);
};
return this.getRecurseCompare(treeToCompare, goalTree, {isEqual: isEqual});
};
TreeCompare.getRecurseCompare = function(treeToCompare, goalTree, options) {
options = options || {};
// we need a recursive comparison function to bubble up the branch
var recurseCompare = function(commitA, commitB) {
// this is the short-circuit base case
var result = options.isEqual ?
options.isEqual(commitA, commitB) : _.isEqual(commitA, commitB);
if (!result) {
return false;
}
// we loop through each parent ID. we sort the parent ID's beforehand
// so the index lookup is valid. for merge commits this will duplicate some of the
// checking (because we aren't doing graph search) but it's not a huge deal
var maxNumParents = Math.max(commitA.parents.length, commitB.parents.length);
for (var index = 0; index < maxNumParents; index++) {
var pAid = commitA.parents[index];
var pBid = commitB.parents[index];
// if treeToCompare or goalTree doesn't have this parent,
// then we get an undefined child which is fine when we pass into _.isEqual
var childA = treeToCompare.commits[pAid];
var childB = goalTree.commits[pBid];
result = result && recurseCompare(childA, childB);
}
// if each of our children recursively are equal, we are good
return result;
};
return recurseCompare;
};
TreeCompare.lowercaseTree = function(tree) {
if (tree.HEAD) {
tree.HEAD.target = tree.HEAD.target.toLocaleLowerCase();
}
var branches = tree.branches || {};
tree.branches = {};
Object.keys(branches).forEach(function(name) {
var obj = branches[name];
obj.id = obj.id.toLocaleLowerCase();
tree.branches[name.toLocaleLowerCase()] = obj;
});
return tree;
};
TreeCompare.convertTreeSafe = function(tree) {
if (typeof tree !== 'string') {
return tree;
}
tree = JSON.parse(unescape(tree));
// ok we are almost done -- but we need to case insensitive
// certain fields. so go ahead and do that.
// handle HEAD target first
this.lowercaseTree(tree);
if (tree.originTree) {
tree.originTree = this.lowercaseTree(tree.originTree);
}
return tree;
};
TreeCompare.reduceTreeFields = function(trees) {
var commitSaveFields = [
'parents',
'id',
'rootCommit'
];
var branchSaveFields = [
'target',
'id',
'remoteTrackingBranchID'
];
var tagSaveFields = [
'target',
'id'
];
var commitSortFields = ['children', 'parents'];
// for backwards compatibility, fill in some fields if missing
var defaults = {
remoteTrackingBranchID: null
};
// also fill tree-level defaults
var treeDefaults = {
tags: {}
};
trees.forEach(function(tree) {
Object.keys(treeDefaults).forEach(function(key) {
var val = treeDefaults[key];
if (tree[key] === undefined) {
tree[key] = val;
}
});
});
// this function saves only the specified fields of a tree
var saveOnly = function(tree, treeKey, saveFields, sortFields) {
var objects = tree[treeKey];
Object.keys(objects).forEach(function(objKey) {
var obj = objects[objKey];
// our blank slate to copy over
var blank = {};
saveFields.forEach(function(field) {
if (obj[field] !== undefined) {
blank[field] = obj[field];
} else if (defaults[field] !== undefined) {
blank[field] = defaults[field];
}
});
Object.values(sortFields || {}).forEach(function(field) {
// also sort some fields
if (obj[field]) {
obj[field].sort();
blank[field] = obj[field];
}
});
tree[treeKey][objKey] = blank;
});
};
trees.forEach(function(tree) {
saveOnly(tree, 'commits', commitSaveFields, commitSortFields);
saveOnly(tree, 'branches', branchSaveFields);
saveOnly(tree, 'tags', tagSaveFields);
tree.HEAD = {
target: tree.HEAD.target,
id: tree.HEAD.id
};
if (tree.originTree) {
this.reduceTreeFields([tree.originTree]);
}
}, this);
};
TreeCompare.compareTrees = function(treeToCompare, goalTree) {
treeToCompare = this.convertTreeSafe(treeToCompare);
goalTree = this.convertTreeSafe(goalTree);
// now we need to strip out the fields we don't care about, aka things
// like createTime, message, author
this.reduceTreeFields([treeToCompare, goalTree]);
return _.isEqual(treeToCompare, goalTree);
};
module.exports = TreeCompare;