git ssb

30+

cel / git-ssb-web



Commit c057842053e5b621b8944a045b087dd563abe19e

Implement inline comment replies; render inline comment threads

cel committed on 11/23/2017, 11:00:55 PM
Parent: 63bcff079120449a73ea2cceec00484d3378041c

Files changed

index.jschanged
lib/forms.jschanged
lib/repos/index.jschanged
lib/repos/pulls.jschanged
locale/en.jsonchanged
locale/eo.jsonchanged
schemas.mdchanged
static/styles.csschanged
index.jsView
@@ -334,8 +334,11 @@
334334 new ParamError('missing repo id'), 400))
335335 if (!data.commitId)
336336 return cb(null, self.serveError(req,
337337 new ParamError('missing commit id'), 400))
338 + if (!data.updateId)
339 + return cb(null, self.serveError(req,
340 + new ParamError('missing update id'), 400))
338341 if (!data.filePath)
339342 return cb(null, self.serveError(req,
340343 new ParamError('missing file path'), 400))
341344 if (!data.line)
@@ -344,29 +347,51 @@
344347 var lineNumber = Number(data.line)
345348 if (isNaN(lineNumber))
346349 return cb(null, self.serveError(req,
347350 new ParamError('bad line number'), 400))
348- var repoBranches = data.repoBranch
349- ? data.repoBranch.split(',') : ''
350351 var msg = {
351352 type: 'line-comment',
352353 text: data.text,
353354 repo: data.repo,
354- repoBranch: repoBranches,
355 + updateId: data.updateId,
355356 commitId: data.commitId,
356357 filePath: data.filePath,
357358 line: lineNumber,
358359 }
359360 msg.issue = data.issue
360361 var mentions = Mentions(data.text)
361362 if (mentions.length)
362363 msg.mentions = mentions
363- return cb(null, self.serveBuffer(200, JSON.stringify(msg, 0, 2)))
364364 return self.ssb.publish(msg, function (err) {
365365 if (err) return cb(null, self.serveError(req, err))
366366 cb(null, self.serveRedirect(req, req.url))
367367 })
368368
369 + case 'line-comment-reply':
370 + if (!data.root)
371 + return cb(null, self.serveError(req,
372 + new ParamError('missing thread root'), 400))
373 + if (!data.branch)
374 + return cb(null, self.serveError(req,
375 + new ParamError('missing thread branch'), 400))
376 + if (!data.text)
377 + return cb(null, self.serveError(req,
378 + new ParamError('missing post text'), 400))
379 + var msg = {
380 + type: 'post',
381 + root: data.root,
382 + branch: data.branch,
383 + text: data.text,
384 + }
385 + var mentions = Mentions(data.text)
386 + if (mentions.length)
387 + msg.mentions = mentions
388 + return self.ssb.publish(msg, function (err) {
389 + if (err) return cb(null, self.serveError(req, err))
390 +
391 + cb(null, self.serveRedirect(req, req.url))
392 + })
393 +
369394 case 'new-issue':
370395 var msg = Issues.schemas.new(dir, data.text)
371396 var mentions = Mentions(data.text)
372397 if (mentions.length)
lib/forms.jsView
@@ -2,26 +2,27 @@
22 var u = require('./util')
33 var forms = exports
44
55 forms.post = function (req, repo, placeholder, rows) {
6- return '<input type="radio" class="tab-radio" id="tab1" name="tab" checked="checked"/>' +
6 + return '<div class="post-form">' +
7 + '<input type="radio" class="tab-radio" id="tab1" name="tab" checked="checked"/>' +
78 '<input type="radio" class="tab-radio" id="tab2" name="tab"/>' +
89 '<div id="tab-links" class="tab-links" style="display:none">' +
910 '<label for="tab1" id="write-tab-link" class="tab1-link">' +
1011 req._t('post.Write') + '</label>' +
1112 '<label for="tab2" id="preview-tab-link" class="tab2-link">' +
1213 req._t('post.Preview') + '</label>' +
1314 '</div>' +
1415 (repo ?
15- '<input type="hidden" id="repo-id" value="' + repo.id + '"/>'
16 + '<input type="hidden" id="repo-id" class="repo-id" value="' + repo.id + '"/>'
1617 : '') +
1718 '<div id="write-tab" class="tab1">' +
18- '<textarea id="post-text" name="text" class="wide-input"' +
19 + '<textarea id="post-text" class="post-text" name="text" class="wide-input"' +
1920 ' rows="' + (rows||4) + '" cols="77"' +
2021 (placeholder ? ' placeholder="' + placeholder + '"' : '') +
2122 '></textarea>' +
2223 '</div>' +
23- '<div class="preview-text tab2" id="preview-tab"></div>' +
24 + '<div class="preview-text tab2" id="preview-tab"></div></div>' +
2425 '<script>' + issueCommentScript + '</script>'
2526 }
2627
2728 forms.name = function (req, enabled, id, name, action, inputId, title, header) {
@@ -53,19 +54,21 @@
5354 ])
5455 }
5556
5657 var issueCommentScript = '(' + function () {
57- var $ = document.getElementById.bind(document)
58- $('tab-links').style.display = 'block'
59- $('preview-tab-link').onclick = function (e) {
58 + var container = [].slice.call(document.querySelectorAll('.post-form')).pop() || document.body
59 + var $ = container.querySelector.bind(container)
60 + $('.tab-links').style.display = 'block'
61 + $('.tab2-link').onclick = function (e) {
6062 with (new XMLHttpRequest()) {
6163 open('POST', '', true)
6264 onload = function() {
63- $('preview-tab').innerHTML = responseText
65 + $('.preview-text').innerHTML = responseText
6466 }
67 + var repoInput = $('.repo-id')
6568 send('action=markdown' +
66- '&repo=' + encodeURIComponent($('repo-id').value) +
67- '&text=' + encodeURIComponent($('post-text').value))
69 + (repoInput ? '&repo=' + encodeURIComponent(repoInput.value) : '') +
70 + '&text=' + encodeURIComponent($('.post-text').value))
6871 }
6972 }
7073 }.toString() + ')()'
7174
@@ -102,12 +105,12 @@
102105 '<script>' + issueCommentButtonScript + '</script>' +
103106 '</form></section>'
104107 }
105108
106-forms.lineComment = function (req, repo, repoBranch, commitId, filePath, line) {
109 +forms.lineComment = function (req, repo, updateId, commitId, filePath, line) {
107110 return '<section><form action="" method="post">' +
108111 '<input type="hidden" name="action" value="line-comment">' +
109- '<input type="hidden" name="repoBranch" value="' + repoBranch.join(',') + '">' +
112 + '<input type="hidden" name="updateId" value="' + updateId + '">' +
110113 '<input type="hidden" name="repo" value="' + repo.id + '">' +
111114 '<input type="hidden" name="commitId" value="' + commitId + '">' +
112115 '<input type="hidden" name="filePath" value="' + filePath + '">' +
113116 '<input type="hidden" name="line" value="' + line + '">' +
@@ -115,4 +118,15 @@
115118 '<input type="submit" class="btn open" value="' +
116119 req._t('issue.LineComment') + '" />' +
117120 '</form></section>'
118121 }
122 +
123 +forms.lineCommentReply = function (req, root, branch) {
124 + return '<section><form action="" method="post">' +
125 + '<input type="hidden" name="action" value="line-comment-reply">' +
126 + '<input type="hidden" name="root" value="' + root + '">' +
127 + '<input type="hidden" name="branch" value="' + branch + '">' +
128 + forms.post(req) +
129 + '<input type="submit" class="btn open" value="' +
130 + req._t('Reply') + '" />' +
131 + '</form></section>'
132 +}
lib/repos/index.jsView
@@ -14,8 +14,9 @@
1414 var ssbRef = require('ssb-ref')
1515 var zlib = require('zlib')
1616 var toPull = require('stream-to-pull-stream')
1717 var h = require('pull-hyperscript')
18 +var getObjectMsgId = require('../../lib/obj-msg-id')
1819
1920 function extend(obj, props) {
2021 for (var k in props)
2122 obj[k] = props[k]
@@ -45,8 +46,115 @@
4546 }
4647
4748 /* Repo */
4849
50 +R.getLineCommentThreads = function (req, repo, updateId, commitId, filename, cb) {
51 + var self = this
52 + var sbot = self.web.ssb
53 + var lineCommentThreads = {}
54 + pull(
55 + sbot.backlinks ? sbot.backlinks.read({
56 + query: [
57 + {$filter: {
58 + dest: updateId,
59 + value: {
60 + content: {
61 + type: 'line-comment',
62 + repo: repo.id,
63 + updateId: updateId,
64 + commitId: commitId,
65 + filePath: filename
66 + }
67 + }
68 + }}
69 + ]
70 + }) : pull(
71 + sbot.links({
72 + dest: updateId,
73 + rel: 'updateId',
74 + values: true
75 + }),
76 + pull.filter(function (msg) {
77 + var c = msg && msg.value && msg.value.content
78 + return c && c.type === 'line-comment'
79 + && c.updateId === updateId
80 + && c.commitId === commitId
81 + && c.filePath === filename
82 + })
83 + ),
84 + paramap(function (msg, cb) {
85 + pull(
86 + self.renderThread(req, repo, msg),
87 + pull.collect(function (err, parts) {
88 + if (err) return cb(err)
89 + cb(null, {
90 + line: msg.value.content.line,
91 + html: parts.join(''),
92 + })
93 + })
94 + )
95 + }, 4),
96 + pull.drain(function (thread) {
97 + lineCommentThreads[thread.line] = thread.html
98 + }, function (err) {
99 + if (err) return cb(err)
100 + cb(null, lineCommentThreads)
101 + })
102 + )
103 +}
104 +
105 +R.renderThread = function (req, repo, msg) {
106 + var newestMsg = msg
107 + var root = msg.key
108 + var self = this
109 + return h('div', [
110 + pull(
111 + cat([
112 + pull.once(msg),
113 + self.web.ssb.links({
114 + dest: root,
115 + values: true
116 + }),
117 + ]),
118 + pull.unique('key'),
119 + u.decryptMessages(self.web.ssb),
120 + u.readableMessages(),
121 + self.web.addAuthorName(),
122 + u.sortMsgs(),
123 + pull.filter(function (msg) {
124 + var c = msg && msg.value && msg.value.content
125 + return c && (c.type === 'post'
126 + || msg.key === root)
127 + }),
128 + pull.through(function (msg) {
129 + // TODO: correctly calculate the thread branches
130 + if (msg.value
131 + && msg.value.timestamp > newestMsg.value.timestamp)
132 + newestMsg = msg
133 + }),
134 + pull.map(function (msg) {
135 + return self.renderLineComment(req, repo, msg)
136 + })
137 + ),
138 + self.web.isPublic ? '' :
139 + pull.once(forms.lineCommentReply(req, root, newestMsg.key))
140 + ])
141 +}
142 +
143 +R.renderLineComment = function (req, repo, msg) {
144 + var c = msg && msg.value && msg.value.content
145 + return h('section', {class: 'collapse'}, [
146 + h('div', [
147 + u.link([msg.value.author], msg.authorName),
148 + ' ',
149 + h('tt', {class: 'right-bar item-id'}, msg.key),
150 + ' &middot; ',
151 + h('a', {href: u.encodeLink(msg.key) + '#' + encodeURIComponent(msg.key)}, new Date(msg.value.timestamp).toLocaleString(req._locale)),
152 + ]),
153 + markdown(c.text, repo)
154 + ])
155 +}
156 +
49157 R.serveRepoPage = function (req, repo, path) {
50158 var self = this
51159 var defaultBranch = 'master'
52160 var query = req._u.query
@@ -593,46 +701,50 @@
593701 /* Repo commit */
594702
595703 R.serveRepoCommit = function (req, repo, rev) {
596704 var self = this
597- var repoBranch = []
598705 return u.readNext(function (cb) {
599706 repo.getCommitParsed(rev, function (err, commit) {
600707 if (err) return cb(null,
601708 self.serveRepoTemplate(req, repo, null, rev, `%{author}/%{repo}@${rev}`,
602709 pull.once(self.web.renderError(err))))
603- var commitPath = [repo.id, 'commit', commit.id]
604- var treePath = [repo.id, 'tree', commit.id]
605- var title = u.escape(commit.title) + ' · ' +
606- '%{author}/%{repo}@' + commit.id.substr(0, 8)
607- cb(null, self.serveRepoTemplate(req, repo, null, rev, title, cat([
608- pull.once(
609- '<h3>' + u.link(commitPath,
610- req._t('CommitRev', {rev: rev})) + '</h3>' +
611- '<section class="collapse">' +
612- '<div class="right-bar">' +
613- u.link(treePath, req._t('BrowseFiles')) +
614- '</div>' +
615- '<h4>' + u.linkify(u.escape(commit.title)) + '</h4>' +
616- (commit.body ? u.linkify(u.pre(commit.body)) : '') +
617- (commit.separateAuthor ? req._t('AuthoredOn', {
618- name: u.escape(commit.author.name),
619- date: commit.author.date.toLocaleString(req._locale)
620- }) + '<br/>' : '') +
621- req._t('CommittedOn', {
622- name: u.escape(commit.committer.name),
623- date: commit.committer.date.toLocaleString(req._locale)
624- }) + '<br/>' +
625- commit.parents.map(function (id) {
626- return req._t('Parent') + ': ' +
627- u.link([repo.id, 'commit', id], id)
628- }).join('<br>') +
629- '</section>' +
630- '<section><h3>' + req._t('FilesChanged') + '</h3>'),
631- // TODO: show diff from all parents (merge commits)
632- self.renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id], commit.id, repoBranch),
633- pull.once('</section>')
634- ])))
710 + getObjectMsgId(repo, commit.id, function (err, objMsgId) {
711 + if (err) return cb(null,
712 + self.serveRepoTemplate(req, repo, null, rev, `%{author}/%{repo}@${rev}`,
713 + pull.once(self.web.renderError(err))))
714 + var commitPath = [repo.id, 'commit', commit.id]
715 + var treePath = [repo.id, 'tree', commit.id]
716 + var title = u.escape(commit.title) + ' · ' +
717 + '%{author}/%{repo}@' + commit.id.substr(0, 8)
718 + cb(null, self.serveRepoTemplate(req, repo, null, rev, title, cat([
719 + pull.once(
720 + '<h3>' + u.link(commitPath,
721 + req._t('CommitRev', {rev: rev})) + '</h3>' +
722 + '<section class="collapse">' +
723 + '<div class="right-bar">' +
724 + u.link(treePath, req._t('BrowseFiles')) +
725 + '</div>' +
726 + '<h4>' + u.linkify(u.escape(commit.title)) + '</h4>' +
727 + (commit.body ? u.linkify(u.pre(commit.body)) : '') +
728 + (commit.separateAuthor ? req._t('AuthoredOn', {
729 + name: u.escape(commit.author.name),
730 + date: commit.author.date.toLocaleString(req._locale)
731 + }) + '<br/>' : '') +
732 + req._t('CommittedOn', {
733 + name: u.escape(commit.committer.name),
734 + date: commit.committer.date.toLocaleString(req._locale)
735 + }) + '<br/>' +
736 + commit.parents.map(function (id) {
737 + return req._t('Parent') + ': ' +
738 + u.link([repo.id, 'commit', id], id)
739 + }).join('<br>') +
740 + '</section>' +
741 + '<section><h3>' + req._t('FilesChanged') + '</h3>'),
742 + // TODO: show diff from all parents (merge commits)
743 + self.renderDiffStat(req, [repo, repo], [commit.parents[0], commit.id], commit.id, objMsgId),
744 + pull.once('</section>')
745 + ])))
746 + })
635747 })
636748 })
637749 }
638750
@@ -673,9 +785,10 @@
673785
674786
675787 /* Diff stat */
676788
677-R.renderDiffStat = function (req, repos, treeIds, commit, repoBranch) {
789 +R.renderDiffStat = function (req, repos, treeIds, commit, updateId) {
790 + var self = this
678791 if (treeIds.length == 0) treeIds = [null]
679792 var id = treeIds[0]
680793 var lastI = treeIds.length - 1
681794 var oldTree = treeIds[0]
@@ -734,25 +847,26 @@
734847 var done = multicb({ pluck: 1, spread: true })
735848 var mode0 = item.mode && item.mode[0]
736849 var modeI = item.mode && item.mode[lastI]
737850 var isSubmodule = (modeI == 0160000)
851 + var repo = repos[1]
738852 getRepoObjectString(repos[0], item.id[0], mode0, done())
739853 getRepoObjectString(repos[1], item.id[lastI], modeI, done())
740- done(function (err, strOld, strNew) {
854 + self.getLineCommentThreads(req, repo, updateId, commit, item.filename, done())
855 + done(function (err, strOld, strNew, lineCommentThreads) {
741856 if (err) return cb(err)
742- var repo = repos[1]
743- cb(null, htmlLineDiff(req, repo, repoBranch, commit, item.filename, item.filename,
857 + cb(null, htmlLineDiff(req, repo, updateId, commit, item.filename, item.filename,
744858 strOld, strNew,
745- u.encodeLink(item.blobPath), !isSubmodule))
859 + u.encodeLink(item.blobPath), !isSubmodule, lineCommentThreads))
746860 })
747861 }, 4)
748862 )
749863 ])
750864 }
751865
752-function htmlLineDiff(req, repo, repoBranch, commit, filename, anchor, oldStr, newStr, blobHref,
753- showViewLink) {
754- return '<pre><table class="code">' +
866 +function htmlLineDiff(req, repo, updateId, commit, filename, anchor, oldStr, newStr, blobHref,
867 + showViewLink, lineCommentThreads) {
868 + return '<div class="code-wrap"><table class="code">' +
755869 '<tr><th colspan=3 id="' + u.escape(anchor) + '">' + filename +
756870 (showViewLink === false ? '' :
757871 '<span class="right-bar">' +
758872 '<a href="' + blobHref + '">' + req._t('View') + '</a> ' +
@@ -761,13 +875,13 @@
761875 (oldStr.length + newStr.length > 200000
762876 ? '<tr><td class="diff-info" colspan=3>' + req._t('diff.TooLarge') + '<br>' +
763877 req._t('diff.OldFileSize', {bytes: oldStr.length}) + '<br>' +
764878 req._t('diff.NewFileSize', {bytes: newStr.length}) + '</td></tr>'
765- : tableDiff(req, repo, repoBranch, commit, oldStr, newStr, filename)) +
766- '</table></pre>'
879 + : tableDiff(req, repo, updateId, commit, oldStr, newStr, filename, lineCommentThreads)) +
880 + '</table></div>'
767881 }
768882
769-function tableDiff(req, repo, repoBranch, commit, oldStr, newStr, filename) {
883 +function tableDiff(req, repo, updateId, commit, oldStr, newStr, filename, lineCommentThreads) {
770884 var query = req._u.query
771885 var diff = JsDiff.structuredPatch('', '', oldStr, newStr)
772886 var groups = diff.hunks.map(function (hunk) {
773887 var oldLine = hunk.oldStart
@@ -782,25 +896,31 @@
782896 var html = u.highlight(line, u.getExtension(filename))
783897 var trClass = s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
784898 var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
785899 var id = [filename].concat(lineNums).join('-')
900 + var newLineNum = lineNums[lineNums.length-1]
786901 return '<tr id="' + u.escape(id) + '" class="' + trClass + '">' +
787902 lineNums.map(function (num, i) {
788903 var idEnc = encodeURIComponent(id)
789904 return '<td class="code-linenum">' +
790905 (num ? '<a href="#' + idEnc + '">' +
791906 num + '</a>' +
792- (i === lineNums.length-1 && s !== '-' ?
907 + (updateId && i === lineNums.length-1 && s !== '-' ?
793908 // TODO: use a more descriptive icon for the comment action
794909 ' <a href="?comment=' + idEnc + '#' + idEnc + '">…</a>'
795910 : '')
796911 : '') + '</td>'
797912 }).join('') +
798913 '<td class="code-text">' + html + '</td></tr>' +
799914 (commit && query.comment === id ?
800915 '<tr><td colspan=4>' +
801- forms.lineComment(req, repo, repoBranch, commit, filename, lineNums[lineNums.length-1]) +
916 + forms.lineComment(req, repo, updateId, commit, filename, newLineNum) +
802917 '</td></tr>'
918 + : '') +
919 + (lineCommentThreads[newLineNum] ?
920 + '<tr><td colspan=4>' +
921 + lineCommentThreads[newLineNum] +
922 + '</td></tr>'
803923 : '')
804924 }))
805925 })
806926 return [].concat.apply([], groups).join('')
lib/repos/pulls.jsView
@@ -7,8 +7,9 @@
77 var u = require('../../lib/util')
88 var markdown = require('../../lib/markdown')
99 var forms = require('../../lib/forms')
1010 var ssbRef = require('ssb-ref')
11 +var getObjectMsgId = require('../../lib/obj-msg-id')
1112
1213 module.exports = function (repoRoutes, web) {
1314 return new RepoPullReqRoutes(repoRoutes, web)
1415 }
@@ -112,16 +113,18 @@
112113 if (!revs.head) return cb(null, pull.once('<section>' +
113114 req._t('pullRequest.NoChanges') + '</section>'))
114115 GitRepo.getMergeBase(baseRepo, revs.base, headRepo, revs.head,
115116 function (err, mergeBase) {
116- var repoBranch = []
117117 if (err) return cb(err)
118- cb(null, cat([
119- pull.once('<section>'),
120- self.repo.renderDiffStat(req,
121- [baseRepo, headRepo], [mergeBase, revs.head], revs.head, repoBranch),
122- pull.once('</section>')
123- ]))
118 + getObjectMsgId(headRepo, revs.head, function (err, objMsgId) {
119 + if (err) return cb(err)
120 + cb(null, cat([
121 + pull.once('<section>'),
122 + self.repo.renderDiffStat(req,
123 + [baseRepo, headRepo], [mergeBase, revs.head], revs.head, objMsgId),
124 + pull.once('</section>')
125 + ]))
126 + })
124127 }
125128 )
126129 })
127130 }
locale/en.jsonView
@@ -136,8 +136,9 @@
136136 "CommentAndClose": "Comment and Close",
137137 "Reopen": "Reopen %{type}",
138138 "CommentAndReopen": "Comment and Reopen"
139139 },
140 + "Reply": "Reply",
140141 "pullRequest": {
141142 "WantToMerge": "%{name} wants to merge commits into %{base} from %{head}",
142143 "Discussion": "Discussion",
143144 "New": "New Pull Request",
locale/eo.jsonView
@@ -136,8 +136,9 @@
136136 "CommentAndClose": "Komenti kaj Fermi",
137137 "Reopen": "Remalfermi %{type}",
138138 "CommandAndReopen": "Komenti kaj Remalfermi"
139139 },
140 + "Reply": "Reply",
140141 "pullRequest": {
141142 "WantToMerge": "%{name} volas kunfandi enmetoj en %{base} de %{head}",
142143 "Discussion": "Priparolado",
143144 "New": "Nova Tiro-peto",
schemas.mdView
@@ -4,14 +4,14 @@
44 {
55 "type": "line-comment",
66 "text": string,
77 "repo": MsgId,
8- "repoBranch": MgsIds,
8 + "updateId": MgsIds,
99 "line": number,
1010 }
1111 ```
1212 `repo`: id of `git-repo`
13-`repoBranch`: id(s) of `git-update` messages that pushed the git commit, git trees, and git blobs needed to render the diff.
13 +`updateId`: id of a `git-update` message that pushed the git commit, git trees, and git blobs needed to render the diff.
1414 `commitId`: commit that the comment is on
1515 `filePath`: path to the file that the comment is on
1616 `line`: line number of the file that the comment is on
1717
static/styles.cssView
@@ -17,8 +17,21 @@
1717 margin-left: auto;
1818 margin-right: auto;
1919 }
2020
21 +.code-wrap {
22 + padding: .5em 1.25ex;
23 + border: 1px solid #ddd;
24 + background: #f5f5f5;
25 +}
26 +
27 +.diff-hunk-header,
28 +.code-linenum,
29 +.code-text {
30 + font: .8em Consolas, "Liberation Mono", Menlo, Courier, monospace;
31 + white-space: pre-wrap;
32 +}
33 +
2134 pre {
2235 font: .8em Consolas, "Liberation Mono", Menlo, Courier, monospace;
2336 padding: .5em 1.25ex;
2437 border: 1px solid #ddd;
@@ -31,8 +44,9 @@
3144 border-radius: .5ex;
3245 background-color: #fff;
3346 }
3447
48 +.code-wrap > code,
3549 pre > code {
3650 padding: 0;
3751 background-color: #f5f5f5;
3852 }

Built with git-ssb-web