Files: 3ee0398629a78832c2fbe78831ce756faeaeca74 / modules / git.js
14693 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 | var KVGraph = require('kvgraph') |
8 | var mergeRepo = require('ssb-git/merge') |
9 | var self_id = require('../keys').id |
10 | var messageLink = require('./helpers').message_link |
11 | var markdown = require('./helpers').markdown |
12 | var links = require('./scuttlebot').links |
13 | var get = require('./scuttlebot').get |
14 | |
15 | exports.needs = { |
16 | message_confirm: 'first', |
17 | message_compose: 'first', |
18 | avatar_name: 'first' |
19 | } |
20 | |
21 | exports.gives = { |
22 | message_action: true, |
23 | message_meta: true, |
24 | message_content: true |
25 | } |
26 | |
27 | |
28 | function shortRefName(ref) { |
29 | return ref.replace(/^refs\/(heads|tags)\//, '') |
30 | } |
31 | |
32 | exports.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