1 // Copyright (c) 2008 Tony Garnock-Jones <tonyg@lshift.net>
2 // Copyright (c) 2008 LShift Ltd. <query@lshift.net>
4 // Permission is hereby granted, free of charge, to any person
5 // obtaining a copy of this software and associated documentation files
6 // (the "Software"), to deal in the Software without restriction,
7 // including without limitation the rights to use, copy, modify, merge,
8 // publish, distribute, sublicense, and/or sell copies of the Software,
9 // and to permit persons to whom the Software is furnished to do so,
10 // subject to the following conditions:
12 // The above copyright notice and this permission notice shall be
13 // included in all copies or substantial portions of the Software.
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19 // BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20 // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 random_hex_string: function(n) {
29 var digits = "0123456789abcdef";
31 for (var i = 0; i < n; i++) {
32 result = result + digits[Math.floor(Math.random() * 16)];
37 random_uuid: function() {
38 if (Dvcs._debugMode) {
39 return Dvcs.Util.random_hex_string(8);
41 return [Dvcs.Util.random_hex_string(8),
42 Dvcs.Util.random_hex_string(4),
43 "4" + Dvcs.Util.random_hex_string(3),
44 ((Math.floor(Math.random() * 256) & ~64) | 128).toString(16) +
45 Dvcs.Util.random_hex_string(2),
46 Dvcs.Util.random_hex_string(12)].join("-");
50 dict_union: function(s1, s2) {
53 for (k in s2) { result[k] = s2[k]; }
54 for (k in s1) { result[k] = s1[k]; }
58 dict_difference: function(s1, s2) {
61 for (k in s1) { result[k] = s1[k]; }
62 for (k in s2) { delete result[k]; }
66 dict_to_set: function(d) {
68 for (var k in d) { result[k] = 1; }
72 deepCopy: function(obj) {
74 // http://keithdevens.com/weblog/archive/2007/Jun/07/javascript.clone
76 // Does not handle recursive structures.
78 if (obj === null || typeof(obj) != 'object') {
82 var temp = obj.constructor();
83 for (var key in obj) {
84 temp[key] = Dvcs.Util.deepCopy(obj[key]);
91 simpleScalarMerger: function(v1, v0, v2) {
92 if (v1 == v2) return [{ok: v1}];
93 if (v1 == v0) return [{ok: v2}];
94 if (v2 == v0) return [{ok: v1}];
95 return [{conflict: {a: v1, o: v0, b: v2}}];
98 simpleTextualMerger: function(v1, v0, v2) {
99 return Diff.diff3_merge(v1, v0, v2, true);
105 Checkout: function(directParent, additionalParent, currentBranch) {
107 this.directParent = directParent;
108 this.additionalParent = additionalParent;
110 this.anyDirty = false;
111 this.currentBranch = currentBranch;
114 Repository: function() {
121 Dvcs.Mergers.Defaults["text"] = Dvcs.Mergers.simpleTextualMerger;
123 Dvcs.Checkout.prototype.setDirty = function(uuid) {
124 this.dirty[uuid] = uuid;
125 this.anyDirty = true;
128 Dvcs.Checkout.prototype.createFile = function() {
129 var uuid = Dvcs.Util.random_uuid();
130 if (Dvcs._debugMode) { uuid = "inode-" + uuid; }
131 this.inodes[uuid] = {};
136 Dvcs.Checkout.prototype.deleteFile = function(uuid) {
137 if (!this.inodes[uuid]) return false;
138 delete this.inodes[uuid];
139 this.anyDirty = true;
143 Dvcs.Checkout.prototype.fileExists = function(uuid) {
144 return !!(this.inodes[uuid]);
147 Dvcs.Checkout.prototype.hasProp = function(uuid, prop) {
148 var inode = this.inodes[uuid];
149 if (!inode) return null;
150 return (inode[prop] !== undefined);
153 Dvcs.Checkout.prototype.getProp = function(uuid, prop, defaultValue) {
154 var inode = this.inodes[uuid];
155 if (!inode) return null;
157 if (v === undefined) {
160 return Dvcs.Util.deepCopy(v);
164 Dvcs.Checkout.prototype.setProp = function(uuid, prop, value) {
165 var inode = this.inodes[uuid];
166 if (!inode) return false;
172 Dvcs.Checkout.prototype.clearProp = function(uuid, prop) {
173 var inode = this.inodes[uuid];
174 if (!inode) return null;
175 if (inode[prop] !== undefined) {
182 Dvcs.Checkout.prototype.forEachProp = function(uuid, f) {
183 var inode = this.inodes[uuid];
184 if (!inode) return null;
185 for (var prop in inode) {
186 f(prop, inode[prop]);
190 Dvcs.Checkout.prototype.forEachFile = function(f) {
191 for (var uuid in this.inodes) {
196 Dvcs.Checkout.prototype.getBranch = function() {
197 return this.currentBranch;
200 Dvcs.Checkout.prototype.setBranch = function(newBranch) {
201 this.currentBranch = newBranch;
204 Dvcs.Checkout.prototype.clone = function() {
205 var result = new Dvcs.Checkout(this.directParent,
206 this.additionalParent,
208 result.inodes = Dvcs.Util.deepCopy(this.inodes);
212 Dvcs.Repository.prototype.resolveRevId = function(revId) {
213 if (this.revisions[revId]) {
216 return this.branchTip(revId);
220 Dvcs.Repository.prototype.lookupRev = function(revId, shouldResolve) {
221 var candidate = this.revisions[revId];
222 if (!candidate && (shouldResolve !== false)) {
223 // shouldResolve is an optional parameter, hence the odd test in the line above
224 candidate = this.revisions[this.branchTip(revId)];
234 additionalParent: null };
237 Dvcs.Repository.prototype.getMetadata = function(revId, shouldResolve) {
238 return this.lookupRev(revId, shouldResolve).metadata;
241 Dvcs.Repository.prototype.getBody = function(revRecord, aliveInodeId) {
242 var bodyId = revRecord.alive[aliveInodeId];
243 if (!bodyId) return {};
244 return Dvcs.Util.deepCopy(this.bodies[bodyId]);
247 Dvcs.Repository.prototype.update = function(unresolvedRevId) {
248 var revId = this.resolveRevId(unresolvedRevId);
249 var rev = this.revisions[revId];
251 if (unresolvedRevId === null) {
252 // meaning "default branch". We only get here if the user
253 // asked for the default branch and there are currently no
254 // commits at all in the repo. Hand back an empty
256 return new Dvcs.Checkout(null, null, null);
258 // Couldn't find what the user asked for.
263 var fs = new Dvcs.Checkout(revId, null, rev.branch);
264 for (var inode in rev.alive) {
265 fs.inodes[inode] = this.getBody(rev, inode);
270 Dvcs.Repository.prototype.commit = function(fs, metadata) {
275 var directParentRev = this.lookupRev(fs.directParent);
276 var additionalParentRev = this.lookupRev(fs.additionalParent);
278 var oldAlive = Dvcs.Util.dict_union(directParentRev.alive, additionalParentRev.alive);
279 var oldDead = Dvcs.Util.dict_union(directParentRev.dead, additionalParentRev.dead);
283 for (var inodeId in fs.inodes) {
284 if (fs.dirty[inodeId]) {
285 var newBodyId = Dvcs.Util.random_uuid();
286 if (Dvcs._debugMode) { newBodyId = "body-" + newBodyId; }
287 this.bodies[newBodyId] = Dvcs.Util.deepCopy(fs.inodes[inodeId]);
288 newAlive[inodeId] = newBodyId;
289 newChanged.push(inodeId);
291 newAlive[inodeId] = oldAlive[inodeId];
295 var newDead = Dvcs.Util.dict_to_set(Dvcs.Util.dict_union(oldDead,
296 Dvcs.Util.dict_difference(oldAlive,
299 var rev = { alive: newAlive,
302 branch: fs.getBranch(),
303 timestamp: (new Date()).getTime(),
305 directParent: fs.directParent,
306 additionalParent: fs.additionalParent };
308 var newRevId = Dvcs.Util.random_uuid();
309 if (Dvcs._debugMode) { newRevId = "rev-" + newRevId; }
310 this.recordRevision(newRevId, rev);
312 fs.directParent = newRevId;
313 fs.additionalParent = null;
320 Dvcs.Repository.prototype.lookupParents = function (revId) {
321 var r = this.lookupRev(revId);
323 if (r.directParent) result.push(r.directParent);
324 if (r.additionalParent) result.push(r.additionalParent);
328 Dvcs.Repository.prototype.lookupChildren = function (revId) {
329 return this.children[revId] || [];
332 Dvcs.Repository.prototype.canMerge = function(r1, r2) {
334 function lookupParents(revId) { return $elf.lookupParents(revId); }
335 var ancestorRevId = Graph.least_common_ancestor(lookupParents, r1, r2);
336 return !(r1 == ancestorRevId || r2 == ancestorRevId);
339 Dvcs.Repository.prototype.merge = function(r1, r2) {
340 var rev1 = this.lookupRev(r1);
341 var rev2 = this.lookupRev(r2);
344 function lookupParents(revId) { return $elf.lookupParents(revId); }
346 var ancestorRevId = Graph.least_common_ancestor(lookupParents, r1, r2);
347 var ancestorRev = this.lookupRev(ancestorRevId, false);
349 if (r1 == ancestorRevId || r2 == ancestorRevId) {
353 var fs = this.update(r1);
354 fs.additionalParent = r2;
359 for (var deadInode in rev2.dead) {
360 fs.deleteFile(deadInode);
362 for (var aliveInode in rev2.alive) {
363 if (fs.fileExists(aliveInode)) {
364 if (ancestorRev.alive[aliveInode] != rev1.alive[aliveInode] ||
365 ancestorRev.alive[aliveInode] != rev2.alive[aliveInode])
367 // It has a different body from the ancestor in one or
368 // both of the revs being merged.
369 var body0 = this.getBody(ancestorRev, aliveInode);
370 var body1 = fs.inodes[aliveInode];
371 var body2 = this.getBody(rev2, aliveInode);
372 this.mergeBodies(body1, body0, body2,
373 function (mergedBody) {
374 fs.inodes[aliveInode] = mergedBody;
375 fs.setDirty(aliveInode);
377 function (partialResult, conflictDetails) {
378 conflicts.push({inode: aliveInode,
379 partialResult: partialResult,
380 conflictDetails: conflictDetails});
383 // It is unchanged. Leave it alone.
385 } else if (!rev1.dead[aliveInode]) {
386 fs.inodes[aliveInode] = this.getBody(rev2, aliveInode);
387 fs.setDirty(aliveInode);
391 return {files: fs, conflicts: conflicts, ancestor: ancestorRevId};
394 Dvcs.Repository.prototype.lookupMerger = function(prop) {
395 return Dvcs.Mergers.Defaults[prop] || Dvcs.Mergers.simpleScalarMerger;
398 Dvcs.Repository.prototype.mergeBodies = function(bThis, bBase, bOther, kSuccess, kConflict) {
399 var props = Dvcs.Util.dict_union(bThis, bOther);
402 var haveConflicts = false;
403 for (var prop in props) {
404 var merger = this.lookupMerger(prop);
405 var mergedPropValue = merger(bThis[prop], bBase[prop], bOther[prop]);
406 if (mergedPropValue.length == 1 && mergedPropValue[0].ok) {
407 bResult[prop] = mergedPropValue[0].ok;
409 failures[prop] = mergedPropValue;
410 haveConflicts = true;
415 return kConflict(bResult, failures);
417 return kSuccess(bResult);
421 Dvcs.Repository.prototype.recordRevision = function(newRevId, rev) {
423 function addChild(parentId) {
424 if (parentId === null) return;
425 if (!$elf.children[parentId]) {
426 $elf.children[parentId] = [newRevId];
428 $elf.children[parentId].push(newRevId);
431 this.revisions[newRevId] = rev;
432 addChild(rev.directParent);
433 addChild(rev.additionalParent);
436 Dvcs.Repository.prototype.exportRevisions = function(revIds) {
439 for (var i = 0; i < revIds; i++) {
440 var rev = this.revisions[revIds[i]];
441 if (rev) revs[revIds[i]] = rev;
445 for (var revId in revs) {
446 var alive = revs[revId].alive;
447 for (var inodeId in alive) {
448 var bodyId = alive[inodeId];
449 bodies[bodyId] = this.bodies[bodyId];
453 return {revisions: revs, bodies: bodies};
455 // Shortcut for all revisions. Be warned: shares structure!
456 return {revisions: this.revisions, bodies: this.bodies};
460 Dvcs.Repository.prototype.importRevisions = function(e) {
467 for (var bodyId in e.bodies) {
468 if (!this.bodies[bodyId]) {
469 this.bodies[bodyId] = e.bodies[bodyId];
475 for (var revId in e.revisions) {
476 if (!this.revisions[revId]) {
477 this.recordRevision(revId, e.revisions[revId]);
486 Dvcs.Repository.prototype.allRevisions = function() {
487 return Dvcs.Util.dict_to_set(this.revisions);
490 Dvcs.Repository.prototype.branchHeads = function(branch) {
492 for (var revId in this.revisions) {
493 var rev = this.revisions[revId];
494 if (rev.branch == branch) {
495 var hasChildrenWithinBranch = false;
496 var kids = this.children[revId] || [];
497 for (var i = 0; i < kids.length; i++) {
498 if (this.revisions[kids[i]].branch == branch) {
499 hasChildrenWithinBranch = true;
503 if (!hasChildrenWithinBranch) {
511 Dvcs.Repository.prototype.branchTip = function(branch) {
512 var newestHead = null;
513 var newestRev = null;
514 var branchHeads = this.branchHeads(branch);
515 for (var i = 0; i < branchHeads.length; i++) {
516 var id = branchHeads[i];
517 var rev = this.lookupRev(id);
518 if (newestHead === null || newestRev.timestamp < rev.timestamp) {
526 Dvcs.Repository.prototype.allBranches = function() {
528 for (var revId in this.revisions) {
529 var rev = this.revisions[revId];
530 var branch = rev.branch;
531 var branchRecord = branches[branch];
533 branchRecord = { active: false, heads: [] };
534 branches[branch] = branchRecord;
537 var hasChildrenWithinBranch = false;
538 var kids = this.children[revId] || [];
539 for (var i = 0; i < kids.length; i++) {
540 if (this.revisions[kids[i]].branch == branch) {
541 hasChildrenWithinBranch = true;
545 if (!hasChildrenWithinBranch) {
546 branchRecord.heads.push(revId);
547 if (kids.length === 0) {
548 branchRecord.active = true;
555 Dvcs.Repository.prototype.childlessRevisions = function() {
557 for (var revId in this.revisions) {
558 var kids = this.children[revId] || [];
559 if (kids.length === 0) {
563 var revs = this.revisions;
564 result.sort(function (r1, r2) { return revs[r2].timestamp - revs[r1].timestamp; });
568 Dvcs.Repository.prototype.fileRevisions = function(uuid) {
570 for (var revId in this.revisions) {
571 var rev = this.revisions[revId];
572 for (var i = rev.changed.length - 1; i >= 0; i--) {
573 if (uuid == rev.changed[i]) {