git ssb

30+

cel / git-ssb-web



Tree: 7f17070bd0ea7f8d0c5e6ab971e847e2a09e28ce

Files: 7f17070bd0ea7f8d0c5e6ab971e847e2a09e28ce / lib / repos / index.js

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

Built with git-ssb-web