git ssb

0+

ev / minbase



Tree: 3ee0398629a78832c2fbe78831ce756faeaeca74

Files: 3ee0398629a78832c2fbe78831ce756faeaeca74 / modules / git.js

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

Built with git-ssb-web