git ssb

30+

cel / git-ssb-web



Tree: 14c6604e9dbde43363ceabea4f528ced348c5652

Files: 14c6604e9dbde43363ceabea4f528ced348c5652 / lib / repos / index.js

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

Built with git-ssb-web