git ssb

30+

cel / git-ssb-web



Tree: dd4e83623a7471b95f8faa0bd8c16453c6b9f6f5

Files: dd4e83623a7471b95f8faa0bd8c16453c6b9f6f5 / lib / repos / index.js

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

Built with git-ssb-web