git ssb

30+

cel / git-ssb-web



Tree: 49d609545707184cd230705dd31dbaba4e3afe0f

Files: 49d609545707184cd230705dd31dbaba4e3afe0f / lib / repos / index.js

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

Built with git-ssb-web