secure-scuttlebot classic
at main 16 kB view raw
1'use strict' 2var h = require('hyperscript') 3var pull = require('pull-stream') 4var paramap = require('pull-paramap') 5var cat = require('pull-cat') 6var human = require('human-time') 7var combobox = require('hypercombo') 8 9var getAvatar = require('ssb-avatar') 10var KVGraph = require('kvgraph') 11var mergeRepo = require('ssb-git/merge') 12 13//var plugs = require('../plugs') 14//var message_link = plugs.first(exports.message_link = []) 15//var message_confirm = plugs.first(exports.message_confirm = []) 16//var message_compose = plugs.first(exports.message_compose = []) 17//var sbot_links = plugs.first(exports.sbot_links = []) 18//var sbot_links2 = plugs.first(exports.sbot_links2 = []) 19//var sbot_get = plugs.first(exports.sbot_get = []) 20//var avatar_name = plugs.first(exports.avatar_name = []) 21//var markdown = plugs.first(exports.markdown = []) 22 23exports.needs = { 24 message_link: 'first', 25 message_confirm: 'first', 26 message_compose: 'first', 27 sbot_links: 'first', 28 sbot_links2: 'first', 29 sbot_get: 'first', 30 avatar_name: 'first', 31 markdown: 'first' 32} 33 34exports.gives = { 35 message_action: true, 36 message_meta: true, 37 message_content: true 38} 39 40 41var self_id = require('../keys').id 42 43function shortRefName(ref) { 44 return ref.replace(/^refs\/(heads|tags)\//, '') 45} 46 47exports.create = function (api) { 48 49 function getRefs(msg) { 50 var updates = new KVGraph('key') 51 var _cb, _refs 52 pull( 53 api.sbot_links({ 54 reverse: true, 55 // source: msg.value.author, 56 dest: msg.key, 57 rel: 'repo', 58 values: true 59 }), 60 pull.drain(function (link) { 61 if (link.value.content.type === 'git-update') { 62 updates.add(link) 63 } 64 }, function (err) { 65 var refs = updates.reduceRight(mergeRepo).refs 66 var cb = _cb 67 if (cb) _cb = null, cb(err, refs) 68 else _refs = refs 69 }) 70 ) 71 72 return pull( 73 function fn(end, cb) { 74 if (end || fn.ended) cb(true) 75 fn.ended = true 76 if (_refs) cb(_refs) 77 else _cb = cb 78 }, 79 pull.flatten() 80 ) 81 } 82 83 function getForks(id) { 84 return pull( 85 api.sbot_links({ 86 reverse: true, 87 dest: id, 88 rel: 'upstream' 89 }), 90 pull.map(function (link) { 91 return { 92 id: link.key, 93 author: link.source 94 } 95 }) 96 ) 97 } 98 99 function repoText(id) { 100 var text = document.createTextNode(id.substr(0, 10) + '…') 101 getAvatar({links: api.sbot_links, get: api.sbot_get}, self_id, id, 102 function (err, avatar) { 103 if(err) return console.error(err) 104 if (avatar.name[0] !== '%') avatar.name = '%' + avatar.name 105 text.nodeValue = avatar.name 106 }) 107 return text 108 } 109 110 function repoLink(id) { 111 return h('a', {href: '#'+id}, repoText(id)) 112 } 113 114 function repoName(id) { 115 return h('ins', repoText(id)) 116 } 117 118 function getIssueState(id, cb) { 119 pull( 120 api.sbot_links({dest: id, rel: 'issues', values: true, reverse: true}), 121 pull.map(function (msg) { 122 return msg.value.content.issues 123 }), 124 pull.flatten(), 125 pull.filter(function (issue) { 126 return issue.link === id 127 }), 128 pull.map(function (issue) { 129 return issue.merged ? 'merged' : issue.open ? 'open' : 'closed' 130 }), 131 pull.take(1), 132 pull.collect(function (err, updates) { 133 cb(err, updates && updates[0] || 'open') 134 }) 135 ) 136 } 137 138 //todo: 139 function messageTimestampLink(msg) { 140 var date = new Date(msg.value.timestamp) 141 return h('a.timestamp', { 142 timestamp: msg.value.timestamp, 143 title: date, 144 href: '#'+msg.key 145 }, human(date)) 146 } 147 148 // a thead+tbody where the thead only is added when the first row is added 149 function tableRows(headerRow) { 150 var thead = h('thead'), tbody = h('tbody') 151 var first = true 152 var t = [thead, tbody] 153 t.append = function (row) { 154 if (first) { 155 first = false 156 thead.appendChild(headerRow) 157 } 158 tbody.appendChild(row) 159 } 160 return t 161 } 162 163 function renderIssueEdit(c) { 164 var id = c.issue || c.link 165 return [ 166 c.title ? h('p', 'renamed issue ', api.message_link(id), 167 ' to ', h('ins', c.title)) : null, 168 c.open === false ? h('p', 'closed issue ', api.message_link(id)) : null, 169 c.open === true ? h('p', 'reopened issue ', api.message_link(id)) : null] 170 } 171 172 function findMessageContent(el) { 173 for(; el; el = el.parentNode) { 174 if(el.classList.contains('message')) { 175 return el.querySelector('.message_content') 176 } 177 } 178 } 179 180 function issueForm(msg, contentEl) { 181 var form = h('form', 182 h('strong', 'New Issue:'), 183 api.message_compose( 184 {type: 'issue', project: msg.key}, 185 function (value) { return value }, 186 function (err, issue) { 187 if(err) return alert(err) 188 if(!issue) return 189 var title = issue.value.content.text 190 if(title.length > 70) title = title.substr(0, 70) + '…' 191 form.appendChild(h('div', 192 h('a', {href: '#'+issue.key}, title) 193 )) 194 } 195 ) 196 ) 197 return form 198 } 199 200 function branchMenu(msg, full) { 201 return combobox({ 202 style: {'max-width': '14ex'}, 203 placeholder: 'branch…', 204 default: 'master', 205 read: msg && pull(getRefs(msg), pull.map(function (ref) { 206 var m = /^refs\/heads\/(.*)$/.exec(ref.name) 207 if(!m) return 208 var branch = m[1] 209 var label = branch 210 if(full) { 211 var updated = new Date(ref.link.value.timestamp) 212 label = branch + 213 ' · ' + human(updated) + 214 ' · ' + ref.hash.substr(1, 8) + 215 (ref.title ? ' · "' + ref.title + '"' : '') 216 } 217 return h('option', {value: branch}, label) 218 })) 219 }) 220 } 221 222 function newPullRequestButton(msg) { 223 return h('div', [ 224 h('a', { 225 href: '#', 226 onclick: function (e) { 227 e.preventDefault() 228 this.parentNode.replaceChild(pullRequestForm(msg), this) 229 }}, 230 'New Pull Request…' 231 ) 232 ]) 233 } 234 235 function pullRequestForm(msg) { 236 var headRepoInput 237 var headBranchInput = branchMenu() 238 var branchInput = branchMenu(msg) 239 var form = h('form', 240 h('strong', 'New Pull Request:'), 241 h('div', 242 'from ', 243 headRepoInput = combobox({ 244 style: {'max-width': '26ex'}, 245 onchange: function () { 246 // list branches for selected repo 247 var repoId = this.value 248 if(repoId) api.sbot_get(repoId, function (err, value) { 249 if(err) console.error(err) 250 var msg = value && {key: repoId, value: value} 251 headBranchInput = headBranchInput.swap(branchMenu(msg, true)) 252 }) 253 else headBranchInput = headBranchInput.swap(branchMenu()) 254 }, 255 read: pull(cat([ 256 pull.once({id: msg.key, author: msg.value.author}), 257 getForks(msg.key) 258 ]), pull.map(function (fork) { 259 return h('option', {value: fork.id}, 260 repoLink(fork.id), ' by ', api.avatar_name(fork.author)) 261 })) 262 }), 263 ':', 264 headBranchInput, 265 ' to ', 266 repoName(msg.key), 267 ':', 268 branchInput), 269 api.message_compose( 270 { 271 type: 'pull-request', 272 project: msg.key, 273 repo: msg.key, 274 }, 275 function (value) { 276 value.branch = branchInput.value 277 value.head_repo = headRepoInput.value 278 value.head_branch = headBranchInput.value 279 return value 280 }, 281 function (err, issue) { 282 if(err) return alert(err) 283 if(!issue) return 284 var title = issue.value.content.text 285 if(title.length > 70) title = title.substr(0, 70) + '…' 286 form.appendChild(h('div', 287 h('a', {href: '#'+issue.key}, title) 288 )) 289 } 290 ) 291 ) 292 return form 293 } 294 295 296 297 return { 298 message_content: function (msg, sbot) { 299 var c = msg.value.content 300 301 if(c.type === 'git-repo') { 302 var branchesT, tagsT, openIssuesT, closedIssuesT, openPRsT, closedPRsT 303 var forksT 304 var div = h('div', 305 h('p', 'git repo ', repoName(msg.key)), 306 c.upstream ? h('p', 'fork of ', repoLink(c.upstream)) : '', 307 h('p', h('code', 'ssb://' + msg.key)), 308 h('div.git-table-wrapper', {style: {'max-height': '12em'}}, 309 h('table', 310 branchesT = tableRows(h('tr', 311 h('th', 'branch'), 312 h('th', 'commit'), 313 h('th', 'last update'))), 314 tagsT = tableRows(h('tr', 315 h('th', 'tag'), 316 h('th', 'commit'), 317 h('th', 'last update'))))), 318 h('div.git-table-wrapper', {style: {'max-height': '16em'}}, 319 h('table', 320 openIssuesT = tableRows(h('tr', 321 h('th', 'open issues'))), 322 closedIssuesT = tableRows(h('tr', 323 h('th', 'closed issues'))))), 324 h('div.git-table-wrapper', {style: {'max-height': '16em'}}, 325 h('table', 326 openPRsT = tableRows(h('tr', 327 h('th', 'open pull requests'))), 328 closedPRsT = tableRows(h('tr', 329 h('th', 'closed pull requests'))))), 330 h('div.git-table-wrapper', 331 h('table', 332 forksT = tableRows(h('tr', 333 h('th', 'forks'))))), 334 h('div', h('a', {href: '#', onclick: function (e) { 335 e.preventDefault() 336 this.parentNode.replaceChild(issueForm(msg), this) 337 }}, 'New Issue…')), 338 newPullRequestButton.call(this, msg) 339 ) 340 341 pull(getRefs(msg), pull.drain(function (ref) { 342 var name = ref.realname || ref.name 343 var author = ref.link && ref.link.value.author 344 var parts = /^refs\/(heads|tags)\/(.*)$/.exec(name) || [] 345 var shortName = parts[2] 346 var t 347 if(parts[1] === 'heads') t = branchesT 348 else if(parts[1] === 'tags') t = tagsT 349 if(t) t.append(h('tr', 350 h('td', shortName, 351 ref.conflict ? [ 352 h('br'), 353 h('a', {href: '#'+author}, api.avatar_name(author)) 354 ] : ''), 355 h('td', h('code', ref.hash)), 356 h('td', messageTimestampLink(ref.link)))) 357 }, function (err) { 358 if(err) console.error(err) 359 })) 360 361 // list issues and pull requests 362 pull( 363 api.sbot_links({ 364 reverse: true, 365 dest: msg.key, 366 rel: 'project', 367 values: true 368 }), 369 paramap(function (link, cb) { 370 getIssueState(link.key, function (err, state) { 371 if(err) return cb(err) 372 link.state = state 373 cb(null, link) 374 }) 375 }), 376 pull.drain(function (link) { 377 var c = link.value.content 378 var title = c.title || (c.text ? c.text.length > 70 379 ? c.text.substr(0, 70) + '…' 380 : c.text : link.key) 381 var author = link.value.author 382 var t = c.type === 'pull-request' 383 ? link.state === 'open' ? openPRsT : closedPRsT 384 : link.state === 'open' ? openIssuesT : closedIssuesT 385 t.append(h('tr', 386 h('td', 387 h('a', {href: '#'+link.key}, title), h('br'), 388 h('small', 389 'opened ', messageTimestampLink(link), 390 ' by ', h('a', {href: '#'+author}, api.avatar_name(author)))))) 391 }, function (err) { 392 if (err) console.error(err) 393 }) 394 ) 395 396 // list forks 397 pull( 398 getForks(msg.key), 399 pull.drain(function (fork) { 400 forksT.append(h('tr', h('td', 401 repoLink(fork.id), 402 ' by ', h('a', {href: '#'+fork.author}, api.avatar_name(fork.author))))) 403 }, function (err) { 404 if (err) console.error(err) 405 }) 406 ) 407 408 return div 409 } 410 411 if(c.type === 'git-update') { 412 return [ 413 h('p', 'pushed to ', repoLink(c.repo)), 414 c.refs ? h('ul', Object.keys(c.refs).map(function (ref) { 415 var rev = c.refs[ref] 416 return h('li', 417 shortRefName(ref) + ': ', 418 rev ? h('code', rev) : h('em', 'deleted')) 419 })) : null, 420 Array.isArray(c.commits) ? [ 421 h('ul', 422 c.commits.map(function (commit) { 423 return h('li', 424 typeof commit.sha1 === 'string' ? 425 [h('code', commit.sha1.substr(0, 8)), ' '] : null, 426 commit.title ? 427 h('q', commit.title) : null) 428 }), 429 c.commits_more > 0 ? 430 h('li', '+ ', c.commits_more, ' more') : null) 431 ] : null, 432 Array.isArray(c.issues) ? c.issues.map(function (issue) { 433 if (issue.merged === true) 434 return h('p', 'Merged ', api.message_link(issue.link), ' in ', 435 h('code', issue.object), ' ', h('q', issue.label)) 436 if (issue.open === false) 437 return h('p', 'Closed ', api.message_link(issue.link), ' in ', 438 h('code', issue.object), ' ', h('q', issue.label)) 439 }) : null, 440 newPullRequestButton.call(this, msg) 441 ] 442 } 443 444 if(c.type === 'issue-edit' 445 || (c.type === 'post' && c.text === '')) { 446 return h('div', 447 c.issue ? renderIssueEdit(c) : null, 448 c.issues ? c.issues.map(renderIssueEdit) : null) 449 } 450 451 if(c.type === 'issue') { 452 return h('div', 453 h('p', 'opened issue on ', repoLink(c.project)), 454 c.title ? h('h4', c.title) : '', 455 api.markdown(c) 456 ) 457 } 458 459 if(c.type === 'pull-request') { 460 return h('div', 461 h('p', 'opened pull-request ', 462 'to ', repoLink(c.repo), ':', c.branch, ' ', 463 'from ', repoLink(c.head_repo), ':', c.head_branch), 464 c.title ? h('h4', c.title) : '', 465 api.markdown(c) 466 ) 467 } 468 }, 469 470 message_meta: function (msg, sbot) { 471 var type = msg.value.content.type 472 if (type === 'issue' || type === 'pull-request') { 473 var el = h('em', '...') 474 // TODO: update if issue is changed 475 getIssueState(msg.key, function (err, state) { 476 if (err) return console.error(err) 477 el.textContent = state 478 }) 479 return el 480 } 481 }, 482 483 message_action: function (msg, sbot) { 484 var c = msg.value.content 485 if(c.type === 'issue' || c.type === 'pull-request') { 486 var isOpen 487 var a = h('a', {href: '#', onclick: function (e) { 488 e.preventDefault() 489 api.message_confirm({ 490 type: 'issue-edit', 491 root: msg.key, 492 issues: [{ 493 link: msg.key, 494 open: !isOpen 495 }] 496 }, function (err, msg) { 497 if(err) return alert(err) 498 if(!msg) return 499 isOpen = msg.value.content.open 500 update() 501 }) 502 }}) 503 getIssueState(msg.key, function (err, state) { 504 if (err) return console.error(err) 505 isOpen = state === 'open' 506 update() 507 }) 508 function update() { 509 a.textContent = c.type === 'pull-request' 510 ? isOpen ? 'Close Pull Request' : 'Reopen Pull Request' 511 : isOpen ? 'Close Issue' : 'Reopen Issue' 512 } 513 return a 514 } 515 } 516 } 517} 518 519