git ssb

0+

ev / minbase



Tree: b62ecd6f843b26f37e70c7b6d65c71db35bbf922

Files: b62ecd6f843b26f37e70c7b6d65c71db35bbf922 / modules / git.js

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

Built with git-ssb-web