git ssb

16+

Dominic / patchbay



Tree: 93d26ea7530a862ffda0f98070f6a1ce09d1d616

Files: 93d26ea7530a862ffda0f98070f6a1ce09d1d616 / modules_extra / git.js

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

Built with git-ssb-web