git ssb

30+

cel / git-ssb-web



Tree: 0b95005d33b39339c34360a02ee6a8f696c7ec92

Files: 0b95005d33b39339c34360a02ee6a8f696c7ec92 / lib / repos / index.js

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

Built with git-ssb-web