git ssb

30+

cel / git-ssb-web



Tree: 94fac4ede25741324d83db694a7252b79325a855

Files: 94fac4ede25741324d83db694a7252b79325a855 / lib / repos / index.js

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

Built with git-ssb-web