git ssb

16+

Dominic / patchbay



Tree: 6eca4b03ca9a3af5f7c0fdae5472d5910db9b768

Files: 6eca4b03ca9a3af5f7c0fdae5472d5910db9b768 / modules_extra / git.js

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

Built with git-ssb-web