Files: 95a067249e90995c9d34e46ae4aa937180c1d2f6 / index.js
8452 bytesRaw
1 | var packCodec = require('js-git/lib/pack-codec') |
2 | var pull = require('pull-stream') |
3 | var cat = require('pull-cat') |
4 | var buffered = require('pull-buffered') |
5 | var pack = require('./pack') |
6 | |
7 | function handleOption(options, name, value) { |
8 | switch (name) { |
9 | case 'verbosity': |
10 | options.verbosity = +value || 0 |
11 | return true |
12 | case 'progress': |
13 | options.progress = !!value && value !== 'false' |
14 | return true |
15 | default: |
16 | console.error('unknown option', name + ': ' + value) |
17 | return false |
18 | } |
19 | } |
20 | |
21 | function capabilitiesCmd(read) { |
22 | read(null, function next(end, data) { |
23 | if(end === true) return |
24 | if(end) throw end |
25 | |
26 | console.log(data) |
27 | read(null, next) |
28 | }) |
29 | } |
30 | |
31 | // return a source that delivers some data and then ends |
32 | // TODO: use pull.once and abortCb for this |
33 | function endSource(data) { |
34 | var done |
35 | return function (end, cb) { |
36 | if (done) return cb(true) |
37 | done = true |
38 | cb(null, data) |
39 | } |
40 | } |
41 | |
42 | function capabilitiesSource(prefix) { |
43 | return endSource([ |
44 | 'option', |
45 | 'connect', |
46 | 'refspec refs/heads/*:refs/' + prefix + '/heads/*', |
47 | 'refspec refs/tags/*:refs/' + prefix + '/tags/*', |
48 | ].join('\n') + '\n\n') |
49 | } |
50 | |
51 | function split2(str, delim) { |
52 | var i = str.indexOf(delim || ' ') |
53 | return (i === -1) ? [str, ''] : [ |
54 | str.substr(0, i), |
55 | str.substr(i + 1) |
56 | ] |
57 | } |
58 | |
59 | function split3(str) { |
60 | var args = split2(str) |
61 | return [args[0]].concat(split2(args[1])) |
62 | } |
63 | |
64 | function optionSource(cmd, options) { |
65 | var args = split2(cmd) |
66 | var msg = handleOption(options, args[0], args[1]) |
67 | msg = (msg === true) ? 'ok' |
68 | : (msg === false) ? 'unsupported' |
69 | : 'error ' + msg |
70 | return endSource(msg + '\n') |
71 | } |
72 | |
73 | function listSource() { |
74 | return endSource([ |
75 | /* TODO */ |
76 | ].join('\n') + '\n\n') |
77 | } |
78 | |
79 | function createHash() { |
80 | var hash = crypto.createHash('sha256') |
81 | var hasher = pull.through(function (data) { |
82 | hash.update(data) |
83 | }, function () { |
84 | hasher.digest = '&'+hash.digest('base64')+'.sha256' |
85 | }) |
86 | return hasher |
87 | } |
88 | |
89 | function uploadPack(read, objectSource, refSource) { |
90 | /* multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress |
91 | * include-tag multi_ack_detailed symref=HEAD:refs/heads/master |
92 | * agent=git/2.7.0 */ |
93 | var sendRefs = receivePackHeader([ |
94 | ], refSource, false) |
95 | |
96 | return packLineEncode( |
97 | cat([ |
98 | sendRefs, |
99 | pull.once('') |
100 | ]) |
101 | ) |
102 | } |
103 | |
104 | function getRefs() { |
105 | return pull.values([ |
106 | { |
107 | hash: '78beaedba9878623cea3862cf18e098cfb901e10', |
108 | name: 'refs/heads/master' |
109 | }, |
110 | { |
111 | hash: '78beaedba9878623cea3862cf18e098cfb901e10', |
112 | name: 'refs/remotes/cel/master' |
113 | } |
114 | ]) |
115 | /* |
116 | return function (err, cb) { |
117 | if (err === true) return |
118 | if (err) throw err |
119 | // TODO |
120 | cb(true) |
121 | } |
122 | */ |
123 | } |
124 | |
125 | function packLineEncode(read) { |
126 | var ended |
127 | return function (end, cb) { |
128 | if (ended) return cb(ended) |
129 | read(end, function (end, data) { |
130 | if (ended = end) { |
131 | cb(end) |
132 | } else { |
133 | var len = data ? data.length + 5 : 0 |
134 | cb(end, ('000' + len.toString(16)).substr(-4) + data + '\n') |
135 | } |
136 | }) |
137 | } |
138 | } |
139 | |
140 | function rev(str) { |
141 | return str === '0000000000000000000000000000000000000000' ? null : str |
142 | } |
143 | |
144 | function packLineDecode(read, options) { |
145 | var b = buffered(read) |
146 | var readPrefix = b.chunks(4) |
147 | var ended |
148 | |
149 | function readPackLine(abort, cb) { |
150 | readPrefix(abort, function (end, buf) { |
151 | if (ended = end) return cb(end) |
152 | var len = parseInt(buf, 16) |
153 | if (!len) |
154 | return cb(null, new Buffer('')) |
155 | // TODO: figure out this -4 thing |
156 | b.chunks(len - 4)(null, function (end, buf) { |
157 | if (ended = end) return cb(end) |
158 | cb(end, buf) |
159 | }) |
160 | }) |
161 | } |
162 | |
163 | function readUpdate(abort, cb) { |
164 | readPackLine(abort, function (end, line) { |
165 | if (end) return cb(end) |
166 | if (options.verbosity >= 2) |
167 | console.error('line', line.toString('ascii')) |
168 | if (!line.length) return cb(true) |
169 | var args = split3(line.toString('ascii')) |
170 | var args2 = split2(args[2], '\0') |
171 | var caps = args2[1] |
172 | if (caps && options.verbosity >= 2) |
173 | console.error('update capabilities:', caps) |
174 | cb(null, { |
175 | old: rev(args[0]), |
176 | new: rev(args[1]), |
177 | name: args2[0] |
178 | }) |
179 | }) |
180 | } |
181 | |
182 | b.packLines = readPackLine |
183 | b.updates = readUpdate |
184 | |
185 | return b |
186 | } |
187 | |
188 | // run a callback when a pipeline ends |
189 | // TODO: find a better way to do this |
190 | function onThroughEnd(onEnd) { |
191 | return function (read) { |
192 | return function (end, cb) { |
193 | read(end, function (end, data) { |
194 | cb(end, data) |
195 | if (end) |
196 | onEnd(end === true ? null : end) |
197 | }) |
198 | } |
199 | } |
200 | } |
201 | |
202 | /* |
203 | TODO: investigate capabilities |
204 | report-status delete-refs side-band-64k quiet atomic ofs-delta |
205 | */ |
206 | |
207 | // Get a line for each ref that we have. The first line also has capabilities. |
208 | // Wrap with packLineEncode. |
209 | function receivePackHeader(capabilities, refSource, usePlaceholder) { |
210 | var first = true |
211 | var ended |
212 | return function (abort, cb) { |
213 | if (ended) return cb(true) |
214 | refSource(abort, function (end, ref) { |
215 | ended = end |
216 | var name = ref && ref.name |
217 | var hash = ref && ref.hash |
218 | if (first && usePlaceholder) { |
219 | first = false |
220 | if (end) { |
221 | // use placeholder data if there are no refs |
222 | hash = '0000000000000000000000000000000000000000' |
223 | name = 'capabilities^{}' |
224 | } |
225 | name += '\0' + capabilities.join(' ') |
226 | } else if (end) { |
227 | return cb(true) |
228 | } |
229 | cb(null, hash + ' ' + name) |
230 | }) |
231 | } |
232 | } |
233 | |
234 | function receivePack(read, objectSink, refSource, refSink, options) { |
235 | var ended |
236 | var sendRefs = receivePackHeader([], refSource, true) |
237 | |
238 | return packLineEncode( |
239 | cat([ |
240 | // send our refs |
241 | sendRefs, |
242 | pull.once(''), |
243 | function (abort, cb) { |
244 | if (abort) return |
245 | // receive their refs |
246 | var lines = packLineDecode(read, options) |
247 | pull( |
248 | lines.updates, |
249 | onThroughEnd(refsDone), |
250 | refSink |
251 | ) |
252 | function refsDone(err) { |
253 | if (err) return cb(err) |
254 | pull( |
255 | lines.passthrough, |
256 | pack.decode(cb), |
257 | objectSink |
258 | ) |
259 | } |
260 | }, |
261 | pull.once('unpack ok') |
262 | ]) |
263 | ) |
264 | } |
265 | |
266 | function prepend(data, read) { |
267 | var done |
268 | return function (end, cb) { |
269 | if (done) { |
270 | read(end, cb) |
271 | } else { |
272 | done = true |
273 | cb(null, data) |
274 | } |
275 | } |
276 | } |
277 | |
278 | module.exports = function (opts) { |
279 | var ended |
280 | var prefix = opts.prefix |
281 | var objectSink = opts.objectSink |
282 | var objectSource = opts.objectSource || pull.empty() |
283 | var refSource = opts.refSource || pull.empty() |
284 | var refSink = opts.refSink |
285 | |
286 | var options = { |
287 | verbosity: 1, |
288 | progress: false |
289 | } |
290 | |
291 | function handleConnect(cmd, read) { |
292 | var args = split2(cmd) |
293 | switch (args[0]) { |
294 | case 'git-upload-pack': |
295 | return prepend('\n', uploadPack(read, objectSource, refSource)) |
296 | case 'git-receive-pack': |
297 | return prepend('\n', receivePack(read, objectSink, refSource, |
298 | refSink, options)) |
299 | default: |
300 | return pull.error(new Error('Unknown service ' + args[0])) |
301 | } |
302 | } |
303 | |
304 | function handleCommand(line, read) { |
305 | var args = split2(line) |
306 | switch (args[0]) { |
307 | case 'capabilities': |
308 | return capabilitiesSource(prefix) |
309 | case 'list': |
310 | return listSource() |
311 | case 'connect': |
312 | return handleConnect(args[1], read) |
313 | case 'option': |
314 | return optionSource(args[1], options) |
315 | default: |
316 | return pull.error(new Error('Unknown command ' + line)) |
317 | } |
318 | } |
319 | |
320 | return function (read) { |
321 | var b = buffered() |
322 | b(read) |
323 | var command |
324 | |
325 | function getCommand(cb) { |
326 | b.lines(null, function next(end, line) { |
327 | if (ended = end) |
328 | return cb(end) |
329 | |
330 | if (line == '') |
331 | return b.lines(null, next) |
332 | |
333 | if (options.verbosity > 1) |
334 | console.error('command:', line) |
335 | |
336 | var cmdSource = handleCommand(line, b.passthrough) |
337 | cb(null, cmdSource) |
338 | }) |
339 | } |
340 | |
341 | return function next(abort, cb) { |
342 | if (ended) return cb(ended) |
343 | |
344 | if (!command) { |
345 | if (abort) return |
346 | getCommand(function (end, cmd) { |
347 | command = cmd |
348 | next(end, cb) |
349 | }) |
350 | return |
351 | } |
352 | |
353 | command(abort, function (err, data) { |
354 | if (err) { |
355 | command = null |
356 | if (err !== true) |
357 | cb(err, data) |
358 | else |
359 | next(abort, cb) |
360 | } else { |
361 | cb(null, data) |
362 | } |
363 | }) |
364 | } |
365 | } |
366 | } |
367 | |
368 |
Built with git-ssb-web