Commit b0f09a2c61341125b8d3bbbaac3e7db729e6db79
Add git issue and pull request creation UI
Charles Lehner committed on 8/28/2016, 4:55:43 AMParent: d4a98043304f590b79f4dbe3398066294b7dba14
Files changed
modules/git.js | changed |
package.json | changed |
modules/git.js | ||
---|---|---|
@@ -1,12 +1,15 @@ | ||
1 | 1 | var h = require('hyperscript') |
2 | 2 | var pull = require('pull-stream') |
3 | 3 | var paramap = require('pull-paramap') |
4 | +var cat = require('pull-cat') | |
4 | 5 | var human = require('human-time') |
6 | +var combobox = require('hypercombo') | |
5 | 7 | |
6 | 8 | var plugs = require('../plugs') |
7 | 9 | var message_link = plugs.first(exports.message_link = []) |
8 | 10 | var message_confirm = plugs.first(exports.message_confirm = []) |
11 | +var message_compose = plugs.first(exports.message_compose = []) | |
9 | 12 | var sbot_links = plugs.first(exports.sbot_links = []) |
10 | 13 | var sbot_links2 = plugs.first(exports.sbot_links2 = []) |
11 | 14 | var sbot_get = plugs.first(exports.sbot_get = []) |
12 | 15 | var getAvatar = require('ssb-avatar') |
@@ -18,8 +21,63 @@ | ||
18 | 21 | function shortRefName(ref) { |
19 | 22 | return ref.replace(/^refs\/(heads|tags)\//, '') |
20 | 23 | } |
21 | 24 | |
25 | +function getRefs(msg) { | |
26 | + var refs = {} | |
27 | + var commitTitles = {} | |
28 | + return pull( | |
29 | + sbot_links({ | |
30 | + reverse: true, | |
31 | + source: msg.value.author, | |
32 | + dest: msg.key, | |
33 | + rel: 'repo', | |
34 | + values: true | |
35 | + }), | |
36 | + pull.map(function (link) { | |
37 | + var refUpdates = link.value.content.refs || {} | |
38 | + var commits = link.value.content.commits | |
39 | + if(commits) { | |
40 | + for(var i = 0; i < commits.length; i++) { | |
41 | + var commit = commits[i] | |
42 | + if(commit && commit.sha1 && commit.title) { | |
43 | + commitTitles[commit.sha1] = commit.title | |
44 | + } | |
45 | + } | |
46 | + } | |
47 | + return Object.keys(refUpdates).reverse().map(function (ref) { | |
48 | + if(refs[ref]) return | |
49 | + refs[ref] = true | |
50 | + var rev = refUpdates[ref] | |
51 | + if(!rev) return | |
52 | + return { | |
53 | + name: ref, | |
54 | + rev: rev, | |
55 | + link: link, | |
56 | + title: commitTitles[rev], | |
57 | + } | |
58 | + }).filter(Boolean) | |
59 | + }), | |
60 | + pull.flatten() | |
61 | + ) | |
62 | +} | |
63 | + | |
64 | +function getForks(id) { | |
65 | + return pull( | |
66 | + sbot_links({ | |
67 | + reverse: true, | |
68 | + dest: id, | |
69 | + rel: 'upstream' | |
70 | + }), | |
71 | + pull.map(function (link) { | |
72 | + return { | |
73 | + id: link.key, | |
74 | + author: link.source | |
75 | + } | |
76 | + }) | |
77 | + ) | |
78 | +} | |
79 | + | |
22 | 80 | function repoLink(id) { |
23 | 81 | var el = h('a', {href: '#'+id}, id.substr(0, 10) + '…') |
24 | 82 | getAvatar({links: sbot_links}, self_id, id, function (err, avatar) { |
25 | 83 | if(err) return console.error(err) |
@@ -127,40 +185,28 @@ | ||
127 | 185 | h('th', 'closed pull requests'))))), |
128 | 186 | h('div.git-table-wrapper', |
129 | 187 | h('table', |
130 | 188 | forksT = tableRows(h('tr', |
131 | - h('th', 'forks')))))) | |
189 | + h('th', 'forks'))))), | |
190 | + h('div', h('a', {href: '#', onclick: function () { | |
191 | + this.parentNode.replaceChild(issueForm(msg), this) | |
192 | + }}, 'New Issue…')), | |
193 | + h('div', h('a', {href: '#', onclick: function () { | |
194 | + this.parentNode.replaceChild(pullRequestForm(msg), this) | |
195 | + }}, 'New Pull Request…'))) | |
132 | 196 | |
133 | - // compute refs | |
134 | - var refs = {} | |
135 | - pull( | |
136 | - sbot_links({ | |
137 | - reverse: true, | |
138 | - source: msg.value.author, | |
139 | - dest: msg.key, | |
140 | - rel: 'repo', | |
141 | - values: true | |
142 | - }), | |
143 | - pull.drain(function (link) { | |
144 | - var refUpdates = link.value.content.refs || {} | |
145 | - Object.keys(refUpdates).reverse().filter(function (ref) { | |
146 | - if (refs[ref]) return | |
147 | - refs[ref] = true | |
148 | - var rev = refUpdates[ref] | |
149 | - if (!rev) return | |
150 | - var parts = /^refs\/(heads|tags)\/(.*)$/.exec(ref) || [] | |
151 | - var t | |
152 | - if (parts[1] === 'heads') t = branchesT | |
153 | - else if (parts[1] === 'tags') t = tagsT | |
154 | - if (t) t.append(h('tr', | |
155 | - h('td', parts[2]), | |
156 | - h('td', h('code', rev)), | |
157 | - h('td', messageTimestampLink(link)))) | |
158 | - }) | |
159 | - }, function (err) { | |
160 | - if (err) console.error(err) | |
161 | - }) | |
162 | - ) | |
197 | + pull(getRefs(msg), pull.drain(function (ref) { | |
198 | + var parts = /^refs\/(heads|tags)\/(.*)$/.exec(ref.name) || [] | |
199 | + var t | |
200 | + if(parts[1] === 'heads') t = branchesT | |
201 | + else if(parts[1] === 'tags') t = tagsT | |
202 | + if(t) t.append(h('tr', | |
203 | + h('td', parts[2]), | |
204 | + h('td', h('code', ref.rev)), | |
205 | + h('td', messageTimestampLink(ref.link)))) | |
206 | + }, function (err) { | |
207 | + if(err) console.error(err) | |
208 | + })) | |
163 | 209 | |
164 | 210 | // list issues and pull requests |
165 | 211 | pull( |
166 | 212 | sbot_links({ |
@@ -197,17 +243,13 @@ | ||
197 | 243 | ) |
198 | 244 | |
199 | 245 | // list forks |
200 | 246 | pull( |
201 | - sbot_links({ | |
202 | - reverse: true, | |
203 | - dest: msg.key, | |
204 | - rel: 'upstream' | |
205 | - }), | |
206 | - pull.drain(function (link) { | |
247 | + getForks(msg.key), | |
248 | + pull.drain(function (fork) { | |
207 | 249 | forksT.append(h('tr', h('td', |
208 | - repoName(link.key, true), | |
209 | - ' by ', h('a', {href: '#'+link.source}, avatar_name(link.source))))) | |
250 | + repoName(fork.id, true), | |
251 | + ' by ', h('a', {href: '#'+fork.author}, avatar_name(fork.author))))) | |
210 | 252 | }, function (err) { |
211 | 253 | if (err) console.error(err) |
212 | 254 | }) |
213 | 255 | ) |
@@ -285,8 +327,117 @@ | ||
285 | 327 | return el |
286 | 328 | } |
287 | 329 | } |
288 | 330 | |
331 | +function findMessageContent(el) { | |
332 | + for(; el; el = el.parentNode) { | |
333 | + if(el.classList.contains('message')) { | |
334 | + return el.querySelector('.message_content') | |
335 | + } | |
336 | + } | |
337 | +} | |
338 | + | |
339 | +function issueForm(msg, contentEl) { | |
340 | + return h('form', | |
341 | + h('strong', 'New Issue:'), | |
342 | + message_compose( | |
343 | + {type: 'issue', project: msg.key}, | |
344 | + function (value) { return value }, | |
345 | + function (err, issue) { | |
346 | + if(err) return alert(err) | |
347 | + if(!issue) return | |
348 | + var title = issue.value.content.text | |
349 | + if(title.length > 70) title = title.substr(0, 70) + '…' | |
350 | + form.appendChild(h('div', | |
351 | + h('a', {href: '#'+issue.key}, title) | |
352 | + )) | |
353 | + } | |
354 | + ) | |
355 | + ) | |
356 | +} | |
357 | + | |
358 | +function branchMenu(msg, full) { | |
359 | + return combobox({ | |
360 | + style: {'max-width': '14ex'}, | |
361 | + placeholder: 'branch…', | |
362 | + default: 'master', | |
363 | + read: msg && pull(getRefs(msg), pull.map(function (ref) { | |
364 | + var m = /^refs\/heads\/(.*)$/.exec(ref.name) | |
365 | + if(!m) return | |
366 | + var branch = m[1] | |
367 | + var label = branch | |
368 | + if(full) { | |
369 | + var updated = new Date(ref.link.value.timestamp) | |
370 | + label = branch + | |
371 | + ' · ' + human(updated) + | |
372 | + ' · ' + ref.rev.substr(1, 8) + | |
373 | + (ref.title ? ' · "' + ref.title + '"' : '') | |
374 | + } | |
375 | + return h('option', {value: branch}, label) | |
376 | + })) | |
377 | + }) | |
378 | +} | |
379 | + | |
380 | +function pullRequestForm(msg) { | |
381 | + var headRepoInput | |
382 | + var headBranchInput = branchMenu() | |
383 | + var branchInput = branchMenu(msg) | |
384 | + var form = h('form', | |
385 | + h('strong', 'New Pull Request:'), | |
386 | + h('div', | |
387 | + 'from ', | |
388 | + headRepoInput = combobox({ | |
389 | + style: {'max-width': '26ex'}, | |
390 | + onchange: function () { | |
391 | + // list branches for selected repo | |
392 | + var repoId = this.value | |
393 | + if(repoId) sbot_get(repoId, function (err, value) { | |
394 | + if(err) console.error(err) | |
395 | + var msg = value && {key: repoId, value: value} | |
396 | + headBranchInput = headBranchInput.swap(branchMenu(msg, true)) | |
397 | + }) | |
398 | + else headBranchInput = headBranchInput.swap(branchMenu()) | |
399 | + }, | |
400 | + read: pull(cat([ | |
401 | + pull.once({id: msg.key, author: msg.value.author}), | |
402 | + getForks(msg.key) | |
403 | + ]), pull.map(function (fork) { | |
404 | + return h('option', {value: fork.id}, | |
405 | + repoName(fork.id), ' by ', avatar_name(fork.author)) | |
406 | + })) | |
407 | + }), | |
408 | + ':', | |
409 | + headBranchInput, | |
410 | + ' to ', | |
411 | + repoName(msg.key), | |
412 | + ':', | |
413 | + branchInput), | |
414 | + message_compose( | |
415 | + { | |
416 | + type: 'pull-request', | |
417 | + project: msg.key, | |
418 | + repo: msg.key, | |
419 | + }, | |
420 | + function (value) { | |
421 | + value.branch = branchInput.value | |
422 | + value.head_repo = headRepoInput.value | |
423 | + value.head_branch = headBranchInput.value | |
424 | + return value | |
425 | + }, | |
426 | + function (err, issue) { | |
427 | + if(err) return alert(err) | |
428 | + if(!issue) return | |
429 | + var title = issue.value.content.text | |
430 | + if(title.length > 70) title = title.substr(0, 70) + '…' | |
431 | + form.appendChild(h('div', | |
432 | + h('a', {href: '#'+issue.key}, title) | |
433 | + )) | |
434 | + } | |
435 | + ) | |
436 | + ) | |
437 | + return form | |
438 | +} | |
439 | + | |
289 | 440 | exports.message_action = function (msg, sbot) { |
290 | 441 | var c = msg.value.content |
291 | 442 | if(c.type === 'issue' || c.type === 'pull-request') { |
292 | 443 | var isOpen |
Built with git-ssb-web