├── .gitignore
├── icons
├── icon-128.png
├── icon-16.png
├── icon-256.png
└── icon-512.png
├── background.js
├── package.sh
├── index.html
├── package.json
├── manifest.json
├── README.md
└── main.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .zedstate
3 | tags
4 |
--------------------------------------------------------------------------------
/icons/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/creationix/js-git-app/HEAD/icons/icon-128.png
--------------------------------------------------------------------------------
/icons/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/creationix/js-git-app/HEAD/icons/icon-16.png
--------------------------------------------------------------------------------
/icons/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/creationix/js-git-app/HEAD/icons/icon-256.png
--------------------------------------------------------------------------------
/icons/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/creationix/js-git-app/HEAD/icons/icon-512.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | JS-Git Sample Chrome App
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------