fs.js
author Tony Garnock-Jones <tonyg@kcbbs.gen.nz>
Mon Dec 21 23:52:38 2009 +0000 (2 years ago)
changeset 123 bc4c90cbd778
parent 72475e25cfa477
permissions -rw-r--r--
For OS X
     1 // Copyright (c) 2008 Tony Garnock-Jones <tonyg@lshift.net>
     2 // Copyright (c) 2008 LShift Ltd. <query@lshift.net>
     3 //
     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:
    11 //
    12 // The above copyright notice and this permission notice shall be
    13 // included in all copies or substantial portions of the Software.
    14 //
    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
    22 // SOFTWARE.
    23 
    24 var Dvcs = {
    25     _debugMode: false,
    26 
    27     Util: {
    28         random_hex_string: function(n) {
    29             var digits = "0123456789abcdef";
    30             var result = "";
    31             for (var i = 0; i < n; i++) {
    32                 result = result + digits[Math.floor(Math.random() * 16)];
    33             }
    34             return result;
    35         },
    36 
    37         random_uuid: function() {
    38             if (Dvcs._debugMode) {
    39                 return Dvcs.Util.random_hex_string(8);
    40             } else {
    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("-");
    47             }
    48         },
    49 
    50         dict_union: function(s1, s2) {
    51             var result = {};
    52             var k;
    53             for (k in s2) { result[k] = s2[k]; }
    54             for (k in s1) { result[k] = s1[k]; }
    55             return result;
    56         },
    57 
    58         dict_difference: function(s1, s2) {
    59             var result = {};
    60             var k;
    61             for (k in s1) { result[k] = s1[k]; }
    62             for (k in s2) { delete result[k]; }
    63             return result;
    64         },
    65 
    66         dict_to_set: function(d) {
    67 	    var result = {};
    68             for (var k in d) { result[k] = 1; }
    69             return result;
    70         },
    71 
    72         deepCopy: function(obj) {
    73             // Courtesy of
    74             // http://keithdevens.com/weblog/archive/2007/Jun/07/javascript.clone
    75             //
    76             // Does not handle recursive structures.
    77 
    78             if (obj === null || typeof(obj) != 'object') {
    79                 return obj;
    80             }
    81 
    82             var temp = obj.constructor();
    83             for (var key in obj) {
    84                 temp[key] = Dvcs.Util.deepCopy(obj[key]);
    85             }
    86             return temp;
    87         }
    88     },
    89 
    90     Mergers: {
    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}}];
    96         },
    97 
    98         simpleTextualMerger: function(v1, v0, v2) {
    99             return Diff.diff3_merge(v1, v0, v2, true);
   100         },
   101 
   102         Defaults: {}
   103     },
   104 
   105     Checkout: function(directParent, additionalParent, currentBranch) {
   106         this.inodes = {};
   107         this.directParent = directParent;
   108         this.additionalParent = additionalParent;
   109         this.dirty = {};
   110 	this.anyDirty = false;
   111         this.currentBranch = currentBranch;
   112     },
   113 
   114     Repository: function() {
   115         this.bodies = {};
   116         this.revisions = {};
   117         this.children = {};
   118     }
   119 };
   120 
   121 Dvcs.Mergers.Defaults["text"] = Dvcs.Mergers.simpleTextualMerger;
   122 
   123 Dvcs.Checkout.prototype.setDirty = function(uuid) {
   124     this.dirty[uuid] = uuid;
   125     this.anyDirty = true;
   126 }
   127 
   128 Dvcs.Checkout.prototype.createFile = function() {
   129     var uuid = Dvcs.Util.random_uuid();
   130     if (Dvcs._debugMode) { uuid = "inode-" + uuid; }
   131     this.inodes[uuid] = {};
   132     this.setDirty(uuid);
   133     return uuid;
   134 };
   135 
   136 Dvcs.Checkout.prototype.deleteFile = function(uuid) {
   137     if (!this.inodes[uuid]) return false;
   138     delete this.inodes[uuid];
   139     this.anyDirty = true;
   140     return true;
   141 };
   142 
   143 Dvcs.Checkout.prototype.fileExists = function(uuid) {
   144     return !!(this.inodes[uuid]);
   145 };
   146 
   147 Dvcs.Checkout.prototype.hasProp = function(uuid, prop) {
   148     var inode = this.inodes[uuid];
   149     if (!inode) return null;
   150     return (inode[prop] !== undefined);
   151 }
   152 
   153 Dvcs.Checkout.prototype.getProp = function(uuid, prop, defaultValue) {
   154     var inode = this.inodes[uuid];
   155     if (!inode) return null;
   156     var v = inode[prop];
   157     if (v === undefined) {
   158 	return defaultValue;
   159     } else {
   160 	return Dvcs.Util.deepCopy(v);
   161     }
   162 };
   163 
   164 Dvcs.Checkout.prototype.setProp = function(uuid, prop, value) {
   165     var inode = this.inodes[uuid];
   166     if (!inode) return false;
   167     inode[prop] = value;
   168     this.setDirty(uuid);
   169     return true;
   170 };
   171 
   172 Dvcs.Checkout.prototype.clearProp = function(uuid, prop) {
   173     var inode = this.inodes[uuid];
   174     if (!inode) return null;
   175     if (inode[prop] !== undefined) {
   176 	delete inode[prop];
   177 	this.setDirty(uuid);
   178     }
   179     return true;
   180 }
   181 
   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]);
   187     }
   188 }
   189 
   190 Dvcs.Checkout.prototype.forEachFile = function(f) {
   191     for (var uuid in this.inodes) {
   192 	f(uuid);
   193     }
   194 };
   195 
   196 Dvcs.Checkout.prototype.getBranch = function() {
   197     return this.currentBranch;
   198 };
   199 
   200 Dvcs.Checkout.prototype.setBranch = function(newBranch) {
   201     this.currentBranch = newBranch;
   202 };
   203 
   204 Dvcs.Checkout.prototype.clone = function() {
   205     var result = new Dvcs.Checkout(this.directParent,
   206                                    this.additionalParent,
   207                                    this.currentBranch);
   208     result.inodes = Dvcs.Util.deepCopy(this.inodes);
   209     return result;
   210 };
   211 
   212 Dvcs.Repository.prototype.resolveRevId = function(revId) {
   213     if (this.revisions[revId]) {
   214         return revId;
   215     } else {
   216         return this.branchTip(revId);
   217     }
   218 };
   219 
   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)];
   225     }
   226     return candidate
   227         || { alive: {},
   228              dead: {},
   229              changed: [],
   230              branch: null,
   231              timestamp: 0,
   232              metadata: null, 
   233              directParent: null,
   234              additionalParent: null };
   235 };
   236 
   237 Dvcs.Repository.prototype.getMetadata = function(revId, shouldResolve) {
   238     return this.lookupRev(revId, shouldResolve).metadata;
   239 };
   240 
   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]);
   245 };
   246 
   247 Dvcs.Repository.prototype.update = function(unresolvedRevId) {
   248     var revId = this.resolveRevId(unresolvedRevId);
   249     var rev = this.revisions[revId];
   250     if (!rev) {
   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
   255             // checkout.
   256             return new Dvcs.Checkout(null, null, null);
   257         } else {
   258             // Couldn't find what the user asked for.
   259             return null;
   260         }
   261     }
   262 
   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);
   266     }
   267     return fs;
   268 };
   269 
   270 Dvcs.Repository.prototype.commit = function(fs, metadata) {
   271     if (!fs.anyDirty) {
   272 	return null;
   273     }
   274 
   275     var directParentRev = this.lookupRev(fs.directParent);
   276     var additionalParentRev = this.lookupRev(fs.additionalParent);
   277 
   278     var oldAlive = Dvcs.Util.dict_union(directParentRev.alive, additionalParentRev.alive);
   279     var oldDead = Dvcs.Util.dict_union(directParentRev.dead, additionalParentRev.dead);
   280 
   281     var newChanged = [];
   282     var newAlive = {};
   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);
   290         } else {
   291             newAlive[inodeId] = oldAlive[inodeId];
   292         }
   293     }
   294 
   295     var newDead = Dvcs.Util.dict_to_set(Dvcs.Util.dict_union(oldDead,
   296                                                              Dvcs.Util.dict_difference(oldAlive,
   297                                                                                        newAlive)));
   298 
   299     var rev = { alive: newAlive,
   300                 dead: newDead,
   301                 changed: newChanged,
   302                 branch: fs.getBranch(),
   303                 timestamp: (new Date()).getTime(),
   304                 metadata: metadata,
   305                 directParent: fs.directParent,
   306                 additionalParent: fs.additionalParent };
   307 
   308     var newRevId = Dvcs.Util.random_uuid();
   309     if (Dvcs._debugMode) { newRevId = "rev-" + newRevId; }
   310     this.recordRevision(newRevId, rev);
   311 
   312     fs.directParent = newRevId;
   313     fs.additionalParent = null;
   314     fs.dirty = {};
   315     fs.anyDirty = false;
   316 
   317     return newRevId;
   318 };
   319 
   320 Dvcs.Repository.prototype.lookupParents = function (revId) {
   321     var r = this.lookupRev(revId);
   322     var result = [];
   323     if (r.directParent) result.push(r.directParent);
   324     if (r.additionalParent) result.push(r.additionalParent);
   325     return result;
   326 };
   327 
   328 Dvcs.Repository.prototype.lookupChildren = function (revId) {
   329     return this.children[revId] || [];
   330 };
   331 
   332 Dvcs.Repository.prototype.canMerge = function(r1, r2) {
   333     var $elf = this;
   334     function lookupParents(revId) { return $elf.lookupParents(revId); }
   335     var ancestorRevId = Graph.least_common_ancestor(lookupParents, r1, r2);
   336     return !(r1 == ancestorRevId || r2 == ancestorRevId);
   337 }
   338 
   339 Dvcs.Repository.prototype.merge = function(r1, r2) {
   340     var rev1 = this.lookupRev(r1);
   341     var rev2 = this.lookupRev(r2);
   342 
   343     var $elf = this;
   344     function lookupParents(revId) { return $elf.lookupParents(revId); }
   345 
   346     var ancestorRevId = Graph.least_common_ancestor(lookupParents, r1, r2);
   347     var ancestorRev = this.lookupRev(ancestorRevId, false);
   348 
   349     if (r1 == ancestorRevId || r2 == ancestorRevId) {
   350 	return null;
   351     }
   352 
   353     var fs = this.update(r1);
   354     fs.additionalParent = r2;
   355     fs.anyDirty = true;
   356 
   357     var conflicts = [];
   358 
   359     for (var deadInode in rev2.dead) {
   360         fs.deleteFile(deadInode);
   361     }
   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])
   366             {
   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);
   376                                  },
   377                                  function (partialResult, conflictDetails) {
   378                                      conflicts.push({inode: aliveInode,
   379                                                      partialResult: partialResult,
   380                                                      conflictDetails: conflictDetails});
   381                                  });
   382             } else {
   383                 // It is unchanged. Leave it alone.
   384             }
   385         } else if (!rev1.dead[aliveInode]) {
   386             fs.inodes[aliveInode] = this.getBody(rev2, aliveInode);
   387 	    fs.setDirty(aliveInode);
   388         }
   389     }
   390 
   391     return {files: fs, conflicts: conflicts, ancestor: ancestorRevId};
   392 };
   393 
   394 Dvcs.Repository.prototype.lookupMerger = function(prop) {
   395     return Dvcs.Mergers.Defaults[prop] || Dvcs.Mergers.simpleScalarMerger;
   396 };
   397 
   398 Dvcs.Repository.prototype.mergeBodies = function(bThis, bBase, bOther, kSuccess, kConflict) {
   399     var props = Dvcs.Util.dict_union(bThis, bOther);
   400     var bResult = {};
   401     var failures = {};
   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;
   408         } else {
   409             failures[prop] = mergedPropValue;
   410             haveConflicts = true;
   411         }
   412     }
   413 
   414     if (haveConflicts) {
   415         return kConflict(bResult, failures);
   416     } else {
   417         return kSuccess(bResult);
   418     }
   419 };
   420 
   421 Dvcs.Repository.prototype.recordRevision = function(newRevId, rev) {
   422     var $elf = this;
   423     function addChild(parentId) {
   424         if (parentId === null) return;
   425         if (!$elf.children[parentId]) {
   426             $elf.children[parentId] = [newRevId];
   427         } else {
   428             $elf.children[parentId].push(newRevId);
   429         }
   430     }
   431     this.revisions[newRevId] = rev;
   432     addChild(rev.directParent);
   433     addChild(rev.additionalParent);
   434 };
   435 
   436 Dvcs.Repository.prototype.exportRevisions = function(revIds) {
   437     if (revIds) {
   438         var revs = {};
   439         for (var i = 0; i < revIds; i++) {
   440             var rev = this.revisions[revIds[i]];
   441             if (rev) revs[revIds[i]] = rev;
   442         }
   443 
   444         var bodies = {};
   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];
   450             }
   451         }
   452 
   453         return {revisions: revs, bodies: bodies};
   454     } else {
   455         // Shortcut for all revisions. Be warned: shares structure!
   456         return {revisions: this.revisions, bodies: this.bodies};
   457     }
   458 };
   459 
   460 Dvcs.Repository.prototype.importRevisions = function(e) {
   461     var stats = {
   462 	bodyCount: 0,
   463 	bodyDups: 0,
   464 	revCount: 0,
   465 	revDups: 0
   466     };
   467     for (var bodyId in e.bodies) {
   468 	if (!this.bodies[bodyId]) {
   469             this.bodies[bodyId] = e.bodies[bodyId];
   470 	    stats.bodyCount++;
   471 	} else {
   472 	    stats.bodyDups++;
   473 	}
   474     }
   475     for (var revId in e.revisions) {
   476 	if (!this.revisions[revId]) {
   477             this.recordRevision(revId, e.revisions[revId]);
   478 	    stats.revCount++;
   479 	} else {
   480 	    stats.revDups++;
   481 	}
   482     }
   483     return stats;
   484 };
   485 
   486 Dvcs.Repository.prototype.allRevisions = function() {
   487     return Dvcs.Util.dict_to_set(this.revisions);
   488 };
   489 
   490 Dvcs.Repository.prototype.branchHeads = function(branch) {
   491     var result = [];
   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;
   500                     break;
   501                 }
   502             }
   503             if (!hasChildrenWithinBranch) {
   504                 result.push(revId);
   505             }
   506         }
   507     }
   508     return result;
   509 };
   510 
   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) {
   519             newestHead = id;
   520             newestRev = rev;
   521         }
   522     }
   523     return newestHead;
   524 };
   525 
   526 Dvcs.Repository.prototype.allBranches = function() {
   527     var branches = {};
   528     for (var revId in this.revisions) {
   529         var rev = this.revisions[revId];
   530         var branch = rev.branch;
   531         var branchRecord = branches[branch];
   532         if (!branchRecord) {
   533             branchRecord = { active: false, heads: [] };
   534             branches[branch] = branchRecord;
   535         }
   536 
   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;
   542                 break;
   543             }
   544         }
   545         if (!hasChildrenWithinBranch) {
   546             branchRecord.heads.push(revId);
   547             if (kids.length === 0) {
   548                 branchRecord.active = true;
   549             }
   550         }
   551     }
   552     return branches;
   553 };
   554 
   555 Dvcs.Repository.prototype.childlessRevisions = function() {
   556     var result = [];
   557     for (var revId in this.revisions) {
   558         var kids = this.children[revId] || [];
   559         if (kids.length === 0) {
   560             result.push(revId);
   561         }
   562     }
   563     var revs = this.revisions;
   564     result.sort(function (r1, r2) { return revs[r2].timestamp - revs[r1].timestamp; });
   565     return result;
   566 };
   567 
   568 Dvcs.Repository.prototype.fileRevisions = function(uuid) {
   569     var result = {};
   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]) {
   574                 result[revId] = rev;
   575                 break;
   576             }
   577         }
   578     }
   579     return result;
   580 };