Files: 4b685a1454e2107ae586ba5cb063bffc4b3a9771 / lib / repos / index.js
39138 bytesRaw
1 | var url = require('url') |
2 | var pull = require('pull-stream') |
3 | var once = pull.once |
4 | var cat = require('pull-cat') |
5 | var catMap = require('pull-cat-map') |
6 | var paramap = require('pull-paramap') |
7 | var multicb = require('multicb') |
8 | var JsDiff = require('diff') |
9 | var GitRepo = require('pull-git-repo') |
10 | var gitPack = require('pull-git-pack') |
11 | var u = require('../util') |
12 | var paginate = require('../paginate') |
13 | var markdown = require('../markdown') |
14 | var forms = require('../forms') |
15 | var ssbRef = require('ssb-ref') |
16 | var zlib = require('zlib') |
17 | var toPull = require('stream-to-pull-stream') |
18 | var h = require('pull-hyperscript') // NB currently only on git-ssb |
19 | |
20 | module.exports = function (web) { |
21 | return new RepoRoutes(web) |
22 | } |
23 | |
24 | function RepoRoutes(web) { |
25 | this.web = web |
26 | this.issues = require('./issues')(this, web) |
27 | this.pulls = require('./pulls')(this, web) |
28 | } |
29 | |
30 | var R = RepoRoutes.prototype |
31 | |
32 | function getRepoObjectString(repo, id, mode, cb) { |
33 | if (!id) return cb(null, '') |
34 | if (mode == 0160000) return cb(null, |
35 | 'Subproject commit ' + id) |
36 | repo.getObjectFromAny(id, function (err, obj) { |
37 | if (err) return cb(err) |
38 | u.readObjectString(obj, cb) |
39 | }) |
40 | } |
41 | |
42 | function ul(props) { |
43 | return function (read) { |
44 | return cat([ |
45 | pull.once('<ul' + (props ? ' ' + props : '') + '>'), |
46 | pull(read, pull.map(function (li) { return '<li>' + li + '</li>' })), |
47 | pull.once('</ul>') |
48 | ]) |
49 | } |
50 | } |
51 | |
52 | /* Repo */ |
53 | |
54 | R.serveRepoPage = function (req, repo, path) { |
55 | var self = this |
56 | var defaultBranch = 'master' |
57 | var query = req._u.query |
58 | |
59 | if (query.rev != null) { |
60 | // Allow navigating revs using GET query param. |
61 | // Replace the branch in the path with the rev query value |
62 | path[0] = path[0] || 'tree' |
63 | path[1] = query.rev |
64 | req._u.pathname = u.encodeLink([repo.id].concat(path)) |
65 | delete req._u.query.rev |
66 | delete req._u.search |
67 | return self.web.serveRedirect(req, url.format(req._u)) |
68 | } |
69 | |
70 | // get branch |
71 | return path[1] ? |
72 | R_serveRepoPage2.call(self, req, repo, path) : |
73 | u.readNext(function (cb) { |
74 | // TODO: handle this in pull-git-repo or ssb-git-repo |
75 | repo.getSymRef('HEAD', true, function (err, ref) { |
76 | if (err) return cb(err) |
77 | repo.resolveRef(ref, function (err, rev) { |
78 | path[1] = rev ? ref : null |
79 | cb(null, R_serveRepoPage2.call(self, req, repo, path)) |
80 | }) |
81 | }) |
82 | }) |
83 | } |
84 | |
85 | function R_serveRepoPage2(req, repo, path) { |
86 | var branch = path[1] |
87 | var filePath = path.slice(2) |
88 | switch (path[0]) { |
89 | case undefined: |
90 | case '': |
91 | return this.serveRepoTree(req, repo, branch, []) |
92 | case 'activity': |
93 | return this.serveRepoActivity(req, repo, branch) |
94 | case 'commits': |
95 | return this.serveRepoCommits(req, repo, branch) |
96 | case 'commit': |
97 | return this.serveRepoCommit(req, repo, path[1]) |
98 | case 'tag': |
99 | return this.serveRepoTag(req, repo, branch, filePath) |
100 | case 'tree': |
101 | return this.serveRepoTree(req, repo, branch, filePath) |
102 | case 'blob': |
103 | return this.serveRepoBlob(req, repo, branch, filePath) |
104 | case 'raw': |
105 | return this.serveRepoRaw(req, repo, branch, filePath) |
106 | case 'digs': |
107 | return this.serveRepoDigs(req, repo) |
108 | case 'fork': |
109 | return this.serveRepoForkPrompt(req, repo) |
110 | case 'forks': |
111 | return this.serveRepoForks(req, repo) |
112 | case 'issues': |
113 | switch (path[1]) { |
114 | case 'new': |
115 | if (filePath.length == 0) |
116 | return this.issues.serveRepoNewIssue(req, repo) |
117 | break |
118 | default: |
119 | return this.issues.serveRepoIssues(req, repo, false) |
120 | } |
121 | case 'pulls': |
122 | return this.issues.serveRepoIssues(req, repo, true) |
123 | case 'compare': |
124 | return this.pulls.serveRepoCompare(req, repo) |
125 | case 'comparing': |
126 | return this.pulls.serveRepoComparing(req, repo) |
127 | case 'info': |
128 | switch (path[1]) { |
129 | case 'refs': |
130 | return this.serveRepoRefs(req, repo) |
131 | default: |
132 | return this.web.serve404(req) |
133 | } |
134 | case 'objects': |
135 | switch (path[1]) { |
136 | case 'info': |
137 | switch (path[2]) { |
138 | case 'packs': |
139 | return this.serveRepoPacksInfo(req, repo) |
140 | default: |
141 | return this.web.serve404(req) |
142 | } |
143 | case 'pack': |
144 | return this.serveRepoPack(req, repo, filePath.join('/')) |
145 | default: |
146 | var hash = path[1] + path[2] |
147 | if (hash.length === 40) { |
148 | return this.serveRepoObject(req, repo, hash) |
149 | } |
150 | return this.web.serve404(req) |
151 | } |
152 | case 'HEAD': |
153 | return this.serveRepoHead(req, repo) |
154 | default: |
155 | return this.web.serve404(req) |
156 | } |
157 | } |
158 | |
159 | R.serveRepoNotFound = function (req, id, err) { |
160 | return this.web.serveTemplate(req, req._t('error.RepoNotFound'), 404) |
161 | (pull.values([ |
162 | '<h2>' + req._t('error.RepoNotFound') + '</h2>', |
163 | '<p>' + req._t('error.RepoNameNotFound') + '</p>', |
164 | '<pre>' + u.escape(err.stack) + '</pre>' |
165 | ])) |
166 | } |
167 | |
168 | R.renderRepoPage = function (req, repo, page, branch, titleTemplate, body) { |
169 | var self = this |
170 | var gitUrl = 'ssb://' + repo.id |
171 | var host = req.headers.host || '127.0.0.1:7718' |
172 | var path = '/' + encodeURIComponent(repo.id) |
173 | var httpUrl = 'http://' + encodeURI(host) + path |
174 | var digsPath = [repo.id, 'digs'] |
175 | var cloneUrls = '<span class="clone-urls">' + |
176 | '<select class="custom-dropdown clone-url-protocol" ' + |
177 | 'onchange="this.nextSibling.value=this.value;this.nextSibling.select()">' + |
178 | '<option selected="selected" value="' + gitUrl + '">SSB</option>' + |
179 | '<option class="http-clone-url" value="' + httpUrl + '">HTTP</option>' + |
180 | '</select>' + |
181 | '<input class="clone-url" readonly="readonly" ' + |
182 | 'value="ssb://' + repo.id + '" size="45" ' + |
183 | 'onclick="this.select()"/>' + |
184 | '<script>' + |
185 | 'var httpOpt = document.querySelector(".http-clone-url")\n' + |
186 | 'if (location.protocol === "https:") httpOpt.text = "HTTPS"\n' + |
187 | 'httpOpt.value = location.origin + "' + path + '"\n' + |
188 | '</script>' + |
189 | '</span>' |
190 | |
191 | var done = multicb({ pluck: 1, spread: true }) |
192 | self.web.getRepoName(repo.feed, repo.id, done()) |
193 | self.web.about.getName(repo.feed, done()) |
194 | self.web.getVotes(repo.id, done()) |
195 | |
196 | if (repo.upstream) { |
197 | self.web.getRepoName(repo.upstream.feed, repo.upstream.id, done()) |
198 | self.web.about.getName(repo.upstream.feed, done()) |
199 | } |
200 | |
201 | return u.readNext(function (cb) { |
202 | done(function (err, repoName, authorName, votes, |
203 | upstreamName, upstreamAuthorName) { |
204 | if (err) return cb(null, self.web.serveError(req, err)) |
205 | var upvoted = votes.upvoters[self.web.myId] > 0 |
206 | var upstreamLink = !repo.upstream ? '' : |
207 | u.link([repo.upstream]) |
208 | var title = titleTemplate ? titleTemplate |
209 | .replace(/%\{repo\}/g, repoName) |
210 | .replace(/%\{author\}/g, authorName) |
211 | : authorName + '/' + repoName |
212 | var isPublic = self.web.isPublic |
213 | cb(null, self.web.serveTemplate(req, title)(cat([ |
214 | pull.once( |
215 | '<div class="repo-title">' + |
216 | '<form class="right-bar" action="" method="post">' + |
217 | '<button class="btn" name="action" value="vote" ' + |
218 | (isPublic ? 'disabled="disabled"' : ' type="submit"') + '>' + |
219 | '<i>✌</i> ' + req._t(!isPublic && upvoted ? 'Undig' : 'Dig') + |
220 | '</button>' + |
221 | (isPublic ? '' : '<input type="hidden" name="value" value="' + |
222 | (upvoted ? '0' : '1') + '">' + |
223 | '<input type="hidden" name="id" value="' + |
224 | u.escape(repo.id) + '">') + ' ' + |
225 | '<strong>' + u.link(digsPath, votes.upvotes) + '</strong> ' + |
226 | (isPublic ? '' : '<button class="btn" type="submit" ' + |
227 | ' name="action" value="fork-prompt">' + |
228 | '<i>⑂</i> ' + req._t('Fork') + |
229 | '</button>') + ' ' + |
230 | u.link([repo.id, 'forks'], '+', false, ' title="' + |
231 | req._t('Forks') + '"') + |
232 | '</form>' + |
233 | forms.name(req, !isPublic, repo.id, repoName, 'repo-name', |
234 | null, req._t('repo.Rename'), |
235 | '<h2 class="bgslash">' + u.link([repo.feed], authorName) + ' / ' + |
236 | u.link([repo.id], repoName) + '</h2>') + |
237 | '</div>' + |
238 | (repo.upstream ? '<small class="bgslash">' + req._t('ForkedFrom', { |
239 | repo: u.link([repo.upstream.feed], upstreamAuthorName) + '/' + |
240 | u.link([repo.upstream.id], upstreamName) |
241 | }) + '</small>' : '') + |
242 | u.nav([ |
243 | [[repo.id], req._t('Code'), 'code'], |
244 | [[repo.id, 'activity'], req._t('Activity'), 'activity'], |
245 | [[repo.id, 'commits', branch||''], req._t('Commits'), 'commits'], |
246 | [[repo.id, 'issues'], req._t('Issues'), 'issues'], |
247 | [[repo.id, 'pulls'], req._t('PullRequests'), 'pulls'] |
248 | ], page, cloneUrls)), |
249 | body |
250 | ]))) |
251 | }) |
252 | }) |
253 | } |
254 | |
255 | R.serveEmptyRepo = function (req, repo) { |
256 | if (repo.feed != this.web.myId) |
257 | return this.renderRepoPage(req, repo, 'code', null, null, pull.once( |
258 | '<section>' + |
259 | '<h3>' + req._t('EmptyRepo') + '</h3>' + |
260 | '</section>')) |
261 | |
262 | var gitUrl = 'ssb://' + repo.id |
263 | return this.renderRepoPage(req, repo, 'code', null, null, pull.once( |
264 | '<section>' + |
265 | '<h3>' + req._t('initRepo.GettingStarted') + '</h3>' + |
266 | '<h4>' + req._t('initRepo.CreateNew') + '</h4><pre>' + |
267 | 'touch ' + req._t('initRepo.README') + '.md\n' + |
268 | 'git init\n' + |
269 | 'git add ' + req._t('initRepo.README') + '.md\n' + |
270 | 'git commit -m "' + req._t('initRepo.InitialCommit') + '"\n' + |
271 | 'git remote add origin ' + gitUrl + '\n' + |
272 | 'git push -u origin master</pre>\n' + |
273 | '<h4>' + req._t('initRepo.PushExisting') + '</h4>\n' + |
274 | '<pre>git remote add origin ' + gitUrl + '\n' + |
275 | 'git push -u origin master</pre>' + |
276 | '</section>')) |
277 | } |
278 | |
279 | R.serveRepoTree = function (req, repo, rev, path) { |
280 | var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch' |
281 | var title = (path.length ? path.join('/') + ' · ' : '') + |
282 | '%{author}/%{repo}' + |
283 | (repo.head == 'refs/heads/' + rev ? '' : '@' + rev) |
284 | return this.renderRepoPage(req, repo, 'code', rev, title, cat([ |
285 | pull.once('<section class="branch-info light-grey"><form action="" method="get">' + |
286 | '<h3>' + req._t(type) + ': '), |
287 | this.revMenu(req, repo, rev), |
288 | pull.once('</h3></form>'), |
289 | type == 'Branch' && renderRepoLatest(req, repo, rev), |
290 | pull.once('</section>'), |
291 | rev ? cat([ |
292 | pull.once('<section class="files">'), |
293 | renderRepoTree(req, repo, rev, path), |
294 | pull.once('</section>'), |
295 | this.renderRepoReadme(req, repo, rev, path) |
296 | ]) : this.serveEmptyRepo(req, repo) |
297 | ])) |
298 | } |
299 | |
300 | /* Repo activity */ |
301 | |
302 | R.serveRepoActivity = function (req, repo, branch) { |
303 | var self = this |
304 | var title = req._t('Activity') + ' · %{author}/%{repo}' |
305 | return self.renderRepoPage(req, repo, 'activity', branch, title, cat([ |
306 | pull.once('<h3>' + req._t('Activity') + '</h3>'), |
307 | pull( |
308 | self.web.ssb.links({ |
309 | dest: repo.id, |
310 | rel: 'repo', |
311 | values: true, |
312 | reverse: true |
313 | }), |
314 | pull.asyncMap(renderRepoUpdate.bind(self, req, repo, false)) |
315 | ), |
316 | u.readOnce(function (cb) { |
317 | var done = multicb({ pluck: 1, spread: true }) |
318 | self.web.about.getName(repo.feed, done()) |
319 | self.web.getMsg(repo.id, done()) |
320 | done(function (err, authorName, msg) { |
321 | if (err) return cb(err) |
322 | self.web.renderFeedItem(req, { |
323 | key: repo.id, |
324 | value: msg, |
325 | authorName: authorName |
326 | }, cb) |
327 | }) |
328 | }) |
329 | ])) |
330 | } |
331 | |
332 | function renderRepoUpdate(req, repo, full, msg, cb) { |
333 | var c = msg.value.content |
334 | |
335 | if (c.type != 'git-update') { |
336 | return '' |
337 | // return renderFeedItem(msg, cb) |
338 | // TODO: render post, issue, pull-request |
339 | } |
340 | |
341 | var branches = [] |
342 | var tags = [] |
343 | if (c.refs) for (var name in c.refs) { |
344 | var m = name.match(/^refs\/(heads|tags)\/(.*)$/) || [,, name] |
345 | ;(m[1] == 'tags' ? tags : branches) |
346 | .push({name: m[2], value: c.refs[name]}) |
347 | } |
348 | var numObjects = c.objects ? Object.keys(c.objects).length : 0 |
349 | |
350 | var dateStr = new Date(msg.value.timestamp).toLocaleString(req._locale) |
351 | |
352 | this.web.about.getName(msg.value.author, function (err, name) { |
353 | if (err) return cb(err) |
354 | cb(null, '<section class="collapse">' + |
355 | u.link([msg.key], dateStr) + '<br>' + |
356 | u.link([msg.value.author], name) + '<br>' + |
357 | |
358 | branches.map(function (update) { |
359 | if (!update.value) { |
360 | return '<s>' + u.escape(update.name) + '</s><br/>' |
361 | } else { |
362 | var commitLink = u.link([repo.id, 'commit', update.value]) |
363 | var branchLink = u.link([repo.id, 'tree', update.name]) |
364 | return branchLink + ' → <tt>' + commitLink + '</tt><br/>' |
365 | } |
366 | }).join('') + |
367 | tags.map(function (update) { |
368 | return update.value |
369 | ? u.link([repo.id, 'tag', update.value], update.name) |
370 | : '<s>' + u.escape(update.name) + '</s>' |
371 | }).join(', ') + |
372 | '</section>') |
373 | }) |
374 | } |
375 | |
376 | /* Repo commits */ |
377 | |
378 | R.serveRepoCommits = function (req, repo, branch) { |
379 | var query = req._u.query |
380 | var title = req._t('Commits') + ' · %{author}/%{repo}' |
381 | return this.renderRepoPage(req, repo, 'commits', branch, title, cat([ |
382 | pull.once('<h3>' + req._t('Commits') + '</h3>'), |
383 | pull( |
384 | repo.readLog(query.start || branch), |
385 | pull.take(20), |
386 | paramap(repo.getCommitParsed.bind(repo), 8), |
387 | paginate( |
388 | !query.start ? '' : function (first, cb) { |
389 | cb(null, '…') |
390 | }, |
391 | pull.map(renderCommit.bind(this, req, repo)), |
392 | function (commit, cb) { |
393 | cb(null, commit.parents && commit.parents[0] ? |
394 | '<a href="?start=' + commit.id + '">' + |
395 | req._t('Older') + '</a>' : '') |
396 | } |
397 | ) |
398 | ) |
399 | ])) |
400 | } |
401 | |
402 | function renderCommit(req, repo, commit) { |
403 | var commitPath = [repo.id, 'commit', commit.id] |
404 | var treePath = [repo.id, 'tree', commit.id] |
405 | return '<section class="collapse">' + |
406 | '<strong>' + u.link(commitPath, commit.title) + '</strong><br>' + |
407 | '<tt>' + commit.id + '</tt> ' + |
408 | u.link(treePath, req._t('Tree')) + '<br>' + |
409 | u.escape(commit.author.name) + ' · ' + |
410 | commit.author.date.toLocaleString(req._locale) + |
411 | (commit.separateAuthor ? '<br>' + req._t('CommittedOn', { |
412 | name: u.escape(commit.committer.name), |
413 | date: commit.committer.date.toLocaleString(req._locale) |
414 | }) : '') + |
415 | '</section>' |
416 | } |
417 | |
418 | /* Branch menu */ |
419 | |
420 | R.formatRevOptions = function (currentName) { |
421 | return function (name) { |
422 | var htmlName = u.escape(name) |
423 | return '<option value="' + htmlName + '"' + |
424 | (name == currentName ? ' selected="selected"' : '') + |
425 | '>' + htmlName + '</option>' |
426 | } |
427 | } |
428 | |
429 | R.formatRevType = function(req, type) { |
430 | return ( |
431 | type == 'heads' ? req._t('Branches') : |
432 | type == 'tags' ? req._t('Tags') : |
433 | type) |
434 | } |
435 | |
436 | R.revMenu = function (req, repo, currentName) { |
437 | var self = this |
438 | return u.readOnce(function (cb) { |
439 | repo.getRefNames(function (err, refs) { |
440 | if (err) return cb(err) |
441 | cb(null, '<select class="custom-dropdown" name="rev" onchange="this.form.submit()">' + |
442 | Object.keys(refs).map(function (group) { |
443 | return '<optgroup ' + |
444 | 'label="' + self.formatRevType(req, group) + '">' + |
445 | refs[group].map(self.formatRevOptions(currentName)).join('') + |
446 | '</optgroup>' |
447 | }).join('') + |
448 | '</select><noscript> ' + |
449 | '<input type="submit" value="' + req._t('Go') + '"/></noscript>') |
450 | }) |
451 | }) |
452 | } |
453 | |
454 | /* Repo tree */ |
455 | |
456 | function renderRepoLatest(req, repo, rev) { |
457 | if (!rev) return pull.empty() |
458 | return u.readOnce(function (cb) { |
459 | repo.getCommitParsed(rev, function (err, commit) { |
460 | if (err) return cb(err) |
461 | var commitPath = [repo.id, 'commit', commit.id] |
462 | var actor = commit.separateAuthor ? 'author' : 'committer' |
463 | var actionKey = actor.slice(0,1).toUpperCase() + actor.slice(1) + 'ReleasedCommit' |
464 | cb(null, |
465 | '<div class="mt1">' + |
466 | '<span>' + |
467 | req._t(actionKey, { |
468 | name: u.escape(commit[actor].name), |
469 | commitName: u.link(commitPath, commit.title) |
470 | }) + |
471 | '</span>' + |
472 | '<tt class="float-right">' + |
473 | req._t('LatestOn', { |
474 | commitId: commit.id.slice(0, 7), |
475 | date: commit[actor].date.toLocaleString(req._locale) |
476 | }) + |
477 | '</tt>' + |
478 | '</div>' |
479 | ) |
480 | }) |
481 | }) |
482 | } |
483 | |
484 | // breadcrumbs |
485 | function linkPath(basePath, path) { |
486 | path = path.slice() |
487 | var last = path.pop() |
488 | return path.map(function (dir, i) { |
489 | return u.link(basePath.concat(path.slice(0, i+1)), dir) |
490 | }).concat(last).join(' / ') |
491 | } |
492 | |
493 | function renderRepoTree(req, repo, rev, path) { |
494 | var source = repo.readDir(rev,path) |
495 | var pathLinks = path.length === 0 ? '' : |
496 | ': ' + linkPath([repo.id, 'tree'], [rev].concat(path)) |
497 | |
498 | var location = once('') |
499 | if (path.length !== 0) { |
500 | var link = linkPath([repo.id, 'tree'], [rev].concat(path)) |
501 | location = h('div', {class: 'fileLocation'}, `${req._t('Files')}: ${link}`) |
502 | } |
503 | |
504 | return cat([ |
505 | location, |
506 | h('table', {class: "files w-100"}, sourceMap(source, file => |
507 | h('tr', [ |
508 | h('td', [ |
509 | h('i', fileIcon(file)) |
510 | ]), |
511 | h('td', u.link(filePath(file), file.name)) |
512 | ]) |
513 | )) |
514 | ]) |
515 | |
516 | function sourceMap (source, fn) { |
517 | return pull( |
518 | source, |
519 | pull.filter(Boolean), |
520 | catMap(fn) |
521 | ) |
522 | } |
523 | |
524 | function fileIcon(file) { |
525 | return fileType(file) === 'tree' ? '📁' : '📄' |
526 | } |
527 | |
528 | function filePath(file) { |
529 | var type = fileType(file) |
530 | return [repo.id, type, rev].concat(path, file.name) |
531 | } |
532 | |
533 | function fileType(file) { |
534 | if (file.mode === 040000) return 'tree' |
535 | else if (file.mode === 0160000) return 'commit' |
536 | else return 'blob' |
537 | } |
538 | } |
539 | |
540 | /* Repo readme */ |
541 | |
542 | R.renderRepoReadme = function (req, repo, branch, path) { |
543 | var self = this |
544 | return u.readNext(function (cb) { |
545 | pull( |
546 | repo.readDir(branch, path), |
547 | pull.filter(function (file) { |
548 | return /readme(\.|$)/i.test(file.name) |
549 | }), |
550 | pull.take(1), |
551 | pull.collect(function (err, files) { |
552 | if (err) return cb(null, pull.empty()) |
553 | var file = files[0] |
554 | if (!file) |
555 | return cb(null, pull.once(path.length ? '' : |
556 | '<p>' + req._t('NoReadme') + '</p>')) |
557 | repo.getObjectFromAny(file.id, function (err, obj) { |
558 | if (err) return cb(err) |
559 | cb(null, cat([ |
560 | pull.once('<section class="readme">'), |
561 | self.web.renderObjectData(obj, file.name, repo, branch, path), |
562 | pull.once('</section>') |
563 | ])) |
564 | }) |
565 | }) |
566 | ) |
567 | }) |
568 | } |
569 | |
570 | /* Repo commit */ |
571 | |
572 | R.serveRepoCommit = function (req, repo, rev) { |
573 | var self = this |
574 | return u.readNext(function (cb) { |
575 | repo.getCommitParsed(rev, function (err, commit) { |
576 | if (err) return cb(err) |
577 | var commitPath = [repo.id, 'commit', commit.id] |
578 | var treePath = [repo.id, 'tree', commit.id] |
579 | var title = u.escape(commit.title) + ' · ' + |
580 | '%{author}/%{repo}@' + commit.id.substr(0, 8) |
581 | cb(null, self.renderRepoPage(req, repo, null, rev, title, cat([ |
582 | pull.once( |
583 | '<h3>' + u.link(commitPath, |
584 | req._t('CommitRev', {rev: rev})) + '</h3>' + |
585 | '<section class="collapse">' + |
586 | '<div class="right-bar">' + |
587 | u.link(treePath, req._t('BrowseFiles')) + |
588 | '</div>' + |
589 | '<h4>' + u.linkify(u.escape(commit.title)) + '</h4>' + |
590 | (commit.body ? u.linkify(u.pre(commit.body)) : '') + |
591 | (commit.separateAuthor ? req._t('AuthoredOn', { |
592 | name: u.escape(commit.author.name), |
593 | date: commit.author.date.toLocaleString(req._locale) |
594 | }) + '<br/>' : '') + |
595 | req._t('CommittedOn', { |
596 | name: u.escape(commit.committer.name), |
597 | date: commit.committer.date.toLocaleString(req._locale) |
598 | }) + '<br/>' + |
599 | commit.parents.map(function (id) { |
600 | return req._t('Parent') + ': ' + |
601 | u.link([repo.id, 'commit', id], id) |
602 | }).join('<br>') + |
603 | '</section>' + |
604 | '<section><h3>' + req._t('FilesChanged') + '</h3>'), |
605 | // TODO: show diff from all parents (merge commits) |
606 | self.renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id]), |
607 | pull.once('</section>') |
608 | ]))) |
609 | }) |
610 | }) |
611 | } |
612 | |
613 | /* Repo tag */ |
614 | |
615 | R.serveRepoTag = function (req, repo, rev, path) { |
616 | var self = this |
617 | return u.readNext(function (cb) { |
618 | repo.getTagParsed(rev, function (err, tag) { |
619 | if (err) { |
620 | if (/Expected tag, got commit/.test(err.message)) { |
621 | req._u.pathname = u.encodeLink([repo.id, 'commit', rev].concat(path)) |
622 | return cb(null, self.web.serveRedirect(req, url.format(req._u))) |
623 | } |
624 | return cb(null, self.web.serveError(req, err)) |
625 | } |
626 | |
627 | var title = req._t('TagName', { |
628 | tag: u.escape(tag.tag) |
629 | }) + ' · %{author}/%{repo}' |
630 | var body = (tag.title + '\n\n' + |
631 | tag.body.replace(/-----BEGIN PGP SIGNATURE-----\n[^.]*?\n-----END PGP SIGNATURE-----\s*$/, '')).trim() |
632 | var date = tag.tagger.date |
633 | cb(null, self.renderRepoPage(req, repo, 'tags', tag.object, title, |
634 | pull.once( |
635 | '<section class="collapse">' + |
636 | '<h3>' + u.link([repo.id, 'tag', rev], tag.tag) + '</h3>' + |
637 | req._t('TaggedOn', { |
638 | name: u.escape(tag.tagger.name), |
639 | date: date && date.toLocaleString(req._locale) |
640 | }) + '<br/>' + |
641 | u.link([repo.id, tag.type, tag.object]) + |
642 | u.linkify(u.pre(body)) + |
643 | '</section>'))) |
644 | }) |
645 | }) |
646 | } |
647 | |
648 | |
649 | /* Diff stat */ |
650 | |
651 | R.renderDiffStat = function (req, repos, treeIds) { |
652 | if (treeIds.length == 0) treeIds = [null] |
653 | var id = treeIds[0] |
654 | var lastI = treeIds.length - 1 |
655 | var oldTree = treeIds[0] |
656 | var changedFiles = [] |
657 | return cat([ |
658 | pull( |
659 | GitRepo.diffTrees(repos, treeIds, true), |
660 | pull.map(function (item) { |
661 | var filename = u.escape(item.filename = item.path.join('/')) |
662 | var oldId = item.id && item.id[0] |
663 | var newId = item.id && item.id[lastI] |
664 | var oldMode = item.mode && item.mode[0] |
665 | var newMode = item.mode && item.mode[lastI] |
666 | var action = |
667 | !oldId && newId ? req._t('action.added') : |
668 | oldId && !newId ? req._t('action.deleted') : |
669 | oldMode != newMode ? req._t('action.changedMode', { |
670 | old: oldMode.toString(8), |
671 | new: newMode.toString(8) |
672 | }) : req._t('changed') |
673 | if (item.id) |
674 | changedFiles.push(item) |
675 | var blobsPath = item.id[1] |
676 | ? [repos[1].id, 'blob', treeIds[1]] |
677 | : [repos[0].id, 'blob', treeIds[0]] |
678 | var rawsPath = item.id[1] |
679 | ? [repos[1].id, 'raw', treeIds[1]] |
680 | : [repos[0].id, 'raw', treeIds[0]] |
681 | item.blobPath = blobsPath.concat(item.path) |
682 | item.rawPath = rawsPath.concat(item.path) |
683 | var fileHref = item.id ? |
684 | '#' + encodeURIComponent(item.path.join('/')) : |
685 | u.encodeLink(item.blobPath) |
686 | return ['<a href="' + fileHref + '">' + filename + '</a>', action] |
687 | }), |
688 | table() |
689 | ), |
690 | pull( |
691 | pull.values(changedFiles), |
692 | paramap(function (item, cb) { |
693 | var extension = u.getExtension(item.filename) |
694 | if (extension in u.imgMimes) { |
695 | var filename = u.escape(item.filename) |
696 | return cb(null, |
697 | '<pre><table class="code">' + |
698 | '<tr><th id="' + u.escape(item.filename) + '">' + |
699 | filename + '</th></tr>' + |
700 | '<tr><td><img src="' + u.encodeLink(item.rawPath) + '"' + |
701 | ' alt="' + filename + '"/></td></tr>' + |
702 | '</table></pre>') |
703 | } |
704 | var done = multicb({ pluck: 1, spread: true }) |
705 | var mode0 = item.mode && item.mode[0] |
706 | var modeI = item.mode && item.mode[lastI] |
707 | var isSubmodule = (modeI == 0160000) |
708 | getRepoObjectString(repos[0], item.id[0], mode0, done()) |
709 | getRepoObjectString(repos[1], item.id[lastI], modeI, done()) |
710 | done(function (err, strOld, strNew) { |
711 | if (err) return cb(err) |
712 | cb(null, htmlLineDiff(req, item.filename, item.filename, |
713 | strOld, strNew, |
714 | u.encodeLink(item.blobPath), !isSubmodule)) |
715 | }) |
716 | }, 4) |
717 | ) |
718 | ]) |
719 | } |
720 | |
721 | function htmlLineDiff(req, filename, anchor, oldStr, newStr, blobHref, |
722 | showViewLink) { |
723 | return '<pre><table class="code">' + |
724 | '<tr><th colspan=3 id="' + u.escape(anchor) + '">' + filename + |
725 | (showViewLink === false ? '' : |
726 | '<span class="right-bar">' + |
727 | '<a href="' + blobHref + '">' + req._t('View') + '</a> ' + |
728 | '</span>') + |
729 | '</th></tr>' + |
730 | (oldStr.length + newStr.length > 200000 |
731 | ? '<tr><td class="diff-info">' + req._t('diff.TooLarge') + '<br>' + |
732 | req._t('diff.OldFileSize', {bytes: oldStr.length}) + '<br>' + |
733 | req._t('diff.NewFileSize', {bytes: newStr.length}) + '</td></tr>' |
734 | : tableDiff(oldStr, newStr, filename)) + |
735 | '</table></pre>' |
736 | } |
737 | |
738 | function tableDiff(oldStr, newStr, filename) { |
739 | var diff = JsDiff.structuredPatch('', '', oldStr, newStr) |
740 | var groups = diff.hunks.map(function (hunk) { |
741 | var oldLine = hunk.oldStart |
742 | var newLine = hunk.newStart |
743 | var header = '<tr class="diff-hunk-header"><td colspan=2></td><td>' + |
744 | '@@ -' + oldLine + ',' + hunk.oldLines + ' ' + |
745 | '+' + newLine + ',' + hunk.newLines + ' @@' + |
746 | '</td></tr>' |
747 | return [header].concat(hunk.lines.map(function (line) { |
748 | var s = line[0] |
749 | if (s == '\\') return |
750 | var html = u.highlight(line, u.getExtension(filename)) |
751 | var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : '' |
752 | var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++] |
753 | var id = [filename].concat(lineNums).join('-') |
754 | return '<tr id="' + u.escape(id) + '" class="' + trClass + '">' + |
755 | lineNums.map(function (num) { |
756 | return '<td class="code-linenum">' + |
757 | (num ? '<a href="#' + encodeURIComponent(id) + '">' + |
758 | num + '</a>' : '') + '</td>' |
759 | }).join('') + |
760 | '<td class="code-text">' + html + '</td></tr>' |
761 | })) |
762 | }) |
763 | return [].concat.apply([], groups).join('') |
764 | } |
765 | |
766 | /* An unknown message linking to a repo */ |
767 | |
768 | R.serveRepoSomething = function (req, repo, id, msg, path) { |
769 | return this.renderRepoPage(req, repo, null, null, null, |
770 | pull.once('<section><h3>' + u.link([id]) + '</h3>' + |
771 | u.json(msg) + '</section>')) |
772 | } |
773 | |
774 | /* Repo update */ |
775 | |
776 | function objsArr(objs) { |
777 | return Array.isArray(objs) ? objs : |
778 | Object.keys(objs).map(function (sha1) { |
779 | var obj = Object.create(objs[sha1]) |
780 | obj.sha1 = sha1 |
781 | return obj |
782 | }) |
783 | } |
784 | |
785 | R.serveRepoUpdate = function (req, repo, id, msg, path) { |
786 | var self = this |
787 | var raw = req._u.query.raw != null |
788 | var title = req._t('Update') + ' · %{author}/%{repo}' |
789 | |
790 | if (raw) |
791 | return self.renderRepoPage(req, repo, 'activity', null, title, pull.once( |
792 | '<a href="?" class="raw-link header-align">' + |
793 | req._t('Info') + '</a>' + |
794 | '<h3>' + req._t('Update') + '</h3>' + |
795 | '<section class="collapse">' + |
796 | u.json({key: id, value: msg}) + '</section>')) |
797 | |
798 | // convert packs to old single-object style |
799 | if (msg.content.indexes) { |
800 | for (var i = 0; i < msg.content.indexes.length; i++) { |
801 | msg.content.packs[i] = { |
802 | pack: {link: msg.content.packs[i].link}, |
803 | idx: msg.content.indexes[i] |
804 | } |
805 | } |
806 | } |
807 | |
808 | var commits = cat([ |
809 | msg.content.objects && pull( |
810 | pull.values(msg.content.objects), |
811 | pull.filter(function (obj) { return obj.type == 'commit' }), |
812 | paramap(function (obj, cb) { |
813 | self.web.getBlob(req, obj.link || obj.key, function (err, readObject) { |
814 | if (err) return cb(err) |
815 | GitRepo.getCommitParsed({read: readObject}, cb) |
816 | }) |
817 | }, 8) |
818 | ), |
819 | msg.content.packs && pull( |
820 | pull.values(msg.content.packs), |
821 | paramap(function (pack, cb) { |
822 | var done = multicb({ pluck: 1, spread: true }) |
823 | self.web.getBlob(req, pack.pack.link, done()) |
824 | self.web.getBlob(req, pack.idx.link, done()) |
825 | done(function (err, readPack, readIdx) { |
826 | if (err) return cb(self.web.renderError(err)) |
827 | cb(null, gitPack.decodeWithIndex(repo, readPack, readIdx)) |
828 | }) |
829 | }, 4), |
830 | pull.flatten(), |
831 | pull.asyncMap(function (obj, cb) { |
832 | if (obj.type == 'commit') |
833 | GitRepo.getCommitParsed(obj, cb) |
834 | else |
835 | pull(obj.read, pull.drain(null, cb)) |
836 | }), |
837 | pull.filter() |
838 | ) |
839 | ]) |
840 | |
841 | return self.renderRepoPage(req, repo, 'activity', null, title, cat([ |
842 | pull.once('<a href="?raw" class="raw-link header-align">' + |
843 | req._t('Data') + '</a>' + |
844 | '<h3>' + req._t('Update') + '</h3>'), |
845 | pull( |
846 | pull.once({key: id, value: msg}), |
847 | pull.asyncMap(renderRepoUpdate.bind(self, req, repo, true)) |
848 | ), |
849 | (msg.content.objects || msg.content.packs) && |
850 | pull.once('<h3>' + req._t('Commits') + '</h3>'), |
851 | pull(commits, pull.map(function (commit) { |
852 | return renderCommit(req, repo, commit) |
853 | })) |
854 | ])) |
855 | } |
856 | |
857 | /* Blob */ |
858 | |
859 | R.serveRepoBlob = function (req, repo, rev, path) { |
860 | var self = this |
861 | return u.readNext(function (cb) { |
862 | repo.getFile(rev, path, function (err, object) { |
863 | if (err) return cb(null, self.web.serveBlobNotFound(req, repo.id, err)) |
864 | var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch' |
865 | var pathLinks = path.length === 0 ? '' : |
866 | ': ' + linkPath([repo.id, 'tree'], [rev].concat(path)) |
867 | var rawFilePath = [repo.id, 'raw', rev].concat(path) |
868 | var dirPath = path.slice(0, path.length-1) |
869 | var filename = path[path.length-1] |
870 | var extension = u.getExtension(filename) |
871 | var title = (path.length ? path.join('/') + ' · ' : '') + |
872 | '%{author}/%{repo}' + |
873 | (repo.head == 'refs/heads/' + rev ? '' : '@' + rev) |
874 | cb(null, self.renderRepoPage(req, repo, 'code', rev, title, cat([ |
875 | pull.once('<section><form action="" method="get">' + |
876 | '<h3>' + req._t(type) + ': ' + rev + ' '), |
877 | self.revMenu(req, repo, rev), |
878 | pull.once('</h3></form>'), |
879 | type == 'Branch' && renderRepoLatest(req, repo, rev), |
880 | pull.once('</section><section class="collapse">' + |
881 | '<h3>' + req._t('Files') + pathLinks + '</h3>' + |
882 | '<div>' + object.length + ' bytes' + |
883 | '<span class="raw-link">' + |
884 | u.link(rawFilePath, req._t('Raw')) + '</span>' + |
885 | '</div></section>' + |
886 | '<section>'), |
887 | extension in u.imgMimes |
888 | ? pull.once('<img src="' + u.encodeLink(rawFilePath) + |
889 | '" alt="' + u.escape(filename) + '" />') |
890 | : self.web.renderObjectData(object, filename, repo, rev, dirPath), |
891 | pull.once('</section>') |
892 | ]))) |
893 | }) |
894 | }) |
895 | } |
896 | |
897 | /* Raw blob */ |
898 | |
899 | R.serveRepoRaw = function (req, repo, branch, path) { |
900 | var self = this |
901 | return u.readNext(function (cb) { |
902 | repo.getFile(branch, path, function (err, object) { |
903 | if (err) return cb(null, |
904 | self.web.serveBuffer(404, req._t('error.BlobNotFound'))) |
905 | var extension = u.getExtension(path[path.length-1]) |
906 | var contentType = u.imgMimes[extension] |
907 | cb(null, pull(object.read, self.web.serveRaw(object.length, contentType))) |
908 | }) |
909 | }) |
910 | } |
911 | |
912 | /* Digs */ |
913 | |
914 | R.serveRepoDigs = function (req, repo) { |
915 | var self = this |
916 | return u.readNext(function (cb) { |
917 | var title = req._t('Digs') + ' · %{author}/%{repo}' |
918 | self.web.getVotes(repo.id, function (err, votes) { |
919 | cb(null, self.renderRepoPage(req, repo, null, null, title, cat([ |
920 | pull.once('<section><h3>' + req._t('Digs') + '</h3>' + |
921 | '<div>' + req._t('Total') + ': ' + votes.upvotes + '</div>'), |
922 | pull( |
923 | pull.values(Object.keys(votes.upvoters)), |
924 | paramap(function (feedId, cb) { |
925 | self.web.about.getName(feedId, function (err, name) { |
926 | if (err) return cb(err) |
927 | cb(null, u.link([feedId], name)) |
928 | }) |
929 | }, 8), |
930 | ul() |
931 | ), |
932 | pull.once('</section>') |
933 | ]))) |
934 | }) |
935 | }) |
936 | } |
937 | |
938 | /* Forks */ |
939 | |
940 | R.getForks = function (repo, includeSelf) { |
941 | var self = this |
942 | return pull( |
943 | cat([ |
944 | includeSelf && pull.once(repo.id), |
945 | // get downstream repos |
946 | pull( |
947 | self.web.ssb.links({ |
948 | dest: repo.id, |
949 | rel: 'upstream' |
950 | }), |
951 | pull.map('key') |
952 | ), |
953 | // look for other repos that previously had pull requests to this one |
954 | pull( |
955 | self.web.ssb.links({ |
956 | dest: repo.id, |
957 | values: true, |
958 | rel: 'project' |
959 | }), |
960 | pull.filter(function (msg) { |
961 | var c = msg && msg.value && msg.value.content |
962 | return c && c.type == 'pull-request' |
963 | }), |
964 | pull.map(function (msg) { return msg.value.content.head_repo }) |
965 | ) |
966 | ]), |
967 | pull.unique(), |
968 | paramap(function (key, cb) { |
969 | self.web.ssb.get(key, function (err, value) { |
970 | if (err) cb(err) |
971 | else cb(null, {key: key, value: value}) |
972 | }) |
973 | }, 4), |
974 | pull.filter(function (msg) { |
975 | var c = msg && msg.value && msg.value.content |
976 | return c && c.type == 'git-repo' |
977 | }), |
978 | paramap(function (msg, cb) { |
979 | self.web.getRepoFullName(msg.value.author, msg.key, |
980 | function (err, repoName, authorName) { |
981 | if (err) return cb(err) |
982 | cb(null, { |
983 | key: msg.key, |
984 | value: msg.value, |
985 | repoName: repoName, |
986 | authorName: authorName |
987 | }) |
988 | }) |
989 | }, 8) |
990 | ) |
991 | } |
992 | |
993 | R.serveRepoForks = function (req, repo) { |
994 | var hasForks |
995 | var title = req._t('Forks') + ' · %{author}/%{repo}' |
996 | return this.renderRepoPage(req, repo, null, null, title, cat([ |
997 | pull.once('<h3>' + req._t('Forks') + '</h3>'), |
998 | pull( |
999 | this.getForks(repo), |
1000 | pull.map(function (msg) { |
1001 | hasForks = true |
1002 | return '<section class="collapse">' + |
1003 | u.link([msg.value.author], msg.authorName) + ' / ' + |
1004 | u.link([msg.key], msg.repoName) + |
1005 | '<span class="right-bar">' + |
1006 | u.timestamp(msg.value.timestamp, req) + |
1007 | '</span></section>' |
1008 | }) |
1009 | ), |
1010 | u.readOnce(function (cb) { |
1011 | cb(null, hasForks ? '' : req._t('NoForks')) |
1012 | }) |
1013 | ])) |
1014 | } |
1015 | |
1016 | R.serveRepoForkPrompt = function (req, repo) { |
1017 | var title = req._t('Fork') + ' · %{author}/%{repo}' |
1018 | return this.renderRepoPage(req, repo, null, null, title, pull.once( |
1019 | '<form action="" method="post" onreset="history.back()">' + |
1020 | '<h3>' + req._t('ForkRepoPrompt') + '</h3>' + |
1021 | '<p>' + u.hiddenInputs({ id: repo.id }) + |
1022 | '<button class="btn open" type="submit" name="action" value="fork">' + |
1023 | req._t('Fork') + |
1024 | '</button>' + |
1025 | ' <button class="btn" type="reset">' + |
1026 | req._t('Cancel') + '</button>' + |
1027 | '</p></form>' |
1028 | )) |
1029 | } |
1030 | |
1031 | R.serveIssueOrPullRequest = function (req, repo, issue, path, id) { |
1032 | return issue.msg.value.content.type == 'pull-request' |
1033 | ? this.pulls.serveRepoPullReq(req, repo, issue, path, id) |
1034 | : this.issues.serveRepoIssue(req, repo, issue, path, id) |
1035 | } |
1036 | |
1037 | function getRepoLastMod(repo, cb) { |
1038 | repo.getState(function (err, state) { |
1039 | if (err) return cb(err) |
1040 | var lastMod = new Date(Math.max.apply(Math, state.refs.map(function (ref) { |
1041 | return ref.link.value.timestamp |
1042 | }))) || new Date() |
1043 | cb(null, lastMod) |
1044 | }) |
1045 | } |
1046 | |
1047 | R.serveRepoRefs = function (req, repo) { |
1048 | var self = this |
1049 | return u.readNext(function (cb) { |
1050 | getRepoLastMod(repo, function (err, lastMod) { |
1051 | if (err) return cb(null, self.web.serveError(req, err, 500)) |
1052 | if (u.ifModifiedSince(req, lastMod)) { |
1053 | return cb(null, pull.once([304])) |
1054 | } |
1055 | repo.getState(function (err, state) { |
1056 | if (err) return cb(null, self.web.serveError(req, err, 500)) |
1057 | var buf = state.refs.sort(function (a, b) { |
1058 | return a.name > b.name ? 1 : a.name < b.name ? -1 : 0 |
1059 | }).map(function (ref) { |
1060 | return ref.hash + '\t' + ref.name + '\n' |
1061 | }).join('') |
1062 | cb(null, pull.values([[200, { |
1063 | 'Content-Type': 'text/plain; charset=utf-8', |
1064 | 'Content-Length': Buffer.byteLength(buf), |
1065 | 'Last-Modified': lastMod.toGMTString() |
1066 | }], buf])) |
1067 | }) |
1068 | }) |
1069 | }) |
1070 | } |
1071 | |
1072 | R.serveRepoObject = function (req, repo, sha1) { |
1073 | var self = this |
1074 | if (!/[0-9a-f]{20}/.test(sha1)) return pull.once([401]) |
1075 | return u.readNext(function (cb) { |
1076 | repo.getObjectFromAny(sha1, function (err, obj) { |
1077 | if (err) return cb(null, pull.once([404])) |
1078 | cb(null, cat([ |
1079 | pull.once([200, { |
1080 | 'Content-Type': 'application/x-git-loose-object', |
1081 | 'Cache-Control': 'max-age=31536000' |
1082 | }]), |
1083 | pull( |
1084 | cat([ |
1085 | pull.values([obj.type, ' ', obj.length.toString(10), '\0']), |
1086 | obj.read |
1087 | ]), |
1088 | toPull(zlib.createDeflate()) |
1089 | ) |
1090 | ])) |
1091 | }) |
1092 | }) |
1093 | } |
1094 | |
1095 | R.serveRepoHead = function (req, repo) { |
1096 | var self = this |
1097 | return u.readNext(function (cb) { |
1098 | repo.getHead(function (err, name) { |
1099 | if (err) return cb(null, pull.once([500])) |
1100 | return cb(null, self.web.serveBuffer(200, 'ref: ' + name)) |
1101 | }) |
1102 | }) |
1103 | } |
1104 | |
1105 | R.serveRepoPacksInfo = function (req, repo) { |
1106 | var self = this |
1107 | return u.readNext(function (cb) { |
1108 | getRepoLastMod(repo, function (err, lastMod) { |
1109 | if (err) return cb(null, self.web.serveError(req, err, 500)) |
1110 | if (u.ifModifiedSince(req, lastMod)) { |
1111 | return cb(null, pull.once([304])) |
1112 | } |
1113 | cb(null, cat([ |
1114 | pull.once([200, { |
1115 | 'Content-Type': 'text/plain; charset=utf-8', |
1116 | 'Last-Modified': lastMod.toGMTString() |
1117 | }]), |
1118 | pull( |
1119 | repo.packs(), |
1120 | pull.map(function (pack) { |
1121 | var sha1 = pack.sha1 |
1122 | if (!sha1) { |
1123 | // make up a sha1 and hope git doesn't notice |
1124 | var packId = new Buffer(pack.packId.substr(1, 44), 'base64') |
1125 | sha1 = packId.slice(0, 20).toString('hex') |
1126 | } |
1127 | return 'P pack-' + sha1 + '.pack\n' |
1128 | }) |
1129 | ) |
1130 | ])) |
1131 | }) |
1132 | }) |
1133 | } |
1134 | |
1135 | R.serveRepoPack = function (req, repo, name) { |
1136 | var m = name.match(/^pack-(.*)\.(pack|idx)$/) |
1137 | if (!m) return pull.once([400]) |
1138 | var hex; |
1139 | try { |
1140 | hex = new Buffer(m[1], 'hex') |
1141 | } catch(e) { |
1142 | return pull.once([400]) |
1143 | } |
1144 | |
1145 | var self = this |
1146 | return u.readNext(function (cb) { |
1147 | pull( |
1148 | repo.packs(), |
1149 | pull.filter(function (pack) { |
1150 | var sha1 = pack.sha1 |
1151 | ? new Buffer(pack.sha1, 'hex') |
1152 | : new Buffer(pack.packId.substr(1, 44), 'base64').slice(0, 20) |
1153 | return sha1.equals(hex) |
1154 | }), |
1155 | pull.take(1), |
1156 | pull.collect(function (err, packs) { |
1157 | if (err) return console.error(err), cb(null, pull.once([500])) |
1158 | if (packs.length < 1) return cb(null, pull.once([404])) |
1159 | var pack = packs[0] |
1160 | |
1161 | if (m[2] === 'pack') { |
1162 | repo.getPackfile(pack.packId, function (err, read) { |
1163 | if (err) return cb(err) |
1164 | cb(null, pull(read, |
1165 | self.web.serveRaw(null, 'application/x-git-packed-objects') |
1166 | )) |
1167 | }) |
1168 | } |
1169 | |
1170 | if (m[2] === 'idx') { |
1171 | repo.getPackIndex(pack.idxId, function (err, read) { |
1172 | if (err) return cb(err) |
1173 | cb(null, pull(read, |
1174 | self.web.serveRaw(null, 'application/x-git-packed-objects-toc') |
1175 | )) |
1176 | }) |
1177 | } |
1178 | }) |
1179 | ) |
1180 | }) |
1181 | } |
1182 |
Built with git-ssb-web