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