Files: 7d510f40ceb47f6b4863bc121426ce7492a1bbbc / modules_extra / git.js
16173 bytesRaw
1 | |
2 | var h = require('hyperscript') |
3 | var pull = require('pull-stream') |
4 | var paramap = require('pull-paramap') |
5 | var cat = require('pull-cat') |
6 | var human = require('human-time') |
7 | var combobox = require('hypercombo') |
8 | |
9 | var getAvatar = require('ssb-avatar') |
10 | var KVGraph = require('kvgraph') |
11 | var 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 | |
23 | exports.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 | |
34 | exports.gives = { |
35 | message_action: true, |
36 | message_meta: true, |
37 | message_content: true, |
38 | message_title: true |
39 | } |
40 | |
41 | |
42 | var self_id = require('../keys').id |
43 | |
44 | function shortRefName(ref) { |
45 | return ref.replace(/^refs\/(heads|tags)\//, '') |
46 | } |
47 | |
48 | exports.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