git ssb

16+

Dominic / patchbay



Tree: 5e399826faea89305f911f9f2fb8a9509f9a0319

Files: 5e399826faea89305f911f9f2fb8a9509f9a0319 / modules_extra / git.js

15617 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}
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
520

Built with git-ssb-web