Files: 787f858837541c7b94531c9b98603402cfb7f06b / modules_extra / git.js
15617 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 | } |
39 | |
40 | |
41 | var self_id = require('../keys').id |
42 | |
43 | function shortRefName(ref) { |
44 | return ref.replace(/^refs\/(heads|tags)\//, '') |
45 | } |
46 | |
47 | exports.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