git ssb

30+

cel / git-ssb-web



Tree: 8df9a55e12e25a2e55af110f3c01603e65aa3c47

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

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

Built with git-ssb-web