git ssb

30+

cel / git-ssb-web



Tree: 9baccbc527825b5e2599e7c1f4bec65eb64ada3d

Files: 9baccbc527825b5e2599e7c1f4bec65eb64ada3d / lib / repos / index.js

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

Built with git-ssb-web