git ssb

30+

cel / git-ssb-web



Tree: ac38f3bf0c59103522861df8b3a1f1f545247977

Files: ac38f3bf0c59103522861df8b3a1f1f545247977 / lib / repos / index.js

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

Built with git-ssb-web