├── .gitignore ├── README.md ├── background.js ├── icons ├── icon-128.png ├── icon-16.png ├── icon-256.png └── icon-512.png ├── index.html ├── main.js ├── manifest.json ├── package.json └── package.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .zedstate 3 | tags 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | js-git-app 2 | ========== 3 | 4 | A js-git packaged app for chrome and chromebooks. 5 | 6 | ## To run this app 7 | 8 | 1. Clone to computer or download and unzip. 9 | 2. Install the dependencies using `npm install --dedupe` 10 | 3. Load this folder as an unpacked extension at . 11 | 12 | ## Progress 13 | 14 | Currently this chrome app does the following things: 15 | 16 | - Connect to github over a raw TCP socket using a special chrome API 17 | - Codec for pkt-line message framing on the binary stream 18 | - Parser and encoder for the contents of some git line messages 19 | - side-band parsing and multiplexing/demultiplexing of streams 20 | - Ref discovery 21 | - Stream of raw pack data 22 | 23 | TODO: 24 | 25 | - Hook raw pack stream to Chris Dickinson's pack parser 26 | - Store resulting object stream to persistent storage 27 | - Implement index and working files 28 | - Plan more awesome stuff. 29 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | /*global chrome*/ 2 | "use strict"; 3 | 4 | chrome.app.runtime.onLaunched.addListener(function() { 5 | chrome.app.window.create('/index.html', { 6 | id: "js-git-app-main", 7 | 8 | }); 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/js-git-app/293344ec0c0f0c53ecf5d82f1d4f3d40c8ae3edd/icons/icon-128.png -------------------------------------------------------------------------------- /icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/js-git-app/293344ec0c0f0c53ecf5d82f1d4f3d40c8ae3edd/icons/icon-16.png -------------------------------------------------------------------------------- /icons/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/js-git-app/293344ec0c0f0c53ecf5d82f1d4f3d40c8ae3edd/icons/icon-256.png -------------------------------------------------------------------------------- /icons/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/js-git-app/293344ec0c0f0c53ecf5d82f1d4f3d40c8ae3edd/icons/icon-512.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JS-Git Sample Chrome App 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var domBuilder = require('dombuilder'); 4 | var log = require('domlog'); 5 | 6 | var tcp = require('min-stream-chrome/tcp.js'); 7 | var chain = require('min-stream/chain.js'); 8 | var demux = require('min-stream/demux.js'); 9 | 10 | var pktLine = require('git-pkt-line'); 11 | var listPack = require('git-list-pack/min.js'); 12 | var hydratePack = require('git-hydrate-pack'); 13 | var bops = require('bops'); 14 | 15 | window.log = log; 16 | 17 | document.body.innerText = ""; 18 | document.body.appendChild(domBuilder([ 19 | ["h1", "JS-Git ChromeApp"], 20 | ["form", 21 | {onsubmit: wrap(function (evt) { 22 | evt.preventDefault(); 23 | clone(this.url.value, this.sideband.checked); 24 | })}, 25 | ["input", {name: "url", size: 50, value: "git://github.com/creationix/conquest.git"}], 26 | ["input", {type:"submit", value: "Clone!"}], 27 | ["label", 28 | ["input", {type:"checkbox", checked:true, name:"sideband"}], 29 | "Include side-band support", 30 | ] 31 | ] 32 | ])); 33 | 34 | log.setup({ 35 | top: "150px", 36 | height: "auto", 37 | background: "#222" 38 | }); 39 | 40 | // Wrap a function in one that redirects exceptions. 41 | // Use for all event-source handlers. 42 | function wrap(fn) { 43 | 44 | return function () { 45 | try { 46 | return fn.apply(this, arguments); 47 | } 48 | catch (err) { 49 | log(err); 50 | } 51 | }; 52 | } 53 | // Same as wrap, but also checks err argument. Use for callbacks. 54 | function check(fn) { 55 | return function (err) { 56 | if (err) return log(err); 57 | try { 58 | return fn.apply(this, Array.prototype.slice.call(arguments, 1)); 59 | } 60 | catch (err) { 61 | log(err); 62 | } 63 | }; 64 | } 65 | 66 | var gitMatch = new RegExp("^git://([^/:]+)(?::([0-9]+))?(/.*)$"); 67 | function parseUrl(url) { 68 | var match = url.match(gitMatch); 69 | if (match) { 70 | return { 71 | type: "tcp", 72 | host: match[1], 73 | port: match[2] ? parseInt(match[2], 10) : 9418, 74 | path: match[3] 75 | }; 76 | } 77 | throw new SyntaxError("Invalid url: " + url); 78 | } 79 | 80 | function clone(url, sideband) { 81 | log.container.textContent = ""; 82 | url = parseUrl(url); 83 | log("Parsed Url", url); 84 | tcp.connect(url.host, url.port, check(function (socket) { 85 | 86 | log("Connected to server"); 87 | 88 | chain 89 | .source(socket.source) 90 | // .map(logger("<")) 91 | .push(pktLine.deframer) 92 | // .map(logger("<-")) 93 | .pull(app) 94 | // .map(logger("->")) 95 | .push(pktLine.framer) 96 | // .map(logger(">")) 97 | .sink(socket.sink); 98 | })); 99 | 100 | 101 | function app(read) { 102 | 103 | var sources = demux(["line", "pack", "progress", "error"], read); 104 | 105 | var output = tube(); 106 | 107 | log("Sending upload-pack request..."); 108 | output.write(null, "git-upload-pack " + url.path + "\0host=" + url.host + "\0"); 109 | 110 | var refs = {}; 111 | var caps; 112 | 113 | consumeTill(sources.line, function (item) { 114 | if (item) { 115 | item = decodeLine(item); 116 | if (item.caps) caps = item.caps; 117 | refs[item[1]] = item[0]; 118 | return true; 119 | } 120 | }, function (err) { 121 | if (err) return log(err); 122 | log("server capabilities", caps); 123 | log("remote refs", refs); 124 | var clientCaps = []; 125 | if (sideband) { 126 | if (caps["side-band-64k"]) { 127 | clientCaps.push("side-band-64k"); 128 | } 129 | else if (caps["side-band"]) { 130 | clientCaps.push("side-band"); 131 | } 132 | } 133 | log("Asking for HEAD", refs.HEAD) 134 | output.write(null, ["want", refs.HEAD].concat(clientCaps).join(" ") + "\n"); 135 | output.write(null, null); 136 | output.write(null, "done"); 137 | 138 | var seen = {}; 139 | var pending = {}; 140 | function find(oid, ready) { 141 | if (seen[oid]) ready(null, seen[oid]); 142 | else pending[oid] = ready; 143 | } 144 | 145 | chain 146 | .source(sources.pack) 147 | // .map(logger("rawpack")) 148 | .pull(listPack) 149 | // .map(logger("partlyparsed")) 150 | .push(hydratePack(find)) 151 | // .map(logger("hydrated")) 152 | .map(function (item) { 153 | seen[item.hash] = item; 154 | if (pending[item.hash]) { 155 | pending[item.hash](null, item); 156 | } 157 | return item; 158 | }) 159 | .map(parseObject) 160 | .map(logger("object")) 161 | .sink(devNull); 162 | 163 | devNull(sources.line); 164 | 165 | chain 166 | .source(sources.progress) 167 | .map(logger("progress")) 168 | .sink(devNull); 169 | 170 | devNull(sources.error); 171 | }); 172 | 173 | return output; 174 | } 175 | 176 | } 177 | 178 | var parsers = { 179 | tree: function (item) { 180 | var list = []; 181 | var data = item.data; 182 | var hash; 183 | var mode; 184 | var path; 185 | var i = 0, l = data.length; 186 | while (i < l) { 187 | var start = i; 188 | while (data[i++] !== 0x20); 189 | mode = parseInt(bops.to(bops.subarray(data, start, i - 1)), 8); 190 | start = i; 191 | while (data[i++]); 192 | path = bops.to(bops.subarray(data, start, i - 1)); 193 | hash = bops.to(bops.subarray(data, i, i + 20), "hex"); 194 | i += 20; 195 | list.push({ 196 | mode: mode, 197 | path: path, 198 | hash: hash 199 | }); 200 | } 201 | return list; 202 | }, 203 | blob: function (item) { 204 | return item.data; 205 | }, 206 | commit: function (item) { 207 | var data = item.data; 208 | var i = 0, l = data.length; 209 | var key; 210 | var items = {parents:[]}; 211 | while (i < l) { 212 | var start = i; 213 | while (data[i++] !== 0x20); 214 | key = bops.to(bops.subarray(data, start, i - 1)); 215 | start = i; 216 | while (data[i++] !== 0x0a); 217 | var value = bops.to(bops.subarray(data, start, i - 1)); 218 | if (key === "parent") { 219 | items.parents.push(value); 220 | } 221 | else items[key] = value; 222 | if (data[i] === 0x0a) { 223 | items.message = bops.to(bops.subarray(data, i + 1)); 224 | break; 225 | } 226 | } 227 | return items; 228 | } 229 | }; 230 | 231 | function parseObject(item) { 232 | var obj = { 233 | hash: item.hash 234 | }; 235 | obj[item.type] = parsers[item.type](item); 236 | return obj; 237 | } 238 | 239 | 240 | function logger(message) { 241 | return function (item) { 242 | log(message, item); 243 | return item; 244 | }; 245 | } 246 | 247 | // Eat all events in a stream 248 | function devNull(read) { 249 | read(null, onRead); 250 | function onRead(err, item) { 251 | if (err) log(err); 252 | else if (item !== undefined) read(null, onRead); 253 | } 254 | } 255 | 256 | function consumeTill(read, check, callback) { 257 | read(null, onRead); 258 | function onRead(err, item) { 259 | if (item === undefined) { 260 | if (err) return callback(err); 261 | return callback(); 262 | } 263 | if (!check(item)) return callback(); 264 | read(null, onRead); 265 | } 266 | } 267 | 268 | function tube() { 269 | var dataQueue = []; 270 | var readQueue = []; 271 | var closed; 272 | function check() { 273 | while (!closed && readQueue.length && dataQueue.length) { 274 | readQueue.shift().apply(null, dataQueue.shift()); 275 | } 276 | } 277 | function write(err, item) { 278 | dataQueue.push([err, item]); 279 | check(); 280 | } 281 | function read(close, callback) { 282 | if (close) closed = close; 283 | if (closed) return callback(); 284 | readQueue.push(callback); 285 | check(); 286 | } 287 | read.write = write; 288 | return read; 289 | } 290 | 291 | 292 | 293 | // Decode a binary line 294 | // returns the data array with caps and request tagging if they are found. 295 | function decodeLine(line) { 296 | var result = []; 297 | 298 | if (line[line.length - 1] === "\0") { 299 | result.request = true; 300 | line = line.substr(0, line.length - 1); 301 | } 302 | line = line.trim(); 303 | var parts = line.split("\0"); 304 | result.push.apply(result, parts[0].split(" ")); 305 | if (parts[1]) { 306 | result.caps = {}; 307 | parts[1].split(" ").forEach(function (cap) { 308 | var pair = cap.split("="); 309 | result.caps[pair[0]] = pair[1] ? pair[1] : true; 310 | }); 311 | } 312 | return result; 313 | } 314 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JS-Git Test App", 3 | "version": "0.1.3", 4 | "manifest_version": 2, 5 | "offline_enabled": true, 6 | "description": "A js-git packaged app for chrome and chromebooks.", 7 | "icons": { 8 | "16": "icons/icon-16.png", 9 | "128": "icons/icon-128.png", 10 | "256": "icons/icon-256.png", 11 | "512": "icons/icon-512.png" 12 | }, 13 | "app": { 14 | "background": { 15 | "scripts": ["background.js"] 16 | } 17 | }, 18 | "permissions": [ 19 | "storage", 20 | "unlimitedStorage", 21 | "http://*/*", 22 | "https://*/*", 23 | {"socket": [ 24 | "tcp-connect:*:*", 25 | "tcp-listen::*", 26 | "udp-send-to::*", 27 | "udp-bind::*" 28 | ]} 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-git-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "base64-js": "0.0.2", 7 | "bops": "0.0.6", 8 | "chrome-app-module-loader": "0.0.2", 9 | "dombuilder": "0.1.2", 10 | "domlog": "0.0.7", 11 | "git-apply-delta": "0.0.7", 12 | "git-hydrate-pack": "0.0.5", 13 | "git-list-pack": "0.0.10", 14 | "git-pkt-line": "0.0.0", 15 | "inflate": "0.0.6", 16 | "min-stream": "0.0.4", 17 | "min-stream-chrome": "0.0.6", 18 | "through": "2.2.7", 19 | "to-utf8": "0.0.1", 20 | "varint": "0.0.3" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/creationix/js-git-app.git" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | VERSION=`node -pe 'JSON.parse(require("fs").readFileSync("manifest.json", "utf8")).version'` 3 | FILE="../js-git-app-$VERSION.zip" 4 | rm -f $FILE 5 | zip -r9o $FILE . -x '.*' '*/.*' tags package.sh 6 | echo "Saved to $FILE" 7 | --------------------------------------------------------------------------------