git ssb

30+

cel / git-ssb-web



Tree: 126b1bb2d5440631bb06f7e3ad4448677d202031

Files: 126b1bb2d5440631bb06f7e3ad4448677d202031 / lib / repos / index.js

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

Built with git-ssb-web