git ssb

30+

cel / git-ssb-web



Tree: c057842053e5b621b8944a045b087dd563abe19e

Files: c057842053e5b621b8944a045b087dd563abe19e / lib / repos / index.js

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

Built with git-ssb-web