Files: 7830521a20a57b285da7357ef05f53b3dd072424 / modules / git.js
13605 bytesRaw
1 | var h = require('hyperscript') |
2 | var pull = require('pull-stream') |
3 | var paramap = require('pull-paramap') |
4 | var cat = require('pull-cat') |
5 | var human = require('human-time') |
6 | var combobox = require('hypercombo') |
7 | |
8 | var plugs = require('../plugs') |
9 | var message_link = plugs.first(exports.message_link = []) |
10 | var message_confirm = plugs.first(exports.message_confirm = []) |
11 | var message_compose = plugs.first(exports.message_compose = []) |
12 | var sbot_links = plugs.first(exports.sbot_links = []) |
13 | var sbot_links2 = plugs.first(exports.sbot_links2 = []) |
14 | var sbot_get = plugs.first(exports.sbot_get = []) |
15 | var getAvatar = require('ssb-avatar') |
16 | var avatar_name = plugs.first(exports.avatar_name = []) |
17 | var markdown = plugs.first(exports.markdown = []) |
18 | |
19 | var self_id = require('../keys').id |
20 | |
21 | function shortRefName(ref) { |
22 | return ref.replace(/^refs\/(heads|tags)\//, '') |
23 | } |
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 | |
80 | function repoLink(id) { |
81 | var el = h('a', {href: '#'+id}, id.substr(0, 10) + '…') |
82 | getAvatar({links: sbot_links, get: sbot_get}, self_id, id, |
83 | function (err, avatar) { |
84 | if(err) return console.error(err) |
85 | el.textContent = avatar.name |
86 | }) |
87 | return el |
88 | } |
89 | |
90 | function getIssueState(id, cb) { |
91 | pull( |
92 | sbot_links({dest: id, rel: 'issues', values: true, reverse: true}), |
93 | pull.map(function (msg) { |
94 | return msg.value.content.issues |
95 | }), |
96 | pull.flatten(), |
97 | pull.filter(function (issue) { |
98 | return issue.link === id |
99 | }), |
100 | pull.map(function (issue) { |
101 | return issue.merged ? 'merged' : issue.open ? 'open' : 'closed' |
102 | }), |
103 | pull.take(1), |
104 | pull.collect(function (err, updates) { |
105 | cb(err, updates && updates[0] || 'open') |
106 | }) |
107 | ) |
108 | } |
109 | |
110 | //todo: |
111 | function messageTimestampLink(msg) { |
112 | var date = new Date(msg.value.timestamp) |
113 | return h('a.timestamp', { |
114 | timestamp: msg.value.timestamp, |
115 | title: date, |
116 | href: '#'+msg.key |
117 | }, human(date)) |
118 | } |
119 | |
120 | // a thead+tbody where the thead only is added when the first row is added |
121 | function tableRows(headerRow) { |
122 | var thead = h('thead'), tbody = h('tbody') |
123 | var first = true |
124 | var t = [thead, tbody] |
125 | t.append = function (row) { |
126 | if (first) { |
127 | first = false |
128 | thead.appendChild(headerRow) |
129 | } |
130 | tbody.appendChild(row) |
131 | } |
132 | return t |
133 | } |
134 | |
135 | function repoName(id, link) { |
136 | var el = link |
137 | ? h('a', {href: '#'+id}, id.substr(0, 8) + '…') |
138 | : h('ins', id.substr(0, 8) + '…') |
139 | getAvatar({links: sbot_links}, self_id, id, function (err, avatar) { |
140 | if(err) return console.error(err) |
141 | el.textContent = avatar.name |
142 | }) |
143 | return el |
144 | } |
145 | |
146 | function renderIssueEdit(c) { |
147 | var id = c.issue || c.link |
148 | return [ |
149 | c.title ? h('p', 'renamed issue ', message_link(id), |
150 | ' to ', h('ins', c.title)) : null, |
151 | c.open === false ? h('p', 'closed issue ', message_link(id)) : null, |
152 | c.open === true ? h('p', 'reopened issue ', message_link(id)) : null] |
153 | } |
154 | |
155 | exports.message_content = function (msg, sbot) { |
156 | var c = msg.value.content |
157 | |
158 | if(c.type === 'git-repo') { |
159 | var branchesT, tagsT, openIssuesT, closedIssuesT, openPRsT, closedPRsT |
160 | var forksT |
161 | var div = h('div', |
162 | h('p', 'git repo ', repoName(msg.key)), |
163 | c.upstream ? h('p', 'fork of ', repoName(c.upstream, true)) : '', |
164 | h('p', h('code', 'ssb://' + msg.key)), |
165 | h('div.git-table-wrapper', {style: {'max-height': '12em'}}, |
166 | h('table', |
167 | branchesT = tableRows(h('tr', |
168 | h('th', 'branch'), |
169 | h('th', 'commit'), |
170 | h('th', 'last update'))), |
171 | tagsT = tableRows(h('tr', |
172 | h('th', 'tag'), |
173 | h('th', 'commit'), |
174 | h('th', 'last update'))))), |
175 | h('div.git-table-wrapper', {style: {'max-height': '16em'}}, |
176 | h('table', |
177 | openIssuesT = tableRows(h('tr', |
178 | h('th', 'open issues'))), |
179 | closedIssuesT = tableRows(h('tr', |
180 | h('th', 'closed issues'))))), |
181 | h('div.git-table-wrapper', {style: {'max-height': '16em'}}, |
182 | h('table', |
183 | openPRsT = tableRows(h('tr', |
184 | h('th', 'open pull requests'))), |
185 | closedPRsT = tableRows(h('tr', |
186 | h('th', 'closed pull requests'))))), |
187 | h('div.git-table-wrapper', |
188 | h('table', |
189 | forksT = tableRows(h('tr', |
190 | h('th', 'forks'))))), |
191 | h('div', h('a', {href: '#', onclick: function () { |
192 | this.parentNode.replaceChild(issueForm(msg), this) |
193 | }}, 'New Issue…')), |
194 | h('div', h('a', {href: '#', onclick: function () { |
195 | this.parentNode.replaceChild(pullRequestForm(msg), this) |
196 | }}, 'New Pull Request…'))) |
197 | |
198 | pull(getRefs(msg), pull.drain(function (ref) { |
199 | var parts = /^refs\/(heads|tags)\/(.*)$/.exec(ref.name) || [] |
200 | var t |
201 | if(parts[1] === 'heads') t = branchesT |
202 | else if(parts[1] === 'tags') t = tagsT |
203 | if(t) t.append(h('tr', |
204 | h('td', parts[2]), |
205 | h('td', h('code', ref.rev)), |
206 | h('td', messageTimestampLink(ref.link)))) |
207 | }, function (err) { |
208 | if(err) console.error(err) |
209 | })) |
210 | |
211 | // list issues and pull requests |
212 | pull( |
213 | sbot_links({ |
214 | reverse: true, |
215 | dest: msg.key, |
216 | rel: 'project', |
217 | values: true |
218 | }), |
219 | paramap(function (link, cb) { |
220 | getIssueState(link.key, function (err, state) { |
221 | if(err) return cb(err) |
222 | link.state = state |
223 | cb(null, link) |
224 | }) |
225 | }), |
226 | pull.drain(function (link) { |
227 | var c = link.value.content |
228 | var title = c.title || (c.text ? c.text.length > 70 |
229 | ? c.text.substr(0, 70) + '…' |
230 | : c.text : link.key) |
231 | var author = link.value.author |
232 | var t = c.type === 'pull-request' |
233 | ? link.state === 'open' ? openPRsT : closedPRsT |
234 | : link.state === 'open' ? openIssuesT : closedIssuesT |
235 | t.append(h('tr', |
236 | h('td', |
237 | h('a', {href: '#'+link.key}, title), h('br'), |
238 | h('small', |
239 | 'opened ', messageTimestampLink(link), |
240 | ' by ', h('a', {href: '#'+author}, avatar_name(author)))))) |
241 | }, function (err) { |
242 | if (err) console.error(err) |
243 | }) |
244 | ) |
245 | |
246 | // list forks |
247 | pull( |
248 | getForks(msg.key), |
249 | pull.drain(function (fork) { |
250 | forksT.append(h('tr', h('td', |
251 | repoName(fork.id, true), |
252 | ' by ', h('a', {href: '#'+fork.author}, avatar_name(fork.author))))) |
253 | }, function (err) { |
254 | if (err) console.error(err) |
255 | }) |
256 | ) |
257 | |
258 | return div |
259 | } |
260 | |
261 | if(c.type === 'git-update') { |
262 | return [ |
263 | h('p', 'pushed to ', repoLink(c.repo)), |
264 | c.refs ? h('ul', Object.keys(c.refs).map(function (ref) { |
265 | var rev = c.refs[ref] |
266 | return h('li', |
267 | shortRefName(ref) + ': ', |
268 | rev ? h('code', rev) : h('em', 'deleted')) |
269 | })) : null, |
270 | Array.isArray(c.commits) ? [ |
271 | h('ul', |
272 | c.commits.map(function (commit) { |
273 | return h('li', |
274 | typeof commit.sha1 === 'string' ? |
275 | [h('code', commit.sha1.substr(0, 8)), ' '] : null, |
276 | commit.title ? |
277 | h('q', commit.title) : null) |
278 | }), |
279 | c.commits_more > 0 ? |
280 | h('li', '+ ', c.commits_more, ' more') : null) |
281 | ] : null, |
282 | Array.isArray(c.issues) ? c.issues.map(function (issue) { |
283 | if (issue.merged === true) |
284 | return h('p', 'Merged ', message_link(issue.link), ' in ', |
285 | h('code', issue.object), ' ', h('q', issue.label)) |
286 | if (issue.open === false) |
287 | return h('p', 'Closed ', message_link(issue.link), ' in ', |
288 | h('code', issue.object), ' ', h('q', issue.label)) |
289 | }) : null |
290 | ] |
291 | } |
292 | |
293 | if(c.type === 'issue-edit') { |
294 | return h('div', |
295 | c.issue ? renderIssueEdit(c) : null, |
296 | c.issues ? c.issues.map(renderIssueEdit) : null) |
297 | } |
298 | |
299 | if(c.type === 'issue') { |
300 | return h('div', |
301 | h('p', 'opened issue on ', repoLink(c.project)), |
302 | c.title ? h('h4', c.title) : '', |
303 | markdown(c) |
304 | ) |
305 | } |
306 | |
307 | if(c.type === 'pull-request') { |
308 | return h('div', |
309 | h('p', 'opened pull-request ', |
310 | 'to ', repoLink(c.repo), ':', c.branch, ' ', |
311 | 'from ', repoLink(c.head_repo), ':', c.head_branch), |
312 | c.title ? h('h4', c.title) : '', |
313 | markdown(c) |
314 | ) |
315 | } |
316 | } |
317 | |
318 | exports.message_meta = function (msg, sbot) { |
319 | var type = msg.value.content.type |
320 | if (type === 'issue' || type === 'pull-request') { |
321 | var el = h('em', '...') |
322 | // TODO: update if issue is changed |
323 | getIssueState(msg.key, function (err, state) { |
324 | if (err) return console.error(err) |
325 | el.textContent = state |
326 | }) |
327 | return el |
328 | } |
329 | } |
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 | |
440 | exports.message_action = function (msg, sbot) { |
441 | var c = msg.value.content |
442 | if(c.type === 'issue' || c.type === 'pull-request') { |
443 | var isOpen |
444 | var a = h('a', {href: '#', onclick: function () { |
445 | message_confirm({ |
446 | type: 'issue-edit', |
447 | root: msg.key, |
448 | issues: [{ |
449 | link: msg.key, |
450 | open: !isOpen |
451 | }] |
452 | }, function (err, msg) { |
453 | if(err) return alert(err) |
454 | if(!msg) return |
455 | isOpen = msg.value.content.open |
456 | update() |
457 | }) |
458 | }}) |
459 | getIssueState(msg.key, function (err, state) { |
460 | if (err) return console.error(err) |
461 | isOpen = state === 'open' |
462 | update() |
463 | }) |
464 | function update() { |
465 | a.textContent = c.type === 'pull-request' |
466 | ? isOpen ? 'Close Pull Request' : 'Reopen Pull Request' |
467 | : isOpen ? 'Close Issue' : 'Reopen Issue' |
468 | } |
469 | return a |
470 | } |
471 | } |
472 | |
473 |
Built with git-ssb-web