git ssb

16+

Dominic / patchbay



Tree: 8645df8d5c4edf3b8fe98b8e3b257e80ab48d09d

Files: 8645df8d5c4edf3b8fe98b8e3b257e80ab48d09d / modules_extra / git.js

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

Built with git-ssb-web