git ssb

30+

cel / git-ssb-web



Tree: 4b685a1454e2107ae586ba5cb063bffc4b3a9771

Files: 4b685a1454e2107ae586ba5cb063bffc4b3a9771 / lib / repos / index.js

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

Built with git-ssb-web