git ssb

30+

cel / git-ssb-web



Commit 15209016c9f99db95578158f7ba1bef4f4894a9b

Add pull requests

Close %o0pjXlk5xdM0mNfP9L7yPueeY+7ZL2mPwiYyFwkpDfE=.sha256
Charles Lehner committed on 4/11/2016, 2:02:59 AM
Parent: 97ef971b5aaa29e8f2c7613f765d91b7f6f2d615

Files changed

index.jschanged
package.jsonchanged
static/styles.csschanged
index.jsView
@@ -15,13 +15,15 @@
1515 var asyncMemo = require('asyncmemo')
1616 var multicb = require('multicb')
1717 var schemas = require('ssb-msg-schemas')
1818 var Issues = require('ssb-issues')
19 +var PullRequests = require('ssb-pull-requests')
1920 var paramap = require('pull-paramap')
2021 var gitPack = require('pull-git-pack')
2122 var Mentions = require('ssb-mentions')
2223 var Highlight = require('highlight.js')
2324 var JsDiff = require('diff')
25 +var many = require('pull-many')
2426
2527 var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles')
2628
2729 // render links to git objects and ssb objects
@@ -211,8 +213,16 @@
211213 '<div class="preview-text tab2" id="preview-tab"></div>' +
212214 '<script>' + issueCommentScript + '</script>'
213215 }
214216
217 +function hiddenInputs(values) {
218 + return Object.keys(values).map(function (key) {
219 + return '<input type="hidden"' +
220 + ' name="' + escapeHTML(key) + '"' +
221 + ' value="' + escapeHTML(values[key]) + '"/>'
222 + }).join('')
223 +}
224 +
215225 function readNext(fn) {
216226 var next
217227 return function (end, cb) {
218228 if (next) return next(end, cb)
@@ -343,11 +353,20 @@
343353 }
344354 }, cb)
345355 }
346356
357 +function getRepoFullName(about, author, repoId, cb) {
358 + var done = multicb({ pluck: 1, spread: true })
359 + getRepoName(about, author, repoId, done())
360 + about.getName(author, done())
361 + done(cb)
362 +}
363 +
347364 function addAuthorName(about) {
348365 return paramap(function (msg, cb) {
349- about.getName(msg.value.author, function (err, authorName) {
366 + var author = msg && msg.value && msg.value.author
367 + if (!author) return cb(null, msg)
368 + about.getName(author, function (err, authorName) {
350369 msg.authorName = authorName
351370 cb(err, msg)
352371 })
353372 }, 8)
@@ -409,9 +428,10 @@
409428
410429 var msgTypes = {
411430 'git-repo': true,
412431 'git-update': true,
413- 'issue': true
432 + 'issue': true,
433 + 'pull-request': true
414434 }
415435
416436 var imgMimes = {
417437 png: 'image/png',
@@ -451,8 +471,9 @@
451471 })
452472 getVotes = ssbVotes(ssb)
453473 getMsg = asyncMemo(ssb.get)
454474 issues = Issues.init(ssb)
475 + pullReqs = PullRequests.init(ssb)
455476 })
456477 }
457478 }
458479
@@ -572,8 +593,19 @@
572593 if (err) return cb(null, serveError(err))
573594 cb(null, serveRedirect(encodeLink(msg.key)))
574595 })
575596
597 + case 'new-pull':
598 + var msg = PullRequests.schemas.new(dir, data.branch,
599 + data.head_repo, data.head_branch, data.title, data.text)
600 + var mentions = Mentions(data.text)
601 + if (mentions.length)
602 + msg.mentions = mentions
603 + return ssb.publish(msg, function (err, msg) {
604 + if (err) return cb(null, serveError(err))
605 + cb(null, serveRedirect(encodeLink(msg.key)))
606 + })
607 +
576608 case 'markdown':
577609 return cb(null, serveMarkdown(data.text, {id: data.repo}))
578610
579611 default:
@@ -817,14 +849,15 @@
817849 cb(null, '<section class="collapse">' + msgLink + '<br>' +
818850 authorLink + ' pushed to ' + repoLink + '</section>')
819851 })
820852 case 'issue':
853 + case 'pull-request':
821854 var issueLink = link([msg.key], c.title)
822855 return getRepoName(about, author, c.project, function (err, repoName) {
823856 if (err) return cb(err)
824857 var repoLink = link([c.project], repoName)
825858 cb(null, '<section class="collapse">' + msgLink + '<br>' +
826- authorLink + ' opened issue ' + issueLink +
859 + authorLink + ' opened ' + c.type + ' ' + issueLink +
827860 ' on ' + repoLink + '</section>')
828861 })
829862 }
830863 }
@@ -912,19 +945,45 @@
912945 if (err) return cb(null, serveError(err))
913946 cb(null, serveRepoIssue(req, Repo(repo), issue, path))
914947 })
915948 })
949 + case 'pull-request':
950 + return getRepo(c.repo, function (err, repo) {
951 + if (err) return cb(null, serveRepoNotFound(c.project, err))
952 + pullReqs.get(id, function (err, pr) {
953 + if (err) return cb(null, serveError(err))
954 + cb(null, serveRepoPullReq(req, Repo(repo), pr, path))
955 + })
956 + })
957 + case 'issue-edit':
958 + if (ref.isMsgId(c.issue)) {
959 + return pullReqs.get(c.issue, function (err, issue) {
960 + if (err) return cb(err)
961 + var serve = issue.msg.value.content.type == 'pull-request' ?
962 + serveRepoPullReq : serveRepoIssue
963 + getRepo(issue.project, function (err, repo) {
964 + if (err) {
965 + if (!repo) return cb(null, serveRepoNotFound(c.repo, err))
966 + return cb(null, serveError(err))
967 + }
968 + cb(null, serve(req, Repo(repo), issue, path, id))
969 + })
970 + })
971 + }
972 + // fallthrough
916973 case 'post':
917974 if (ref.isMsgId(c.issue) && ref.isMsgId(c.repo)) {
918975 var done = multicb({ pluck: 1, spread: true })
919976 getRepo(c.repo, done())
920- issues.get(c.issue, done())
977 + pullReqs.get(c.issue, done())
921978 return done(function (err, repo, issue) {
922979 if (err) {
923980 if (!repo) return cb(null, serveRepoNotFound(c.repo, err))
924981 return cb(null, serveError(err))
925982 }
926- cb(null, serveRepoIssue(req, Repo(repo), issue, path, id))
983 + var serve = issue.msg.value.content.type == 'pull-request' ?
984 + serveRepoPullReq : serveRepoIssue
985 + cb(null, serve(req, Repo(repo), issue, path, id))
927986 })
928987 }
929988 // fallthrough
930989 default:
@@ -1008,10 +1067,16 @@
10081067 if (filePath.length == 0)
10091068 return serveRepoNewIssue(repo)
10101069 break
10111070 default:
1012- return serveRepoIssues(req, repo, branch, filePath)
1071 + return serveRepoIssues(req, repo, filePath)
10131072 }
1073 + case 'pulls':
1074 + return serveRepoPullReqs(req, repo)
1075 + case 'compare':
1076 + return serveRepoCompare(req, repo)
1077 + case 'comparing':
1078 + return serveRepoComparing(req, repo)
10141079 default:
10151080 return serve404(req)
10161081 }
10171082 }
@@ -1026,9 +1091,9 @@
10261091
10271092 function renderRepoPage(repo, page, branch, body) {
10281093 var gitUrl = 'ssb://' + repo.id
10291094 var gitLink = '<input class="clone-url" readonly="readonly" ' +
1030- 'value="' + gitUrl + '" size="61" ' +
1095 + 'value="' + gitUrl + '" size="45" ' +
10311096 'onclick="this.select()"/>'
10321097 var digsPath = [repo.id, 'digs']
10331098
10341099 var done = multicb({ pluck: 1, spread: true })
@@ -1069,9 +1134,9 @@
10691134 link([repo.id, 'forks'], '+', false, ' title="Forks"') +
10701135 '</form>' +
10711136 renderNameForm(!isPublic, repo.id, repoName, 'repo-name', null,
10721137 'Rename the repo',
1073- '<h2>' + link([repo.feed], authorName) + ' / ' +
1138 + '<h2 class="bgslash">' + link([repo.feed], authorName) + ' / ' +
10741139 link([repo.id], repoName) + '</h2>') +
10751140 '</div>' +
10761141 (repo.upstream ?
10771142 '<small>forked from ' +
@@ -1081,9 +1146,10 @@
10811146 nav([
10821147 [[repo.id], 'Code', 'code'],
10831148 [[repo.id, 'activity'], 'Activity', 'activity'],
10841149 [[repo.id, 'commits', branch || ''], 'Commits', 'commits'],
1085- [[repo.id, 'issues'], 'Issues', 'issues']
1150 + [[repo.id, 'issues'], 'Issues', 'issues'],
1151 + [[repo.id, 'pulls'], 'Pull Requests', 'pulls']
10861152 ], page, gitLink)),
10871153 body
10881154 ])))
10891155 })
@@ -1136,9 +1202,8 @@
11361202 return renderRepoPage(repo, 'activity', branch, cat([
11371203 pull.once('<h3>Activity</h3>'),
11381204 pull(
11391205 ssb.links({
1140- type: 'git-update',
11411206 dest: repo.id,
11421207 source: repo.feed,
11431208 rel: 'repo',
11441209 values: true,
@@ -1164,8 +1229,14 @@
11641229
11651230 function renderRepoUpdate(repo, msg, full) {
11661231 var c = msg.value.content
11671232
1233 + if (c.type != 'git-update') {
1234 + return ''
1235 + // return renderFeedItem(msg, cb)
1236 + // TODO: render post, issue, pull-request
1237 + }
1238 +
11681239 var refs = c.refs ? Object.keys(c.refs).map(function (ref) {
11691240 return {name: ref, value: c.refs[ref]}
11701241 }) : []
11711242 var numObjects = c.objects ? Object.keys(c.objects).length : 0
@@ -1222,29 +1293,53 @@
12221293 (commit.separateAuthor ? '<br>' + escapeHTML(commit.committer.name) + ' committed on ' + commit.committer.date.toLocaleString() : "") +
12231294 '</section>'
12241295 }
12251296
1226- /* Repo tree */
1297 + /* Branch menu */
12271298
1299 + function formatRevOptions(currentName) {
1300 + return function (name) {
1301 + var htmlName = escapeHTML(name)
1302 + return '<option value="' + htmlName + '"' +
1303 + (name == currentName ? ' selected="selected"' : '') +
1304 + '>' + htmlName + '</option>'
1305 + }
1306 + }
1307 +
12281308 function revMenu(repo, currentName) {
12291309 return readOnce(function (cb) {
12301310 repo.getRefNames(true, function (err, refs) {
12311311 if (err) return cb(err)
12321312 cb(null, '<select name="rev" onchange="this.form.submit()">' +
12331313 Object.keys(refs).map(function (group) {
12341314 return '<optgroup label="' + group + '">' +
1235- refs[group].map(function (name) {
1236- var htmlName = escapeHTML(name)
1237- return '<option value="' + htmlName + '"' +
1238- (name == currentName ? ' selected="selected"' : '') +
1239- '>' + htmlName + '</option>'
1240- }).join('') + '</optgroup>'
1315 + refs[group].map(formatRevOptions(currentName)).join('') +
1316 + '</optgroup>'
12411317 }).join('') +
12421318 '</select><noscript> <input type="submit" value="Go"/></noscript>')
12431319 })
12441320 })
12451321 }
12461322
1323 + function branchMenu(repo, name, currentName) {
1324 + return cat([
1325 + pull.once('<select name="' + name + '">'),
1326 + pull(
1327 + repo.refs(),
1328 + pull.map(function (ref) {
1329 + var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
1330 + return m[1] == 'heads' && m[2]
1331 + }),
1332 + pull.filter(Boolean),
1333 + pullSort(),
1334 + pull.map(formatRevOptions(currentName))
1335 + ),
1336 + pull.once('</select>')
1337 + ])
1338 + }
1339 +
1340 + /* Repo tree */
1341 +
12471342 function renderRepoLatest(repo, rev) {
12481343 return readOnce(function (cb) {
12491344 repo.getCommitParsed(rev, function (err, commit) {
12501345 if (err) return cb(err)
@@ -1345,27 +1440,30 @@
13451440 commit.committer.date.toLocaleString() + '<br/>' +
13461441 commit.parents.map(function (id) {
13471442 return 'Parent: ' + link([repo.id, 'commit', id], id)
13481443 }).join('<br>') +
1349- '</section>'),
1350- renderDiffStat(repo, commit.id, commit.parents)
1444 + '</section>' +
1445 + '<section><h3>Files changed</h3>'),
1446 + // TODO: show diff from all parents (merge commits)
1447 + renderDiffStat([repo, repo], [commit.parents[0], commit.id]),
1448 + pull.once('</section>')
13511449 ]))
13521450 })
13531451 })
13541452 ]))
13551453 }
13561454
13571455 /* Diff stat */
13581456
1359- function renderDiffStat(repo, id, parentIds) {
1360- if (parentIds.length == 0) parentIds = [null]
1361- var lastI = parentIds.length
1362- var oldTree = parentIds[0]
1457 + function renderDiffStat(repos, treeIds) {
1458 + if (treeIds.length == 0) treeIds = [null]
1459 + var id = treeIds[0]
1460 + var lastI = treeIds.length - 1
1461 + var oldTree = treeIds[0]
13631462 var changedFiles = []
13641463 return cat([
1365- pull.once('<section><h3>Files changed</h3>'),
13661464 pull(
1367- repo.diffTrees(parentIds.concat(id), true),
1465 + Repo.diffTrees(repos, treeIds, true),
13681466 pull.map(function (item) {
13691467 var filename = item.filename = escapeHTML(item.path.join('/'))
13701468 var oldId = item.id && item.id[0]
13711469 var newId = item.id && item.id[lastI]
@@ -1381,29 +1479,28 @@
13811479 if (item.id)
13821480 changedFiles.push(item)
13831481 var fileHref = item.id ?
13841482 '#' + encodeURIComponent(item.path.join('/')) :
1385- encodeLink([repo.id, 'blob', id].concat(item.path))
1483 + encodeLink([repos[0].id, 'blob', id].concat(item.path))
13861484 return ['<a href="' + fileHref + '">' + filename + '</a>', action]
13871485 }),
13881486 table()
13891487 ),
13901488 pull(
13911489 pull.values(changedFiles),
13921490 paramap(function (item, cb) {
13931491 var done = multicb({ pluck: 1, spread: true })
1394- getRepoObjectString(repo, item.id[0], done())
1395- getRepoObjectString(repo, item.id[lastI], done())
1492 + getRepoObjectString(repos[0], item.id[0], done())
1493 + getRepoObjectString(repos[1], item.id[lastI], done())
13961494 done(function (err, strOld, strNew) {
13971495 if (err) return cb(err)
1398- var commitId = item.id[lastI] ? id : parentIds.filter(Boolean)[0]
1496 + var commitId = item.id[lastI] ? id : treeIds.filter(Boolean)[0]
13991497 cb(null, htmlLineDiff(item.filename, item.filename,
14001498 strOld, strNew,
1401- encodeLink([repo.id, 'blob', commitId].concat(item.path))))
1499 + encodeLink([repos[0].id, 'blob', commitId].concat(item.path))))
14021500 })
14031501 }, 4)
1404- ),
1405- pull.once('</section>'),
1502 + )
14061503 ])
14071504 }
14081505
14091506 function htmlLineDiff(filename, anchor, oldStr, newStr, blobHref, rawHref) {
@@ -1643,39 +1740,55 @@
16431740 }
16441741
16451742 /* Forks */
16461743
1744 + function getForks(repo, includeSelf) {
1745 + return pull(
1746 + cat([
1747 + includeSelf && readOnce(function (cb) {
1748 + getMsg(repo.id, function (err, value) {
1749 + cb(err, value && {key: repo.id, value: value})
1750 + })
1751 + }),
1752 + ssb.links({
1753 + dest: repo.id,
1754 + values: true,
1755 + rel: 'upstream'
1756 + })
1757 + ]),
1758 + pull.filter(function (msg) {
1759 + return msg.value.content && msg.value.content.type == 'git-repo'
1760 + }),
1761 + paramap(function (msg, cb) {
1762 + getRepoFullName(about, msg.value.author, msg.key,
1763 + function (err, repoName, authorName) {
1764 + if (err) return cb(err)
1765 + cb(null, {
1766 + key: msg.key,
1767 + value: msg.value,
1768 + repoName: repoName,
1769 + authorName: authorName
1770 + })
1771 + })
1772 + }, 8)
1773 + )
1774 + }
1775 +
16471776 function serveRepoForks(repo) {
16481777 var hasForks
16491778 return renderRepoPage(repo, null, null, cat([
16501779 pull.once('<h3>Forks</h3>'),
16511780 pull(
1652- ssb.links({
1653- dest: repo.id,
1654- values: true,
1655- rel: 'upstream'
1656- }),
1657- pull.filter(function (msg) {
1658- var c = msg.value.content
1659- return (c && c.type == 'git-repo')
1660- }),
1661- paramap(function (msg, cb) {
1781 + getForks(repo),
1782 + pull.map(function (msg) {
16621783 hasForks = true
1663- var author = msg.value.author
1664- var done = multicb({ pluck: 1, spread: true })
1665- getRepoName(about, author, msg.key, done())
1666- about.getName(author, done())
1667- done(function (err, repoName, authorName) {
1668- if (err) return cb(err)
1669- var authorLink = link([author], authorName)
1670- var repoLink = link([msg.key], repoName)
1671- cb(null, '<section class="collapse">' +
1672- authorLink + ' / ' + repoLink +
1673- '<span class="right-bar">' +
1674- timestamp(msg.value.timestamp) +
1675- '</span></section>')
1676- })
1677- }, 8)
1784 + return '<section class="collapse">' +
1785 + link([msg.value.author], msg.authorName) + ' / ' +
1786 + link([msg.key], msg.repoName) +
1787 + '<span class="right-bar">' +
1788 + timestamp(msg.value.timestamp) +
1789 + '</span></section>'
1790 + })
16781791 ),
16791792 readOnce(function (cb) {
16801793 cb(null, hasForks ? '' : 'No forks')
16811794 })
@@ -1683,9 +1796,9 @@
16831796 }
16841797
16851798 /* Issues */
16861799
1687- function serveRepoIssues(req, repo, issueId, path) {
1800 + function serveRepoIssues(req, repo, path) {
16881801 var numIssues = 0
16891802 var state = req._u.query.state || 'open'
16901803 return renderRepoPage(repo, 'issues', null, cat([
16911804 pull.once(
@@ -1724,8 +1837,51 @@
17241837 })
17251838 ]))
17261839 }
17271840
1841 + /* Pull Requests */
1842 +
1843 + function serveRepoPullReqs(req, repo) {
1844 + var count = 0
1845 + var state = req._u.query.state || 'open'
1846 + return renderRepoPage(repo, 'pulls', null, cat([
1847 + pull.once(
1848 + (isPublic ? '' :
1849 + '<div class="right-bar">' + link([repo.id, 'compare'],
1850 + '<button class="btn">&plus; New Pull Request</button>', true) +
1851 + '</div>') +
1852 + '<h3>Pull Requests</h3>' +
1853 + nav([
1854 + ['?', 'Open', 'open'],
1855 + ['?state=closed', 'Closed', 'closed'],
1856 + ['?state=all', 'All', 'all']
1857 + ], state)),
1858 + pull(
1859 + pullReqs.list({
1860 + repo: repo.id,
1861 + open: {open: true, closed: false}[state]
1862 + }),
1863 + pull.map(function (issue) {
1864 + count++
1865 + var state = (issue.open ? 'open' : 'closed')
1866 + return '<section class="collapse">' +
1867 + '<i class="issue-state issue-state-' + state + '"' +
1868 + ' title="' + ucfirst(state) + '">◼</i> ' +
1869 + '<a href="' + encodeLink(issue.id) + '">' +
1870 + escapeHTML(issue.title) +
1871 + '<span class="right-bar">' +
1872 + new Date(issue.created_at).toLocaleString() +
1873 + '</span>' +
1874 + '</a>' +
1875 + '</section>'
1876 + })
1877 + ),
1878 + readOnce(function (cb) {
1879 + cb(null, count > 0 ? '' : '<p>No pull requests</p>')
1880 + })
1881 + ]))
1882 + }
1883 +
17281884 /* New Issue */
17291885
17301886 function serveRepoNewIssue(repo, issueId, path) {
17311887 return renderRepoPage(repo, 'issues', null, pull.once(
@@ -1756,15 +1912,13 @@
17561912 readOnce(function (cb) {
17571913 about.getName(issue.author, function (err, authorName) {
17581914 if (err) return cb(err)
17591915 var authorLink = link([issue.author], authorName)
1760- cb(null,
1761- authorLink + ' opened this issue on ' + timestamp(issue.created_at) +
1762- '<hr/>' +
1763- markdown(issue.text, repo) +
1764- '</section>')
1916 + cb(null, authorLink + ' opened this issue on ' +
1917 + timestamp(issue.created_at))
17651918 })
17661919 }),
1920 + pull.once('<hr/>' + markdown(issue.text, repo) + '</section>'),
17671921 // render posts and edits
17681922 pull(
17691923 ssb.links({
17701924 dest: issue.id,
@@ -1772,101 +1926,453 @@
17721926 }),
17731927 pull.unique('key'),
17741928 addAuthorName(about),
17751929 sortMsgs(),
1776- pull.map(function (msg) {
1777- var authorLink = link([msg.value.author], msg.authorName)
1778- var msgTimeLink = link([msg.key],
1779- new Date(msg.value.timestamp).toLocaleString(), false,
1780- 'name="' + escapeHTML(msg.key) + '"')
1781- var c = msg.value.content
1930 + pull.through(function (msg) {
17821931 if (msg.value.timestamp > newestMsg.value.timestamp)
17831932 newestMsg = msg
1784- switch (c.type) {
1785- case 'post':
1786- if (c.root == issue.id) {
1787- var changed = issues.isStatusChanged(msg, issue)
1788- return '<section class="collapse">' +
1789- (msg.key == postId ? '<div class="highlight">' : '') +
1790- authorLink +
1791- (changed == null ? '' : ' ' + (
1792- changed ? 'reopened this issue' : 'closed this issue')) +
1793- ' &middot; ' + msgTimeLink +
1794- (msg.key == postId ? '</div>' : '') +
1795- markdown(c.text, repo) +
1796- '</section>'
1797- } else {
1798- var text = c.text || (c.type + ' ' + msg.key)
1799- return '<section class="collapse mention-preview">' +
1800- authorLink + ' mentioned this issue in ' +
1801- '<a href="/' + msg.key + '#' + msg.key + '">' +
1802- String(text).substr(0, 140) + '</a>' +
1803- '</section>'
1804- }
1805- case 'issue':
1806- return '<section class="collapse mention-preview">' +
1807- authorLink + ' mentioned this issue in ' +
1808- link([msg.key], String(c.title || msg.key).substr(0, 140)) +
1809- '</section>'
1810- case 'issue-edit':
1811- return '<section class="collapse">' +
1812- (c.title == null ? '' :
1813- authorLink + ' renamed this issue to <q>' +
1814- escapeHTML(c.title) + '</q>') +
1815- ' &middot; ' + msgTimeLink +
1816- '</section>'
1817- case 'git-update':
1818- var mention = issues.getMention(msg, issue)
1819- if (mention) {
1820- var commitLink = link([repo.id, 'commit', mention.object],
1821- mention.label || mention.object)
1822- return '<section class="collapse">' +
1823- authorLink + ' ' +
1824- (mention.open ? 'reopened this issue' :
1825- 'closed this issue') +
1826- ' &middot; ' + msgTimeLink + '<br/>' +
1827- commitLink +
1828- '</section>'
1829- } else if ((mention = getMention(msg, issue.id))) {
1830- var commitLink = link(mention.object ?
1831- [repo.id, 'commit', mention.object] : [msg.key],
1832- mention.label || mention.object || msg.key)
1833- return '<section class="collapse">' +
1834- authorLink + ' mentioned this issue' +
1835- ' &middot; ' + msgTimeLink + '<br/>' +
1836- commitLink +
1837- '</section>'
1838- } else {
1839- // fallthrough
1840- }
1933 + }),
1934 + pull.map(renderIssueActivityMsg.bind(null, repo, issue,
1935 + 'issue', postId))
1936 + ),
1937 + isPublic ? pull.empty() : readOnce(function (cb) {
1938 + cb(null, renderIssueCommentForm(issue, repo, newestMsg.key, isAuthor,
1939 + 'issue'))
1940 + })
1941 + ]))
1942 + }
18411943
1842- default:
1843- return '<section class="collapse">' +
1844- authorLink +
1845- ' &middot; ' + msgTimeLink +
1846- json(c) +
1847- '</section>'
1848- }
1944 + function renderIssueActivityMsg(repo, issue, type, postId, msg) {
1945 + var authorLink = link([msg.value.author], msg.authorName)
1946 + var msgTimeLink = link([msg.key],
1947 + new Date(msg.value.timestamp).toLocaleString(), false,
1948 + 'name="' + escapeHTML(msg.key) + '"')
1949 + var c = msg.value.content
1950 + switch (c.type) {
1951 + case 'post':
1952 + if (c.root == issue.id) {
1953 + var changed = issues.isStatusChanged(msg, issue)
1954 + return '<section class="collapse">' +
1955 + (msg.key == postId ? '<div class="highlight">' : '') +
1956 + authorLink +
1957 + (changed == null ? '' : ' ' + (
1958 + changed ? 'reopened this ' : 'closed this ') + type) +
1959 + ' &middot; ' + msgTimeLink +
1960 + (msg.key == postId ? '</div>' : '') +
1961 + markdown(c.text, repo) +
1962 + '</section>'
1963 + } else {
1964 + var text = c.text || (c.type + ' ' + msg.key)
1965 + return '<section class="collapse mention-preview">' +
1966 + authorLink + ' mentioned this issue in ' +
1967 + '<a href="/' + msg.key + '#' + msg.key + '">' +
1968 + String(text).substr(0, 140) + '</a>' +
1969 + '</section>'
1970 + }
1971 + case 'issue':
1972 + case 'pull-request':
1973 + return '<section class="collapse mention-preview">' +
1974 + authorLink + ' mentioned this ' + type + ' in ' +
1975 + link([msg.key], String(c.title || msg.key).substr(0, 140)) +
1976 + '</section>'
1977 + case 'issue-edit':
1978 + return '<section class="collapse">' +
1979 + (msg.key == postId ? '<div class="highlight">' : '') +
1980 + (c.title == null ? '' :
1981 + authorLink + ' renamed this ' + type + ' to <q>' +
1982 + escapeHTML(c.title) + '</q>') +
1983 + ' &middot; ' + msgTimeLink +
1984 + (msg.key == postId ? '</div>' : '') +
1985 + '</section>'
1986 + case 'git-update':
1987 + var mention = issues.getMention(msg, issue)
1988 + if (mention) {
1989 + var commitLink = link([repo.id, 'commit', mention.object],
1990 + mention.label || mention.object)
1991 + return '<section class="collapse">' +
1992 + authorLink + ' ' +
1993 + (mention.open ? 'reopened this ' :
1994 + 'closed this ') + type +
1995 + ' &middot; ' + msgTimeLink + '<br/>' +
1996 + commitLink +
1997 + '</section>'
1998 + } else if ((mention = getMention(msg, issue.id))) {
1999 + var commitLink = link(mention.object ?
2000 + [repo.id, 'commit', mention.object] : [msg.key],
2001 + mention.label || mention.object || msg.key)
2002 + return '<section class="collapse">' +
2003 + authorLink + ' mentioned this ' + type +
2004 + ' &middot; ' + msgTimeLink + '<br/>' +
2005 + commitLink +
2006 + '</section>'
2007 + } else {
2008 + // fallthrough
2009 + }
2010 +
2011 + default:
2012 + return '<section class="collapse">' +
2013 + authorLink +
2014 + ' &middot; ' + msgTimeLink +
2015 + json(c) +
2016 + '</section>'
2017 + }
2018 + }
2019 +
2020 + function renderIssueCommentForm(issue, repo, branch, isAuthor, type) {
2021 + return '<section><form action="" method="post">' +
2022 + '<input type="hidden" name="action" value="comment">' +
2023 + '<input type="hidden" name="id" value="' + issue.id + '">' +
2024 + '<input type="hidden" name="issue" value="' + issue.id + '">' +
2025 + '<input type="hidden" name="repo" value="' + repo.id + '">' +
2026 + '<input type="hidden" name="branch" value="' + branch + '">' +
2027 + renderPostForm(repo) +
2028 + '<input type="submit" class="btn open" value="Comment" />' +
2029 + (isAuthor ?
2030 + '<input type="submit" class="btn"' +
2031 + ' name="' + (issue.open ? 'close' : 'open') + '"' +
2032 + ' value="' + (issue.open ? 'Close ' : 'Reopen ') + type + '"' +
2033 + '/>' : '') +
2034 + '</form></section>'
2035 + }
2036 +
2037 + /* Pull Request */
2038 +
2039 + function serveRepoPullReq(req, repo, pr, path, postId) {
2040 + var headRepo, authorLink
2041 + var page = path[0] || 'activity'
2042 + return renderRepoPage(repo, 'pulls', null, cat([
2043 + pull.once('<div class="pull-request">' +
2044 + renderNameForm(!isPublic, pr.id, pr.title, 'issue-title', null,
2045 + 'Rename the pull request',
2046 + '<h3>' + link([pr.id], pr.title) + '</h3>') +
2047 + '<code>' + pr.id + '</code>'),
2048 + readOnce(function (cb) {
2049 + var done = multicb({ pluck: 1, spread: true })
2050 + about.getName(pr.author, done())
2051 + var sameRepo = (pr.headRepo == pr.baseRepo)
2052 + getRepo(pr.headRepo, function (err, headRepo) {
2053 + if (err) return cb(err)
2054 + done()(null, headRepo)
2055 + getRepoName(about, headRepo.feed, headRepo.id, done())
2056 + about.getName(headRepo.feed, done())
18492057 })
2058 +
2059 + done(function (err, issueAuthorName, _headRepo,
2060 + headRepoName, headRepoAuthorName) {
2061 + if (err) return cb(err)
2062 + headRepo = _headRepo
2063 + authorLink = link([pr.author], issueAuthorName)
2064 + var repoLink = link([pr.headRepo], headRepoName)
2065 + var headRepoAuthorLink = link([headRepo.feed], headRepoAuthorName)
2066 + var headRepoLink = link([headRepo.id], headRepoName)
2067 + var headBranchLink = link([headRepo.id, 'tree', pr.headBranch])
2068 + var baseBranchLink = link([repo.id, 'tree', pr.baseBranch])
2069 + cb(null, '<section class="collapse">' +
2070 + (pr.open
2071 + ? '<strong class="issue-status open">Open</strong>'
2072 + : '<strong class="issue-status closed">Closed</strong>') +
2073 + authorLink + ' wants to merge commits into ' +
2074 + '<code>' + baseBranchLink + '</code> from ' +
2075 + (sameRepo ? '<code>' + headBranchLink + '</code>' :
2076 + '<code class="bgslash">' +
2077 + headRepoAuthorLink + ' / ' +
2078 + headRepoLink + ' / ' +
2079 + headBranchLink + '</code>') +
2080 + '</section>')
2081 + })
2082 + }),
2083 + pull.once(
2084 + nav([
2085 + [[pr.id], 'Discussion', 'activity'],
2086 + [[pr.id, 'commits'], 'Commits', 'commits'],
2087 + [[pr.id, 'files'], 'Files', 'files']
2088 + ], page)),
2089 + readNext(function (cb) {
2090 + if (page == 'commits') cb(null,
2091 + renderPullReqCommits(pr, repo, headRepo))
2092 + else if (page == 'files') cb(null,
2093 + renderPullReqFiles(pr, repo, headRepo))
2094 + else cb(null,
2095 + renderPullReqActivity(pr, repo, headRepo, authorLink, postId))
2096 + })
2097 + ]))
2098 + }
2099 +
2100 + function renderPullReqCommits(pr, baseRepo, headRepo) {
2101 + return cat([
2102 + pull.once('<section>'),
2103 + renderCommitLog(baseRepo, pr.baseBranch, headRepo, pr.headBranch),
2104 + pull.once('</section>')
2105 + ])
2106 + }
2107 +
2108 + function renderPullReqFiles(pr, baseRepo, headRepo) {
2109 + return cat([
2110 + pull.once('<section>'),
2111 + renderDiffStat([baseRepo, headRepo], [pr.baseBranch, pr.headBranch]),
2112 + pull.once('</section>')
2113 + ])
2114 + }
2115 +
2116 + function renderPullReqActivity(pr, repo, headRepo, authorLink, postId) {
2117 + var msgTimeLink = link([pr.id], new Date(pr.created_at).toLocaleString())
2118 + var newestMsg = {key: pr.id, value: {timestamp: pr.created_at}}
2119 + var isAuthor = (myId == pr.author) || (myId == repo.feed)
2120 + return cat([
2121 + readOnce(function (cb) {
2122 + cb(null,
2123 + '<section class="collapse">' +
2124 + authorLink + ' &middot; ' + msgTimeLink +
2125 + markdown(pr.text, repo) + '</section>')
2126 + }),
2127 + // render posts, edits, and updates
2128 + pull(
2129 + many([
2130 + pull(
2131 + ssb.links({
2132 + dest: pr.id,
2133 + values: true
2134 + }),
2135 + pull.unique('key')
2136 + ),
2137 + readNext(function (cb) {
2138 + cb(null, pull(
2139 + ssb.links({
2140 + dest: headRepo.id,
2141 + source: headRepo.feed,
2142 + rel: 'repo',
2143 + values: true,
2144 + reverse: true
2145 + }),
2146 + pull.take(function (link) {
2147 + return link.value.timestamp > pr.created_at
2148 + }),
2149 + pull.filter(function (link) {
2150 + return link.value.content.type == 'git-update'
2151 + && ('refs/heads/' + pr.headBranch) in link.value.content.refs
2152 + })
2153 + ))
2154 + })
2155 + ]),
2156 + addAuthorName(about),
2157 + pull.through(function (msg) {
2158 + if (msg.value.timestamp > newestMsg.value.timestamp)
2159 + newestMsg = msg
2160 + }),
2161 + sortMsgs(),
2162 + pull.map(function (item) {
2163 + if (item.value.content.type == 'git-update')
2164 + return renderBranchUpdate(pr, item)
2165 + return renderIssueActivityMsg(repo, pr,
2166 + 'pull request', postId, item)
2167 + })
18502168 ),
1851- isPublic ? pull.empty() : readOnce(renderCommentForm)
2169 + isPublic ? pull.empty() : readOnce(function (cb) {
2170 + cb(null, renderIssueCommentForm(pr, repo, newestMsg.key, isAuthor,
2171 + 'pull request'))
2172 + })
2173 + ])
2174 + }
2175 +
2176 + function renderBranchUpdate(pr, msg) {
2177 + var authorLink = link([msg.value.author], msg.authorName)
2178 + var msgLink = link([msg.key],
2179 + new Date(msg.value.timestamp).toLocaleString())
2180 + var rev = msg.value.content.refs['refs/heads/' + pr.headBranch]
2181 + if (!rev)
2182 + return '<section class="collapse">' +
2183 + authorLink + ' deleted the <code>' + pr.headBranch + '</code> branch' +
2184 + ' &middot; ' + msgLink +
2185 + '</section>'
2186 +
2187 + var revLink = link([pr.headRepo, 'commit', rev], rev.substr(0, 8))
2188 + return '<section class="collapse">' +
2189 + authorLink + ' updated the branch to <code>' + revLink + '</code>' +
2190 + ' &middot; ' + msgLink +
2191 + '</section>'
2192 + }
2193 +
2194 + /* Compare changes */
2195 +
2196 + function serveRepoCompare(req, repo) {
2197 + var query = req._u.query
2198 + var base
2199 + var count = 0
2200 +
2201 + return renderRepoPage(repo, 'pulls', null, cat([
2202 + pull.once('<h3>Compare changes</h3>' +
2203 + '<form action="' + encodeLink(repo.id) + '/comparing" method="get">' +
2204 + '<section>'),
2205 + pull.once('Base branch: '),
2206 + readNext(function (cb) {
2207 + if (query.base) gotBase(null, query.base)
2208 + else repo.getSymRef('HEAD', true, gotBase)
2209 + function gotBase(err, ref) {
2210 + if (err) return cb(err)
2211 + cb(null, branchMenu(repo, 'base', base = ref || 'HEAD'))
2212 + }
2213 + }),
2214 + pull.once('<br/>Comparison repo/branch:'),
2215 + pull(
2216 + getForks(repo, true),
2217 + pull.asyncMap(function (msg, cb) {
2218 + getRepo(msg.key, function (err, repo) {
2219 + if (err) return cb(err)
2220 + cb(null, {
2221 + msg: msg,
2222 + repo: repo
2223 + })
2224 + })
2225 + }),
2226 + pull.map(renderFork),
2227 + pull.flatten()
2228 + ),
2229 + pull.once('</section>'),
2230 + readOnce(function (cb) {
2231 + cb(null, count == 0 ? 'No branches to compare!' :
2232 + '<button type="submit" class="btn">Compare</button>')
2233 + }),
2234 + pull.once('</form>')
18522235 ]))
18532236
1854- function renderCommentForm(cb) {
1855- cb(null, '<section><form action="" method="post">' +
1856- '<input type="hidden" name="action" value="comment">' +
1857- '<input type="hidden" name="id" value="' + issue.id + '">' +
1858- '<input type="hidden" name="issue" value="' + issue.id + '">' +
1859- '<input type="hidden" name="repo" value="' + repo.id + '">' +
1860- '<input type="hidden" name="branch" value="' + newestMsg.key + '">' +
1861- renderPostForm(repo) +
1862- '<input type="submit" class="btn open" value="Comment" />' +
1863- (isAuthor ?
1864- '<input type="submit" class="btn"' +
1865- ' name="' + (issue.open ? 'close' : 'open') + '"' +
1866- ' value="' + (issue.open ? 'Close issue' : 'Reopen issue') + '"' +
1867- '/>' : '') +
1868- '</form></section>')
2237 + function renderFork(fork) {
2238 + return pull(
2239 + fork.repo.refs(),
2240 + pull.map(function (ref) {
2241 + var m = /^refs\/([^\/]*)\/(.*)$/.exec(ref.name) || [,ref.name]
2242 + return {
2243 + type: m[1],
2244 + name: m[2],
2245 + value: ref.value
2246 + }
2247 + }),
2248 + pull.filter(function (ref) {
2249 + return ref.type == 'heads'
2250 + && !(ref.name == base && fork.msg.key == repo.id)
2251 + }),
2252 + pull.map(function (ref) {
2253 + var branchLink = link([fork.msg.key, 'tree', ref.name], ref.name)
2254 + var authorLink = link([fork.msg.value.author], fork.msg.authorName)
2255 + var repoLink = link([fork.msg.key], fork.msg.repoName)
2256 + var value = fork.msg.key + ':' + ref.name
2257 + count++
2258 + return '<div class="bgslash">' +
2259 + '<input type="radio" name="head"' +
2260 + ' value="' + escapeHTML(value) + '"' +
2261 + (query.head == value ? ' checked="checked"' : '') + '> ' +
2262 + authorLink + ' / ' + repoLink + ' / ' + branchLink + '</div>'
2263 + })
2264 + )
18692265 }
18702266 }
18712267
2268 + function serveRepoComparing(req, repo) {
2269 + var query = req._u.query
2270 + var baseBranch = query.base
2271 + var s = (query.head || '').split(':')
2272 +
2273 + if (!s || !baseBranch)
2274 + return serveRedirect(encodeLink([repo.id, 'compare']))
2275 +
2276 + var headRepoId = s[0]
2277 + var headBranch = s[1]
2278 + var baseLink = link([repo.id, 'tree', baseBranch])
2279 + var headBranchLink = link([headRepoId, 'tree', headBranch])
2280 + var backHref = encodeLink([repo.id, 'compare']) + req._u.search
2281 +
2282 + return renderRepoPage(repo, 'pulls', null, cat([
2283 + pull.once('<h3>' +
2284 + (query.expand ? 'Open a pull request' : 'Comparing changes') +
2285 + '</h3>'),
2286 + readNext(function (cb) {
2287 + getRepo(headRepoId, function (err, headRepo) {
2288 + if (err) return cb(err)
2289 + getRepoFullName(about, headRepo.feed, headRepo.id,
2290 + function (err, repoName, authorName) {
2291 + if (err) return cb(err)
2292 + cb(null, renderRepoInfo(Repo(headRepo), repoName, authorName))
2293 + }
2294 + )
2295 + })
2296 + })
2297 + ]))
2298 +
2299 + function renderRepoInfo(headRepo, headRepoName, headRepoAuthorName) {
2300 + var authorLink = link([headRepo.feed], headRepoAuthorName)
2301 + var repoLink = link([headRepoId], headRepoName)
2302 + return cat([
2303 + pull.once('<section>' +
2304 + 'Base: ' + baseLink + '<br/>' +
2305 + 'Head: <span class="bgslash">' + authorLink + ' / ' + repoLink +
2306 + ' / ' + headBranchLink + '</span>' +
2307 + '</section>' +
2308 + (query.expand ? '<section><form method="post" action="">' +
2309 + hiddenInputs({
2310 + action: 'new-pull',
2311 + branch: baseBranch,
2312 + head_repo: headRepoId,
2313 + head_branch: headBranch
2314 + }) +
2315 + '<input class="wide-input" name="title"' +
2316 + ' placeholder="Title" size="77"/>' +
2317 + renderPostForm(repo, 'Description', 8) +
2318 + '<button type="submit" class="btn open">Create</button>' +
2319 + '</form></section>'
2320 + : '<section><form method="get" action="">' +
2321 + hiddenInputs({
2322 + base: baseBranch,
2323 + head: query.head
2324 + }) +
2325 + '<button class="btn open" type="submit" name="expand" value="1">' +
2326 + '<i>⎇</i> Create pull request</button> ' +
2327 + '<a href="' + backHref + '">Back</a>' +
2328 + '</form></section>') +
2329 + '<div id="commits"></div>' +
2330 + '<div class="tab-links">' +
2331 + '<a href="#" id="files-link">Files changed</a> ' +
2332 + '<a href="#commits" id="commits-link">Commits</a>' +
2333 + '</div>' +
2334 + '<section id="files-tab">'),
2335 + renderDiffStat([repo, headRepo], [baseBranch, headBranch]),
2336 + pull.once('</section>' +
2337 + '<section id="commits-tab">'),
2338 + renderCommitLog(repo, baseBranch, headRepo, headBranch),
2339 + pull.once('</section>')
2340 + ])
2341 + }
2342 + }
2343 +
2344 + function renderCommitLog(baseRepo, baseBranch, headRepo, headBranch) {
2345 + return cat([
2346 + pull.once('<table class="compare-commits">'),
2347 + readNext(function (cb) {
2348 + baseRepo.resolveRef(baseBranch, function (err, baseBranchRev) {
2349 + if (err) return cb(err)
2350 + var currentDay
2351 + return cb(null, pull(
2352 + headRepo.readLog(headBranch),
2353 + pull.take(function (rev) { return rev != baseBranchRev }),
2354 + // pull.take(2),
2355 + pullReverse(),
2356 + paramap(headRepo.getCommitParsed.bind(headRepo), 8),
2357 + pull.map(function (commit) {
2358 + var commitPath = [baseRepo.id, 'commit', commit.id]
2359 + var commitIdShort = '<tt>' + commit.id.substr(0, 8) + '</tt>'
2360 + var day = Math.floor(commit.author.date / 86400000)
2361 + var dateRow = day == currentDay ? '' :
2362 + '<tr><th colspan=3 class="date-info">' +
2363 + commit.author.date.toLocaleDateString() +
2364 + '</th><tr>'
2365 + currentDay = day
2366 + return dateRow + '<tr>' +
2367 + '<td>' + escapeHTML(commit.author.name) + '</td>' +
2368 + '<td>' + link(commitPath, commit.title) + '</td>' +
2369 + '<td>' + link(commitPath, commitIdShort, true) + '</td>' +
2370 + '</tr>'
2371 + })
2372 + ))
2373 + })
2374 + }),
2375 + pull.once('</table>')
2376 + ])
2377 + }
18722378 }
package.jsonView
@@ -9,9 +9,10 @@
99 "highlight.js": "^9.2.0",
1010 "multicb": "^1.2.1",
1111 "pull-cat": "^1.1.8",
1212 "pull-git-pack": "^0.2.0",
13- "pull-git-repo": "^0.3.2",
13 + "pull-git-repo": "^0.4.0",
14 + "pull-many": "^1.0.6",
1415 "pull-paramap": "^1.1.2",
1516 "pull-stream": "^3.1.0",
1617 "ssb-client": "^3.0.1",
1718 "ssb-config": "^1.1.0",
@@ -19,8 +20,9 @@
1920 "ssb-keys": "^5.0.0",
2021 "ssb-marked": "^0.5.4",
2122 "ssb-mentions": "^0.1.0",
2223 "ssb-msg-schemas": "^6.1.0",
24 + "ssb-pull-requests": "^0.0.0",
2325 "ssb-ref": "^2.2.2",
2426 "ssb-reconnect": "^0.1.0",
2527 "ssb-issues": "^0.0.5",
2628 "stream-to-pull-stream": "^1.6.6"
static/styles.cssView
@@ -96,12 +96,15 @@
9696 height: 2em;
9797 }
9898
9999 .repo-title h2 {
100- color: #888;
101100 height: 1.2em;
102101 }
103102
103 +.bgslash {
104 + color: #888;
105 +}
106 +
104107 .petname h2,
105108 .petname h3 {
106109 float: left;
107110 }
@@ -167,8 +170,9 @@
167170 border-style: none solid solid none;
168171 border-radius: .5ex;
169172 float: right;
170173 margin: 0;
174 + max-width: 50%;
171175 }
172176 .clone-url:hover {
173177 background-color: #f6f6f6;
174178 }
@@ -278,9 +282,10 @@
278282 white-space: nowrap;
279283 text-overflow: ellipsis;
280284 }
281285
282-.tab-links label {
286 +.tab-links label,
287 +.tab-links a {
283288 padding: 1ex;
284289 color: #333;
285290 }
286291
@@ -367,4 +372,34 @@
367372 }
368373
369374 .diff-old { background-color: #ffe2dd; }
370375 .diff-new { background-color: #d1ffd6; }
376 +
377 +/* Pull requests */
378 +
379 +#commits:not(:target) ~ #commits-tab,
380 +#commits:target ~ #files-tab {
381 + display: none;
382 +}
383 +
384 +#commits:not(:target) + .tab-links #files-link,
385 +#commits:target + .tab-links #commits-link {
386 + font-weight: bold;
387 +}
388 +
389 +.compare-commits {
390 + width: 100%;
391 +}
392 +
393 +.compare-commits .date-info {
394 + font-weight: normal;
395 + text-align: left;
396 + color: #666;
397 +}
398 +
399 +.compare-commits td:first-child {
400 + padding-left: 2ex;
401 +}
402 +
403 +.pr-tab-links {
404 + margin: .5em 0 0.25em;
405 +}

Built with git-ssb-web