Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

cli - snob is now self hosting

  • Loading branch information...
commit cf293e94bf09f7d2eaece7463c2fe8d888ec7514 1 parent 9fc30eb
@dominictarr authored
View
BIN  .readme.markdown.swp
Binary file not shown
View
2  .snob/commits
@@ -0,0 +1,2 @@
+
+{"parent":null,"changes":{"cli.js":[[0,0,"var fs = require('fs')","var join = require('path').join","var Repo = require('./snob')","","// just for a joke, lets add a CLI so that snob can be self hosting.","","/*"," COMMANDS",""," init // create a repo and persist it."," save commits in an append only log"," save state in a json file"," just needs branches,"," and current checkout commit"," commit file... save the current file in a new commit"," checkout commitish (commitish = commit/tag/branch)"," tag name commitish"," merge commitish1 commitish2 || current_branch "," branch branchname"," whereami show current branch","","*/","","function Snob (dir) {"," this.dir = dir"," this.repo = new Repo()","}","","Snob.prototype = {"," read: function (file, ready) {"," fs.readFile(join(this.dir, file), 'utf-8', ready)"," },"," load: function (callback) {"," var repo = this.repo, self = this"," var n = 2, err"," function ready (e) {"," err = err || e "," if(--n) return"," callback(err)"," }",""," fs.readFile(join(this.dir, '.snob', 'commits'), 'utf-8',"," function (err, commits) {"," commits.split('\\n').map(function (e) {"," if(!e) return"," var commit = JSON.parse(e)"," repo.commits[commit.id] = commit"," })"," ready(err)"," })",""," fs.readFile(join(this.dir, '.snob', 'state'), 'utf-8',"," function (err, data) {"," var state = JSON.parse(data)"," repo.branches = state.branches || {}"," repo.tags = state.tags || {}"," self.current = state.current "," console.log(state)"," ready(err)"," })"," },"," save: function (commit, callback) {"," var n = 2, err"," function ready (e) {"," err = err || e"," if(!--n) return"," callback(err)"," }"," append(join(this.dir, '.snob', 'commits')"," , JSON.stringify(commit) + '\\n', ready)",""," fs.writeFile(join(this.dir, '.snob', 'state')"," , JSON.stringify({"," branches: this.repo.branches, "," tags: this.repo.tags,"," current: this.current || 'master' ,"," }), ready)"," }","}","","function append (file, text, callback) {"," var s = fs.createWriteStream(file, {flags: 'a'})"," s.on('end', callback)"," s.on('error', callback)"," s.write(text)"," s.end()","}","","function findDir (dir) {"," if(dir === '/')"," throw new Error('is not a SNOB repo')"," dir = dir || process.cwd()"," var dotsnob = join(dir, '.snob')"," try {"," fs.readdirSync(dotsnob)"," return new Snob(dir)"," } catch (err) {"," throw err // currently must use snob from root of repo"," if(err.code === 'ENOENT')"," return findDir(join(dir, '..'))"," }","}","","var commands = {"," init: function (dir) {"," dir = dir || process.cwd()"," var dotsnob = join(dir, '.snob')"," try {"," fs.readdirSync(dotsnob) "," } catch (err) {"," //"," if(err.code === 'ENOENT') {"," // create "," fs.mkdirSync(dotsnob)"," fs.writeFileSync(join(dotsnob, 'commits'), '')"," fs.writeFileSync(join(dotsnob, 'state'), "," JSON.stringify({current: 'master'})"," )"," }"," }"," },"," commit: function () {"," var state = findDir()"," var files = [].slice.call(arguments)"," var n = files.length + 1// extra one is the repo commits file"," var repo = state.repo"," state.load (done)"," if(!files.length)"," throw new Error('expected args: files to commit')"," console.log(files)"," //read each file, and "," var world = {}",""," files.map(function (f) {"," state.read(f, function (err, text) {"," world[f] = text.split('\\n')"," done(err)"," })"," })",""," function done (err, text) {"," if(err) throw err"," if(--n) return"," var commit = repo.commit(world, {parent: state.current || 'master'})"," console.log(commit)"," state.save(commit, console.log) "," }"," },"," heads: function () {"," var state = findDir()"," state.load(function () {"," console.log(state.repo.heads())"," })"," },"," branch: function () {"," var state = findDir()"," state.load(function (err) {"," if(err) throw err"," console.log(state.repo.branches, 'current:', state.current)"," }) "," },"," log: function () {"," var state = findDir()"," state.load(function () {"," state.repo.revlist('master') // add changing branch"," .map(function (id) {"," "," var commit = state.repo.get(id)"," console.log(commit.id, commit.parent, new Date(commit.timestamp))"," })"," })"," }","}","","var args = process.argv","args.splice(0, 2)","","console.log(args)","var cmd = args.shift()","if(commands[cmd])"," commands[cmd].apply(null, args)",""]],"index.js":[[0,0,"function head (a) {"," return a[0]","}","","function last (a) {"," return a[a.length - 1]","}","","function tail(a) {"," return a.slice(1)","}","","function retreat (e) {"," return e.pop()","}","","function hasLength (e) {"," return e.length","}","","function any(ary, test) {"," for(var i in ary)"," if(test(ary[i]))"," return true"," return false","}","","function equal(a, b) {"," if(a.length != b.length) return false"," for(var i in a)"," if(a[i] !== b[i]) return false"," return true","}","","function getArgs(args) {"," return args.length == 1 ? args[0] : [].slice.call(args)","}","","// return the index of the element not like the others, or -1","function oddElement(ary, cmp) {"," var c"," function guess(a) {"," var odd = -1"," c = 0"," for (var i = a; i < ary.length; i ++) {"," if(!cmp(ary[a], ary[i])) {"," odd = i, c++"," }"," }"," return c > 1 ? -1 : odd"," }"," //assume that it is the first element."," var g = guess(0)"," if(-1 != g) return g"," //0 was the odd one, then all the other elements are equal"," //else there more than one different element"," guess(1)"," return c == 0 ? 0 : -1","}","","module.exports = function (exports, deps) {"," exports = exports || {} "," exports.lcs = "," function lcs() {"," var cache = {}"," var args = getArgs(arguments)"," "," function key (a,b){"," return a.length + ':' + b.length"," }",""," function recurse (a, b) {",""," if(!a.length || !b.length) return []"," //avoid exponential time by caching the results"," if(cache[key(a, b)]) return cache[key(a, b)]",""," if(a[0] == b[0])"," return [head(a)].concat(recurse(tail(a), tail(b)))"," else { "," var _a = recurse(tail(a), b)"," var _b = recurse(a, tail(b))"," return cache[key(a,b)] = _a.length > _b.length ? _a : _b "," }"," }",""," if(args.length > 2) {"," //if called with multiple sequences"," //recurse, since lcs(a, b, c, d) == lcs(lcs(a,b), lcs(c,d))"," args.push(lcs(args.shift(), args.shift()))"," return lcs(args)"," }"," return recurse(args[0], args[1])"," }",""," // given n sequences, calc the lcs, and then chunk strings into stable and unstable sections."," // unstable chunks are passed to build"," exports.chunk ="," function (q, build) {"," var q = q.map(function (e) { return e.slice() })"," var lcs = exports.lcs.apply(null, q)"," var all = [lcs].concat(q)",""," function matchLcs (e) {"," return last(e) == last(lcs) || ((e.length + lcs.length) === 0)"," }",""," while(any(q, hasLength)) {"," //if each element is at the lcs then this chunk is stable."," while(q.every(matchLcs) && q.every(hasLength)) "," all.forEach(retreat)",""," //collect the changes in each array upto the next match with the lcs"," var c = false"," var unstable = q.map(function (e) {"," var change = []"," while(!matchLcs(e)) {"," change.unshift(retreat(e))"," c = true"," }"," return change"," })"," if(c) build(q[0].length, unstable)"," }"," }",""," exports.diff ="," function (a, b) {"," var changes = []"," exports.chunk([a, b], function (index, unstable) {"," var del = unstable.shift().length"," var insert = unstable.shift()"," changes.push([index, del].concat(insert))"," })"," return changes"," }",""," exports.patch = function (a, changes, mutate) {"," if(mutate !== true) a = a.slice(a)//copy a"," changes.forEach(function (change) {"," [].splice.apply(a, change)"," })"," return a"," }",""," // http://en.wikipedia.org/wiki/Concestor"," // me, concestor, you..."," exports.merge = function () {"," var args = getArgs(arguments)"," var patch = exports.diff3(args)"," return exports.patch(args[0], patch)"," }",""," exports.diff3 = function () {"," var args = getArgs(arguments)"," var r = []"," exports.chunk(args, function (index, unstable) {"," var mine = unstable[0]"," var insert = resolve(unstable)"," if(equal(mine, insert)) return "," r.push([index, mine.length].concat(insert)) "," })"," return r"," }",""," var rules = exports.rules = ["," function oddOneOut (changes) {"," changes = changes.slice()"," //put the concestor first"," changes.unshift(changes.splice(1,1)[0])"," var i = oddElement(changes, equal)"," if(i == 0) // concestor was different, 'false conflict'"," return changes[1]"," if (~i)"," return changes[i] "," },"," //i've implemented this as a seperate rule,"," //because I had second thoughts about this."," function insertMergeOverDelete (changes) {"," changes = changes.slice()"," changes.splice(1,1)// remove concestor"," "," //if there is only one non empty change thats okay."," //else full confilct"," for (var i = 0, nonempty; i < changes.length; i++)"," if(changes[i].length) "," if(!nonempty) nonempty = changes[i]"," else return // full conflict"," return nonempty"," }"," ]",""," function resolve (changes) {"," var l = rules.length"," for (var i in rules) { // first"," var c = rules[i](changes)"," if(c) return c"," }"," changes.splice(1,1) // remove concestor"," return {'?': changes}"," }","}","module.exports(module.exports, {equal: equal})",""]],"snob.js":[[0,0,"var a = require('./index')","// reimplementing git, because I'm insane.","","function Repository () {"," this.commits = {}"," this.branches = {}"," this.tags = {}","}","","function map(obj, itr) {"," var r = {}"," for (var i in obj)"," r[i] = itr(obj[i], i, obj)"," return r","}","","function copy(obj) {"," return map(obj, function(e) {return e})","}","function keys (obj) {"," var ks = []"," for (var k in obj)"," ks.push(k)"," return ks","}","","var createHash = require('crypto').createHash // make this injectable...","function hash (obj) {"," return createHash('sha').update(JSON.stringify(obj)).digest('hex')","}","","Repository.prototype = {"," commit: function (world, meta) {"," //meta is author, message, parent commit"," //this is the current state of the repo."," //commit will diff it with the head of the given branch "," //and then save that diff in the commit list.",""," // head = checkout (branch)"," // diff(head, world)"," // bundle with meta, add to commits "," var branch = meta.parent //save the branch name"," meta.parent = this.getId(branch) "," var commit = copy(meta) // filter correct attributs only?"," commit.changes = this.diff(meta.parent, world)"," commit.depth = (this.commits[meta.parent] || {depth: 0}).depth + 1"," commit.timestamp = Date.now()"," commit.id = hash(commit)",""," // XXX make an error if the commits are empty !!! XXX ",""," this.commits[commit.id] = commit"," this.branch(branch, commit.id)"," console.log('new commit',branch, commit.id, this.getId(commit.id))"," return commit"," // emit the new commit "," },"," get: function (commitish) {"," return this.commits[commitish] || this.commits[this.branches[commitish] || this.tags[commitish]]"," },"," getId: function (commitish) {"," return (this.get(commitish) || {id:null}).id "," },"," tag: function (name, commitish) {"," this.tags[name] = this.getId(commitish) "," },"," branch: function (name, commitish) {"," // do not save this as a branch if it's actually a commit, or a tag."," if(this.commits[name] || this.tags[name]) {"," console.log('!!!!!!!!!!!!!!!!!!!!!')"," return this.getId(commitish)"," }"," console.log('BRANCH', name, commitish)"," return this.branches[name] = this.getId(commitish)"," },"," diff: function (parent, world) {"," var head = this.checkout(parent)"," return map(world, function (b, f) {"," return a.diff(head[f] || [], b)"," })"," },"," revlist: function (id) {"," var commit = this.get(id)"," if(!commit) return []"," return this.revlist(commit.parent).concat(id)"," },"," concestor: function (heads) { //a list of commits you want to merge"," if(arguments.length > 1)"," heads = [].slice.call(arguments)"," // find the concestor of the heads"," // this is the only interesting problem left!"," // get the revlist of the first head"," // recurse down from each head, looking for the last index of that item."," // chop the tail when you find something, and move to the next head."," // the concestor(a, b, c) must equal concestor(concestor(a, b), c)"," var getId = this.getId.bind(this)"," heads = heads.map(getId)"," var first = heads.shift()"," var revlist = this.revlist(first)"," var commits = this.commits"," var l = -1"," function last (a) {"," return a[a.length - 1]"," }"," function find (h) {"," var i = revlist.lastIndexOf(h, ~l ? l : null)"," if(i !== -1) l = i"," else find(commits[h].parent)"," }"," while(heads.length)"," find(heads.shift())"," return revlist[l]"," },"," merge: function (branches, meta) { //branches..."," // TODO, the interesting problem here is to handle async conflict resolution."," // hmm, maybe just mark the conflicts but do not update the branch?"," // afterall, this isn't really for SCM, the usecases are usally gonna take automatic resolves."," "," // find the concestor of the branches,"," // then calculate an n-way merge of all the checkouts."," var mine = branches[0]"," var concestor = this.concestor(branches)"," branches.splice(1, 0, concestor)"," var self = this"," var commit = copy(meta)"," var checkouts = branches.map(function (e) {"," return self.checkout(e)"," })"," commit.changes = map(checkouts[1], function (obj, key) {"," var collect = checkouts.map(function (e) {"," return e[key]"," })"," return a.diff3(collect)"," })"," //TODO build the commit, and stick it in."," "," commit.merged = branches"," commit.parent = branches[0]"," commit.depth = this.commits[branches[0]].depth + 1"," commit.timestamp = Date.now()",""," commit.id = hash(commit)",""," this.commits[commit.id] = commit"," this.branch(mine, commit.id) // if this was merge( ['master', ...], ...) update the branch"," return commit"," },"," checkout: function (commitish) {"," if(commitish == null)"," return {}"," var commit = this.get(commitish)"," var state = this.checkout(commit.parent)"," return map(commit.changes, function (change, key) {"," return a.patch(state[key] || [], change)"," })"," },"," heads: function () {"," var heads = {}"," heads = copy(this.commits)"," for (var k in this.commits)"," delete heads[this.commits[k].parent] "," return heads "," //return commits that have no children"," }","}","","module.exports = Repository",""]],"readme.markdown":[[0,0,"# snob","","snob is a version control system written in js. consider it a a minimial port of git.","one of the best uses is to learn you exactly how git works. ","","## how git works.","","if you want to know what is the difference between two files, you must first know what is the same.","this is called the Longest Common Subsequence problem. if you have two sequences `x = \"ABDCEF\" and `y = \"ABCXYZF\"` then `LCS(x,y)` is clearly \"ABCF\".","","","## lcs","","```","function lcs (a,b)"," if head(a) == head(b)"," then lcs(a,b) = head(a) + lcs(tail(a), tail(b))"," else lcs(a, b) = max(lcs(tail(a),b), lcs(a, tail(b)))","```","","(where max returns the longer list, head return the first element, and tail returns the rest of the sequence minus the head)","","this is very simple, but with exponential time complexity.","however, it can easily be made sufficantly performant by cacheing the return value of each call to lcs().","","see js implementation, [index.js#L64-94](https://github.com/dominictarr/adiff/blob/master/index.js#L63-94)","","## chunking","","now, we can see when the strings differ, by comparing them to the lcs. the next step is dividing them into 'stable' chunks where they match the lcs, and unstable chunks where they differ.","","basically, to go from `chunk(\"ABDCEF\", \"ABCXYZF\")` to ","`[\"AB\", [\"D\", \"\"], \"C\", [\"E\", \"XYZ\"], \"F\"]`","","note that stable and unstable chunks always alternate.","","basically, you iterate over the sequences and while the heads match the head of the lcs, shift that value to a stable chunk.","then, while the heads do not match the next head of the lcs,","collect add those items into an unstable chunk.","","## diff","","once you have the chunks getting a list of changes that you can apply is easy...","","making a diff from a to b we want to know what changes to make to a to get b. ","the way I have node this [Array#splice](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice)","so, for `[\"AB\", [\"D\", \"\"], \"C\", [\"E\", \"XYZ\"], \"F\"]`we want:","","``` js"," var changes = ["," [4, 1, 'X', 'Y', 'Z'], //delete 1 item (\"E\") at index 4, then insert \"X\", \"Y\", \"Z\""," [2, 1] //delete 1 item at index 2 (\"D\")"," ]","``` ","","note, you can apply changes to the end of the array without altering the indexes in the start of the array.","","this makes the function to apply the patch _very_ simple","","## patch","","``` js"," function patch (orig, changes) {"," var ary = orig.split('') //assuming that orig is just a string"," changes.forEach(function (ch) {"," [].splice.apply(ary, ch)"," })"," return ary.join('')"," }","```","","## diff3"," ","if we want a _distributed_ version management system, the we need to be able to make changes in parallel.","this is only a slightly more complicated problem. given a string `\"ABDCEF\"`, If I changed it to `\"ABCXYZF\"`","and meanwhile you changed it to \"AXBCEFG\". we must compare each of our changes to the original string, the [Concestor](http://en.wikipedia.org/wiki/Concestor)","","TODO: worked example with chunks, resolve.","","","## what next?","","refactor snob out of adiff. ","still thinking about how I will manage diffs on objects,"," - refactor vu, with what I know now.","giving snob a cli, and in particular, push/pullable remotes, is the first step to being distributed.",""," - improve support for injection with merge resolution."," - get running in the browser"," - make Repository an EventEmitter",""]],"test/snob-test-branches.js":[[0,0,"","var assert = require('assert')","var Repo = require('../snob')","","var snob = new Repo()","var world","var init = snob.commit(world = {"," hello: ['who', 'what', 'when','why']"," },"," { message: 'init', parent: 'master'})","","assert.equal(init.depth, 1)","","assert.equal(snob.getId('master'), init.id)","","snob.branch('underdog', init.id)","","var _world = snob.checkout(init.id)","","assert.deepEqual(_world, world)","","world.hello.splice(2, 0, 'how')","","var second = snob.commit(world, {"," message: 'second',"," parent: 'master'"," })","","console.log(second)","console.log(snob.revlist(second.id))","","assert.deepEqual("," snob.revlist(second.id), "," [ init.id, second.id])","","assert.deepEqual(second.depth, 2)","","//a branch off init, so can test merge","","_world.hello.push('WTF!?')","var branch = snob.commit(_world, {"," message: 'branch',"," parent: 'underdog' "," })","","assert.equal(branch.depth, 2)","console.log(branch)","","var concestor = snob.concestor(branch.id, second.id)","","assert.equal(concestor, init.id)","console.log(concestor)","","var merged = snob.merge([branch.id, second.id], {message: 'merge1'})","","console.log(merged)","assert.equal(3, merged.depth)","","var world3 = snob.checkout(merged.id)","","console.log(world3)","","",""]],"test/snob-test.js":[[0,0,"","var assert = require('assert')","var Repo = require('../snob')","","var snob = new Repo()","var world","var init = snob.commit(world = {"," hello: ['who', 'what', 'when','why']"," },"," { message: 'init'})","","assert.equal(init.depth, 1)","console.log(init)","","var _world = snob.checkout(init.id)","","assert.deepEqual(_world, world)","","console.log(_world, world)","world.hello.splice(2, 0, 'how')","var second = snob.commit(world, {"," message: 'second',"," parent: init.id"," })","","console.log(second)","console.log(snob.revlist(second.id))","","assert.deepEqual("," snob.revlist(second.id), "," [ init.id, second.id])","","//a branch off init, so can test merge","","_world.hello.push('WTF!?')","var branch = snob.commit(_world, {"," message: 'branch',"," parent: init.id"," })","","assert.equal(branch.depth, 2)","console.log(branch)","","var concestor = snob.concestor(branch.id, second.id)","","assert.equal(concestor, init.id)","console.log(concestor)","","var merged = snob.merge([branch.id, second.id], {message: 'merge1'})","","console.log(merged)","assert.equal(3, merged.depth)","","var world3 = snob.checkout(merged.id)","","console.log(world3)","","",""]],"package.json":[[0,0,"{"," \"author\": \"Dominic Tarr <dominic.tarr@gmail.com> (dominictarr.com)\","," \"name\": \"adiff\","," \"description\": \"diff and patch arrays.\","," \"version\": \"0.0.0\","," \"homepage\": \"https://github.com/dominictarr/adiff\","," \"repository\": {"," \"type\": \"git\","," \"url\": \"git://github.com/dominictarr/adiff.git\""," },"," \"main\": \"./index.js\","," \"scripts\": {"," \"test\": \"node test.js\""," },"," \"engines\": {"," \"node\": \"*\""," },"," \"dependencies\": {},"," \"devDependencies\": {},"," \"optionalDependencies\": {}","}",""]],"test/test.js":[[0,0,"var d = require('../')","","if(!module.parent) {",""," var assert = require('assert')"," function split(a) {"," if('string' === typeof a)"," return a.split('')"," return a"," }",""," function test (a, b, lcs) {"," a = split(a)"," b = split(b)"," lcs = split(lcs)"," var _lcs = d.lcs(a, b)"," d.chunk([a, b], console.log)"," assert.deepEqual(_lcs, lcs)"," var changes = d.diff(a,b)"," var newA = d.patch(a, changes)"," assert.deepEqual(newA, b)"," }",""," test('AA', 'AA', 'AA')"," test('AB', 'BA', 'A')"," test('ABA', 'BAA', 'AA')"," test('TANYANA', 'BANANA', 'ANANA')"," // the naive model takes 2.5 seconds to find this:"," // time to optimise..."," test('aoenuthooao', 'eukmcybkraoaeuo', 'aoeuo')"," test('aoenuthooaeuoao', 'eukipoimcybkraoaeuo', 'euooaeuo')"," // added caching... now it's way faster.",""," function test3way(args, expected) {"," args = args.map(split)",""," console.log('----- TEST', args)"," console.log('***********')"," var p = d.diff3.apply(null, args)"," var r = d.patch(args[0], p)"," assert.deepEqual(r, split(expected))",""," }",""," // [this, concestor, other], expected"," test3way(['abaaaa','aaaaa', 'aaacaa'], 'abaacaa') // simple change"," test3way(['abaa','aaa', 'aacca'], 'abacca') // simple change"," test3way(['abaaa','aaaaa', 'abaaa'], 'abaaa') // same change aka 'false conflict'"," test3way(['aaaaa','aaccaaa', 'aaccaaba'], 'aaaaba') // simple delete"," // since b is deleted."," test3way(['abaaa','abaaa', 'aacaa'], 'aacaa')"," // delete from middle and add to end."," test3way(['aaa','axaa', 'axaab'], 'aaab') "," test3way(['abaaba','aaaaa', 'aaacca'],"," ['a', 'b', 'a', 'a', {'?': [['b'], ['c','c']]}, 'a'])",""," // in these tests, i've replaced something, but you have just deleted it."," // it makes sense to merge my replace over your delete"," test3way(['aBc', 'abc', 'acD'], 'aBcD')"," test3way(['abaaa', 'aaaa', 'aacca'], 'abaacca')",""," //note, it's possible for this case to occur in a"," //n-way merge where there is a delete and a false conflict."," //most merges will be only 3 ways, so lets leave that for now.",""," console.log(d.diff3(split(\"ABCXYZF\"), split(\"ABDCEF\"), split(\"AXBCEFG\")))","}",""]],"test/tree-test.js":[[0,0,"","var assert = require('assert')","","function listify(tree){"," var a = []"," function cat (b) {"," while(b.length)"," a.push(b.shift())"," }"," if(Array.isArray(tree)) {"," a.push('[')"," "," for (var i in tree) {"," // a.push(i + ':')"," cat(listify(tree[i]))"," a.push(',')"," }"," if(a[a.length - 1 ] == ',') a.pop()"," a.push(']')"," } else if ('object' == typeof tree) {",""," a.push('{')"," "," for (var i in tree) {"," a.push(i + ':')"," cat(listify(tree[i]))"," a.push(',')"," }"," if(a[a.length - 1] == ',') a.pop()"," "," a.push('}')"," } else"," return [JSON.stringify(tree)]"," return a","}","","","var d = require('../')","","assert.deepEqual(listify({hello: {}}), ['{', 'hello:','{', '}','}'])","","diff3("," {hello: {value: 250}, bye: 'ok'}, "," {hello: 250, bye: 'ok'},"," {hello: 250, bye: 'okay'}",")","","diff3 ("," {hello: {whatever: true}, value: null},"," {hello: 'hello', value: {whatever: true}},"," {hello: 'hello', value: {whatever: true, changed: 'YES'}}",")","","","function diff3 () {"," var args = [].slice.call(arguments).map(listify)"," console.log(args)"," var x = d.diff3.apply(null, args)"," console.log('DIFF', x)","}","",""]]},"depth":1,"timestamp":1332058834273,"id":"4f63d2637ae35e9313fd4f23ea6cf4e8e527ba3c"}
View
1  .snob/state
@@ -0,0 +1 @@
+{"branches":{"master":"4f63d2637ae35e9313fd4f23ea6cf4e8e527ba3c"},"tags":{},"current":"master"}
View
181 cli.js
@@ -0,0 +1,181 @@
+var fs = require('fs')
+var join = require('path').join
+var Repo = require('./snob')
+
+// just for a joke, lets add a CLI so that snob can be self hosting.
+
+/*
+ COMMANDS
+
+ init // create a repo and persist it.
+ save commits in an append only log
+ save state in a json file
+ just needs branches,
+ and current checkout commit
+ commit file... save the current file in a new commit
+ checkout commitish (commitish = commit/tag/branch)
+ tag name commitish
+ merge commitish1 commitish2 || current_branch
+ branch branchname
+ whereami show current branch
+
+*/
+
+function Snob (dir) {
+ this.dir = dir
+ this.repo = new Repo()
+}
+
+Snob.prototype = {
+ read: function (file, ready) {
+ fs.readFile(join(this.dir, file), 'utf-8', ready)
+ },
+ load: function (callback) {
+ var repo = this.repo, self = this
+ var n = 2, err
+ function ready (e) {
+ err = err || e
+ if(--n) return
+ callback(err)
+ }
+
+ fs.readFile(join(this.dir, '.snob', 'commits'), 'utf-8',
+ function (err, commits) {
+ commits.split('\n').map(function (e) {
+ if(!e) return
+ var commit = JSON.parse(e)
+ repo.commits[commit.id] = commit
+ })
+ ready(err)
+ })
+
+ fs.readFile(join(this.dir, '.snob', 'state'), 'utf-8',
+ function (err, data) {
+ var state = JSON.parse(data)
+ repo.branches = state.branches || {}
+ repo.tags = state.tags || {}
+ self.current = state.current
+ console.log(state)
+ ready(err)
+ })
+ },
+ save: function (commit, callback) {
+ var n = 2, err
+ function ready (e) {
+ err = err || e
+ if(!--n) return
+ callback(err)
+ }
+ append(join(this.dir, '.snob', 'commits')
+ , JSON.stringify(commit) + '\n', ready)
+
+ fs.writeFile(join(this.dir, '.snob', 'state')
+ , JSON.stringify({
+ branches: this.repo.branches,
+ tags: this.repo.tags,
+ current: this.current || 'master' ,
+ }), ready)
+ }
+}
+
+function append (file, text, callback) {
+ var s = fs.createWriteStream(file, {flags: 'a'})
+ s.on('end', callback)
+ s.on('error', callback)
+ s.write(text)
+ s.end()
+}
+
+function findDir (dir) {
+ if(dir === '/')
+ throw new Error('is not a SNOB repo')
+ dir = dir || process.cwd()
+ var dotsnob = join(dir, '.snob')
+ try {
+ fs.readdirSync(dotsnob)
+ return new Snob(dir)
+ } catch (err) {
+ throw err // currently must use snob from root of repo
+ if(err.code === 'ENOENT')
+ return findDir(join(dir, '..'))
+ }
+}
+
+var commands = {
+ init: function (dir) {
+ dir = dir || process.cwd()
+ var dotsnob = join(dir, '.snob')
+ try {
+ fs.readdirSync(dotsnob)
+ } catch (err) {
+ //
+ if(err.code === 'ENOENT') {
+ // create
+ fs.mkdirSync(dotsnob)
+ fs.writeFileSync(join(dotsnob, 'commits'), '')
+ fs.writeFileSync(join(dotsnob, 'state'),
+ JSON.stringify({current: 'master'})
+ )
+ }
+ }
+ },
+ commit: function () {
+ var state = findDir()
+ var files = [].slice.call(arguments)
+ var n = files.length + 1// extra one is the repo commits file
+ var repo = state.repo
+ state.load (done)
+ if(!files.length)
+ throw new Error('expected args: files to commit')
+ console.log(files)
+ //read each file, and
+ var world = {}
+
+ files.map(function (f) {
+ state.read(f, function (err, text) {
+ world[f] = text.split('\n')
+ done(err)
+ })
+ })
+
+ function done (err, text) {
+ if(err) throw err
+ if(--n) return
+ var commit = repo.commit(world, {parent: state.current || 'master'})
+ console.log(commit)
+ state.save(commit, console.log)
+ }
+ },
+ heads: function () {
+ var state = findDir()
+ state.load(function () {
+ console.log(state.repo.heads())
+ })
+ },
+ branch: function () {
+ var state = findDir()
+ state.load(function (err) {
+ if(err) throw err
+ console.log(state.repo.branches, 'current:', state.current)
+ })
+ },
+ log: function () {
+ var state = findDir()
+ state.load(function () {
+ state.repo.revlist('master') // add changing branch
+ .map(function (id) {
+
+ var commit = state.repo.get(id)
+ console.log(commit.id, commit.parent, new Date(commit.timestamp))
+ })
+ })
+ }
+}
+
+var args = process.argv
+args.splice(0, 2)
+
+console.log(args)
+var cmd = args.shift()
+if(commands[cmd])
+ commands[cmd].apply(null, args)
View
3  notes
@@ -0,0 +1,3 @@
+[ZS97, Cha99, p. 17]. "Even simple variants of the unordered tree problem have been shown to be NP-hard
+ordered tree more computationally feasible, apparently!
+
View
23 readme.markdown
@@ -8,7 +8,6 @@ one of the best uses is to learn you exactly how git works.
if you want to know what is the difference between two files, you must first know what is the same.
this is called the Longest Common Subsequence problem. if you have two sequences `x = "ABDCEF" and `y = "ABCXYZF"` then `LCS(x,y)` is clearly "ABCF".
-
## lcs
```
@@ -23,7 +22,7 @@ function lcs (a,b)
this is very simple, but with exponential time complexity.
however, it can easily be made sufficantly performant by cacheing the return value of each call to lcs().
-see js implementation, [index.js#L64-94](index.js#L64-94)
+see js implementation, [index.js#L64-94](https://github.com/dominictarr/adiff/blob/master/index.js#L63-94)
## chunking
@@ -76,3 +75,23 @@ this is only a slightly more complicated problem. given a string `"ABDCEF"`, If
and meanwhile you changed it to "AXBCEFG". we must compare each of our changes to the original string, the [Concestor](http://en.wikipedia.org/wiki/Concestor)
TODO: worked example with chunks, resolve.
+
+## self hosting!
+
+snob became self hosting
+
+```
+4f63d2637ae35e9313fd4f23ea6cf4e8e527ba3c null Sun, 18 Mar 2012 08:20:34 GMT
+```
+
+
+## what next?
+
+refactor snob out of adiff.
+still thinking about how I will manage diffs on objects,
+ - refactor vu, with what I know now.
+giving snob a cli, and in particular, push/pullable remotes, is the first step to being distributed.
+
+ - improve support for injection with merge resolution.
+ - get running in the browser
+ - make Repository an EventEmitter
View
23 snob.js
@@ -47,13 +47,16 @@ Repository.prototype = {
commit.timestamp = Date.now()
commit.id = hash(commit)
+ // XXX make an error if the commits are empty !!! XXX
+
this.commits[commit.id] = commit
this.branch(branch, commit.id)
+ console.log('new commit',branch, commit.id, this.getId(commit.id))
return commit
// emit the new commit
},
get: function (commitish) {
- return this.commits[commitish] || this.branches[commitish] || this.tags[commitish]
+ return this.commits[commitish] || this.commits[this.branches[commitish] || this.tags[commitish]]
},
getId: function (commitish) {
return (this.get(commitish) || {id:null}).id
@@ -63,18 +66,24 @@ Repository.prototype = {
},
branch: function (name, commitish) {
// do not save this as a branch if it's actually a commit, or a tag.
- if(this.commits[name] || this.tags[name]) return
- this.branches[name] = this.getId(commitish)
+ if(this.commits[name] || this.tags[name]) {
+ console.log('!!!!!!!!!!!!!!!!!!!!!')
+ return this.getId(commitish)
+ }
+ console.log('BRANCH', name, commitish)
+ return this.branches[name] = this.getId(commitish)
},
diff: function (parent, world) {
var head = this.checkout(parent)
+ if('object' !== typeof world)
+ world = this.checkout(world)
return map(world, function (b, f) {
return a.diff(head[f] || [], b)
})
},
revlist: function (id) {
- if(!id) return []
- var commit = this.commits[id]
+ var commit = this.get(id)
+ if(!commit) return []
return this.revlist(commit.parent).concat(id)
},
concestor: function (heads) { //a list of commits you want to merge
@@ -129,9 +138,11 @@ Repository.prototype = {
commit.merged = branches
commit.parent = branches[0]
+ commit.depth = this.commits[branches[0]].depth + 1
commit.timestamp = Date.now()
+
commit.id = hash(commit)
- commit.depth = this.commits[branches[0]].depth + 1
+
this.commits[commit.id] = commit
this.branch(mine, commit.id) // if this was merge( ['master', ...], ...) update the branch
return commit
View
63 test/snob-test-branches.js
@@ -0,0 +1,63 @@
+
+var assert = require('assert')
+var Repo = require('../snob')
+
+var snob = new Repo()
+var world
+var init = snob.commit(world = {
+ hello: ['who', 'what', 'when','why']
+ },
+ { message: 'init', parent: 'master'})
+
+assert.equal(init.depth, 1)
+
+assert.equal(snob.getId('master'), init.id)
+
+snob.branch('underdog', init.id)
+
+var _world = snob.checkout(init.id)
+
+assert.deepEqual(_world, world)
+
+world.hello.splice(2, 0, 'how')
+
+var second = snob.commit(world, {
+ message: 'second',
+ parent: 'master'
+ })
+
+console.log(second)
+console.log(snob.revlist(second.id))
+
+assert.deepEqual(
+ snob.revlist(second.id),
+ [ init.id, second.id])
+
+assert.deepEqual(second.depth, 2)
+
+//a branch off init, so can test merge
+
+_world.hello.push('WTF!?')
+var branch = snob.commit(_world, {
+ message: 'branch',
+ parent: 'underdog'
+ })
+
+assert.equal(branch.depth, 2)
+console.log(branch)
+
+var concestor = snob.concestor(branch.id, second.id)
+
+assert.equal(concestor, init.id)
+console.log(concestor)
+
+var merged = snob.merge([branch.id, second.id], {message: 'merge1'})
+
+console.log(merged)
+assert.equal(3, merged.depth)
+
+var world3 = snob.checkout(merged.id)
+
+console.log(world3)
+
+
View
4 test/snob-test.js
@@ -40,9 +40,12 @@ var branch = snob.commit(_world, {
assert.equal(branch.depth, 2)
console.log(branch)
+
var concestor = snob.concestor(branch.id, second.id)
+
assert.equal(concestor, init.id)
console.log(concestor)
+
var merged = snob.merge([branch.id, second.id], {message: 'merge1'})
console.log(merged)
@@ -52,3 +55,4 @@ var world3 = snob.checkout(merged.id)
console.log(world3)
+
View
2  test/test.js
@@ -62,4 +62,6 @@ if(!module.parent) {
//note, it's possible for this case to occur in a
//n-way merge where there is a delete and a false conflict.
//most merges will be only 3 ways, so lets leave that for now.
+
+ console.log(d.diff3(split("ABCXYZF"), split("ABDCEF"), split("AXBCEFG")))
}
View
61 test/tree-test.js
@@ -0,0 +1,61 @@
+
+var assert = require('assert')
+
+function listify(tree){
+ var a = []
+ function cat (b) {
+ while(b.length)
+ a.push(b.shift())
+ }
+ if(Array.isArray(tree)) {
+ a.push('[')
+
+ for (var i in tree) {
+ // a.push(i + ':')
+ cat(listify(tree[i]))
+ a.push(',')
+ }
+ if(a[a.length - 1 ] == ',') a.pop()
+ a.push(']')
+ } else if ('object' == typeof tree) {
+
+ a.push('{')
+
+ for (var i in tree) {
+ a.push(i + ':')
+ cat(listify(tree[i]))
+ a.push(',')
+ }
+ if(a[a.length - 1] == ',') a.pop()
+
+ a.push('}')
+ } else
+ return [JSON.stringify(tree)]
+ return a
+}
+
+
+var d = require('../')
+
+assert.deepEqual(listify({hello: {}}), ['{', 'hello:','{', '}','}'])
+
+diff3(
+ {hello: {value: 250}, bye: 'ok'},
+ {hello: 250, bye: 'ok'},
+ {hello: 250, bye: 'okay'}
+)
+
+diff3 (
+ {hello: {whatever: true}, value: null},
+ {hello: 'hello', value: {whatever: true}},
+ {hello: 'hello', value: {whatever: true, changed: 'YES'}}
+)
+
+
+function diff3 () {
+ var args = [].slice.call(arguments).map(listify)
+ console.log(args)
+ var x = d.diff3.apply(null, args)
+ console.log('DIFF', x)
+}
+
Please sign in to comment.
Something went wrong with that request. Please try again.