git ssb

30+

cel / git-ssb-web



Tree: 8fbdc0d4ddc18ce405cf50944702c26c42885bce

Files: 8fbdc0d4ddc18ce405cf50944702c26c42885bce / lib / repos / index.js

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

Built with git-ssb-web