git ssb

30+

cel / git-ssb-web



Tree: c870aa737762e1030c802a7cdddd3a9c494d279b

Files: c870aa737762e1030c802a7cdddd3a9c494d279b / lib / repos / index.js

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

Built with git-ssb-web