git ssb

30+

cel / git-ssb-web



Tree: 12cbbc0fcd92a745f0439eff6cad0263ed4a8fcc

Files: 12cbbc0fcd92a745f0439eff6cad0263ed4a8fcc / lib / repos / index.js

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

Built with git-ssb-web