Files: f1cc08ac986f18880000b780dae6fb2043e76948 / modules / git.js
14679 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 | var avatar = require('./avatar') |
15 | |
16 | |
17 | exports.needs = { |
18 | message_confirm: 'first', |
19 | message_compose: 'first' |
20 | } |
21 | |
22 | exports.gives = { |
23 | message_action: true, |
24 | message_meta: true, |
25 | message_content: true |
26 | } |
27 | |
28 | |
29 | function shortRefName(ref) { |
30 | return ref.replace(/^refs\/(heads|tags)\//, '') |
31 | } |
32 | |
33 | exports.create = function (api) { |
34 | function getRefs(msg) { |
35 | var updates = new KVGraph('key') |
36 | var _cb, _refs |
37 | pull( |
38 | links({ |
39 | reverse: true, |
40 | // source: msg.value.author, |
41 | dest: msg.key, |
42 | rel: 'repo', |
43 | values: true |
44 | }), |
45 | pull.drain(function (link) { |
46 | if (link.value.content.type === 'git-update') { |
47 | updates.add(link) |
48 | } |
49 | }, function (err) { |
50 | var refs = updates.reduceRight(mergeRepo).refs |
51 | var cb = _cb |
52 | if (cb) _cb = null, cb(err, refs) |
53 | else _refs = refs |
54 | }) |
55 | ) |
56 | |
57 | return pull( |
58 | function fn(end, cb) { |
59 | if (end || fn.ended) cb(true) |
60 | fn.ended = true |
61 | if (_refs) cb(_refs) |
62 | else _cb = cb |
63 | }, |
64 | pull.flatten() |
65 | ) |
66 | } |
67 | |
68 | function getForks(id) { |
69 | return pull( |
70 | links({ |
71 | reverse: true, |
72 | dest: id, |
73 | rel: 'upstream' |
74 | }), |
75 | pull.map(function (link) { |
76 | return { |
77 | id: link.key, |
78 | author: link.source |
79 | } |
80 | }) |
81 | ) |
82 | } |
83 | |
84 | function repoLink(id) { |
85 | return h('a', {href: '#'+id}, messageLink(id)) |
86 | } |
87 | |
88 | function repoName(id) { |
89 | return h('ins', messageLink(id)) |
90 | } |
91 | |
92 | function getIssueState(id, cb) { |
93 | pull( |
94 | links({dest: id, rel: 'issues', values: true, reverse: true}), |
95 | pull.map(function (msg) { |
96 | return msg.value.content.issues |
97 | }), |
98 | pull.flatten(), |
99 | pull.filter(function (issue) { |
100 | return issue.link === id |
101 | }), |
102 | pull.map(function (issue) { |
103 | return issue.merged ? 'merged' : issue.open ? 'open' : 'closed' |
104 | }), |
105 | pull.take(1), |
106 | pull.collect(function (err, updates) { |
107 | cb(err, updates && updates[0] || 'open') |
108 | }) |
109 | ) |
110 | } |
111 | |
112 | //todo: |
113 | function messageTimestampLink(msg) { |
114 | var date = new Date(msg.value.timestamp) |
115 | return h('a.timestamp', { |
116 | timestamp: msg.value.timestamp, |
117 | title: date, |
118 | href: '#'+msg.key |
119 | }, human(date)) |
120 | } |
121 | |
122 | // a thead+tbody where the thead only is added when the first row is added |
123 | function tableRows(headerRow) { |
124 | var thead = h('thead'), tbody = h('tbody') |
125 | var first = true |
126 | var t = [thead, tbody] |
127 | t.append = function (row) { |
128 | if (first) { |
129 | first = false |
130 | thead.appendChild(headerRow) |
131 | } |
132 | tbody.appendChild(row) |
133 | } |
134 | return t |
135 | } |
136 | |
137 | function renderIssueEdit(c) { |
138 | var id = c.issue || c.link |
139 | return [ |
140 | c.title ? h('p', 'renamed issue ', messageLink(id), |
141 | ' to ', h('ins', c.title)) : null, |
142 | c.open === false ? h('p', 'closed issue ', messageLink(id)) : null, |
143 | c.open === true ? h('p', 'reopened issue ', messageLink(id)) : null] |
144 | } |
145 | |
146 | function findMessageContent(el) { |
147 | for(; el; el = el.parentNode) { |
148 | if(el.classList.contains('message')) { |
149 | return el.querySelector('.message_content') |
150 | } |
151 | } |
152 | } |
153 | |
154 | function issueForm(msg, contentEl) { |
155 | var form = h('form', |
156 | h('strong', 'New Issue:'), |
157 | api.message_compose( |
158 | {type: 'issue', project: msg.key}, |
159 | function (value) { return value }, |
160 | function (err, issue) { |
161 | if(err) return alert(err) |
162 | if(!issue) return |
163 | var title = issue.value.content.text |
164 | if(title.length > 70) title = title.substr(0, 70) + '…' |
165 | form.appendChild(h('div', |
166 | h('a', {href: '#'+issue.key}, title) |
167 | )) |
168 | } |
169 | ) |
170 | ) |
171 | return form |
172 | } |
173 | |
174 | function branchMenu(msg, full) { |
175 | return combobox({ |
176 | style: {'max-width': '14ex'}, |
177 | placeholder: 'branch…', |
178 | default: 'master', |
179 | read: msg && pull(getRefs(msg), pull.map(function (ref) { |
180 | var m = /^refs\/heads\/(.*)$/.exec(ref.name) |
181 | if(!m) return |
182 | var branch = m[1] |
183 | var label = branch |
184 | if(full) { |
185 | var updated = new Date(ref.link.value.timestamp) |
186 | label = branch + |
187 | ' · ' + human(updated) + |
188 | ' · ' + ref.hash.substr(1, 8) + |
189 | (ref.title ? ' · "' + ref.title + '"' : '') |
190 | } |
191 | return h('option', {value: branch}, label) |
192 | })) |
193 | }) |
194 | } |
195 | |
196 | function newPullRequestButton(msg) { |
197 | return h('div', [ |
198 | h('a', { |
199 | href: '#', |
200 | onclick: function (e) { |
201 | e.preventDefault() |
202 | this.parentNode.replaceChild(pullRequestForm(msg), this) |
203 | }}, |
204 | 'New Pull Request…' |
205 | ) |
206 | ]) |
207 | } |
208 | |
209 | function pullRequestForm(msg) { |
210 | var headRepoInput |
211 | var headBranchInput = branchMenu() |
212 | var branchInput = branchMenu(msg) |
213 | var form = h('form', |
214 | h('strong', 'New Pull Request:'), |
215 | h('div', |
216 | 'from ', |
217 | headRepoInput = combobox({ |
218 | style: {'max-width': '26ex'}, |
219 | onchange: function () { |
220 | // list branches for selected repo |
221 | var repoId = this.value |
222 | if(repoId) get(repoId, function (err, value) { |
223 | if(err) console.error(err) |
224 | var msg = value && {key: repoId, value: value} |
225 | headBranchInput = headBranchInput.swap(branchMenu(msg, true)) |
226 | }) |
227 | else headBranchInput = headBranchInput.swap(branchMenu()) |
228 | }, |
229 | read: pull(cat([ |
230 | pull.once({id: msg.key, author: msg.value.author}), |
231 | getForks(msg.key) |
232 | ]), pull.map(function (fork) { |
233 | return h('option', {value: fork.id}, |
234 | repoLink(fork.id), ' by ', avatar.name(fork.author)) |
235 | })) |
236 | }), |
237 | ':', |
238 | headBranchInput, |
239 | ' to ', |
240 | repoName(msg.key), |
241 | ':', |
242 | branchInput), |
243 | api.message_compose( |
244 | { |
245 | type: 'pull-request', |
246 | project: msg.key, |
247 | repo: msg.key, |
248 | }, |
249 | function (value) { |
250 | value.branch = branchInput.value |
251 | value.head_repo = headRepoInput.value |
252 | value.head_branch = headBranchInput.value |
253 | return value |
254 | }, |
255 | function (err, issue) { |
256 | if(err) return alert(err) |
257 | if(!issue) return |
258 | var title = issue.value.content.text |
259 | if(title.length > 70) title = title.substr(0, 70) + '…' |
260 | form.appendChild(h('div', |
261 | h('a', {href: '#'+issue.key}, title) |
262 | )) |
263 | } |
264 | ) |
265 | ) |
266 | return form |
267 | } |
268 | |
269 | return { |
270 | message_content: function (msg, sbot) { |
271 | var c = msg.value.content |
272 | |
273 | if(c.type === 'git-repo') { |
274 | var branchesT, tagsT, openIssuesT, closedIssuesT, openPRsT, closedPRsT |
275 | var forksT |
276 | var div = h('div', |
277 | h('p', 'git repo ', repoName(msg.key)), |
278 | c.upstream ? h('p', 'fork of ', repoLink(c.upstream)) : '', |
279 | h('p', h('code', 'ssb://' + msg.key)), |
280 | h('div.git-table-wrapper', {style: {'max-height': '12em'}}, |
281 | h('table', |
282 | branchesT = tableRows(h('tr', |
283 | h('th', 'branch'), |
284 | h('th', 'commit'), |
285 | h('th', 'last update'))), |
286 | tagsT = tableRows(h('tr', |
287 | h('th', 'tag'), |
288 | h('th', 'commit'), |
289 | h('th', 'last update'))))), |
290 | h('div.git-table-wrapper', {style: {'max-height': '16em'}}, |
291 | h('table', |
292 | openIssuesT = tableRows(h('tr', |
293 | h('th', 'open issues'))), |
294 | closedIssuesT = tableRows(h('tr', |
295 | h('th', 'closed issues'))))), |
296 | h('div.git-table-wrapper', {style: {'max-height': '16em'}}, |
297 | h('table', |
298 | openPRsT = tableRows(h('tr', |
299 | h('th', 'open pull requests'))), |
300 | closedPRsT = tableRows(h('tr', |
301 | h('th', 'closed pull requests'))))), |
302 | h('div.git-table-wrapper', |
303 | h('table', |
304 | forksT = tableRows(h('tr', |
305 | h('th', 'forks'))))), |
306 | h('div', h('a', {href: '#', onclick: function (e) { |
307 | e.preventDefault() |
308 | this.parentNode.replaceChild(issueForm(msg), this) |
309 | }}, 'New Issue…')), |
310 | newPullRequestButton.call(this, msg) |
311 | ) |
312 | |
313 | pull(getRefs(msg), pull.drain(function (ref) { |
314 | var name = ref.realname || ref.name |
315 | var author = ref.link && ref.link.value.author |
316 | var parts = /^refs\/(heads|tags)\/(.*)$/.exec(name) || [] |
317 | var shortName = parts[2] |
318 | var t |
319 | if(parts[1] === 'heads') t = branchesT |
320 | else if(parts[1] === 'tags') t = tagsT |
321 | if(t) t.append(h('tr', |
322 | h('td', shortName, |
323 | ref.conflict ? [ |
324 | h('br'), |
325 | h('a', {href: '#'+author}, avatar.name(author)) |
326 | ] : ''), |
327 | h('td', h('code', ref.hash)), |
328 | h('td', messageTimestampLink(ref.link)))) |
329 | }, function (err) { |
330 | if(err) console.error(err) |
331 | })) |
332 | |
333 | // list issues and pull requests |
334 | pull( |
335 | links({ |
336 | reverse: true, |
337 | dest: msg.key, |
338 | rel: 'project', |
339 | values: true |
340 | }), |
341 | paramap(function (link, cb) { |
342 | getIssueState(link.key, function (err, state) { |
343 | if(err) return cb(err) |
344 | link.state = state |
345 | cb(null, link) |
346 | }) |
347 | }), |
348 | pull.drain(function (link) { |
349 | var c = link.value.content |
350 | var title = c.title || (c.text ? c.text.length > 70 |
351 | ? c.text.substr(0, 70) + '…' |
352 | : c.text : link.key) |
353 | var author = link.value.author |
354 | var t = c.type === 'pull-request' |
355 | ? link.state === 'open' ? openPRsT : closedPRsT |
356 | : link.state === 'open' ? openIssuesT : closedIssuesT |
357 | t.append(h('tr', |
358 | h('td', |
359 | h('a', {href: '#'+link.key}, title), h('br'), |
360 | h('small', |
361 | 'opened ', messageTimestampLink(link), |
362 | ' by ', h('a', {href: '#'+author}, avatar.name(author)))))) |
363 | }, function (err) { |
364 | if (err) console.error(err) |
365 | }) |
366 | ) |
367 | |
368 | // list forks |
369 | pull( |
370 | getForks(msg.key), |
371 | pull.drain(function (fork) { |
372 | forksT.append(h('tr', h('td', |
373 | repoLink(fork.id), |
374 | ' by ', h('a', {href: '#'+fork.author}, avatar.name(fork.author))))) |
375 | }, function (err) { |
376 | if (err) console.error(err) |
377 | }) |
378 | ) |
379 | |
380 | return div |
381 | } |
382 | |
383 | if(c.type === 'git-update') { |
384 | return [ |
385 | h('p', 'pushed to ', repoLink(c.repo)), |
386 | c.refs ? h('ul', Object.keys(c.refs).map(function (ref) { |
387 | var rev = c.refs[ref] |
388 | return h('li', |
389 | shortRefName(ref) + ': ', |
390 | rev ? h('code', rev) : h('em', 'deleted')) |
391 | })) : null, |
392 | Array.isArray(c.commits) ? [ |
393 | h('ul', |
394 | c.commits.map(function (commit) { |
395 | return h('li', |
396 | typeof commit.sha1 === 'string' ? |
397 | [h('code', commit.sha1.substr(0, 8)), ' '] : null, |
398 | commit.title ? |
399 | h('q', commit.title) : null) |
400 | }), |
401 | c.commits_more > 0 ? |
402 | h('li', '+ ', c.commits_more, ' more') : null) |
403 | ] : null, |
404 | Array.isArray(c.issues) ? c.issues.map(function (issue) { |
405 | if (issue.merged === true) |
406 | return h('p', 'Merged ', messageLink(issue.link), ' in ', |
407 | h('code', issue.object), ' ', h('q', issue.label)) |
408 | if (issue.open === false) |
409 | return h('p', 'Closed ', messageLink(issue.link), ' in ', |
410 | h('code', issue.object), ' ', h('q', issue.label)) |
411 | }) : null, |
412 | newPullRequestButton.call(this, msg) |
413 | ] |
414 | } |
415 | |
416 | if(c.type === 'issue-edit' |
417 | || (c.type === 'post' && c.text === '')) { |
418 | return h('div', |
419 | c.issue ? renderIssueEdit(c) : null, |
420 | c.issues ? c.issues.map(renderIssueEdit) : null) |
421 | } |
422 | |
423 | if(c.type === 'issue') { |
424 | return h('div', |
425 | h('p', 'opened issue on ', repoLink(c.project)), |
426 | c.title ? h('h4', c.title) : '', |
427 | markdown(c) |
428 | ) |
429 | } |
430 | |
431 | if(c.type === 'pull-request') { |
432 | return h('div', |
433 | h('p', 'opened pull-request ', |
434 | 'to ', repoLink(c.repo), ':', c.branch, ' ', |
435 | 'from ', repoLink(c.head_repo), ':', c.head_branch), |
436 | c.title ? h('h4', c.title) : '', |
437 | markdown(c) |
438 | ) |
439 | } |
440 | }, |
441 | |
442 | message_meta: function (msg, sbot) { |
443 | var type = msg.value.content.type |
444 | if (type === 'issue' || type === 'pull-request') { |
445 | var el = h('em', '...') |
446 | // TODO: update if issue is changed |
447 | getIssueState(msg.key, function (err, state) { |
448 | if (err) return console.error(err) |
449 | el.textContent = state |
450 | }) |
451 | return el |
452 | } |
453 | }, |
454 | |
455 | message_action: function (msg, sbot) { |
456 | var c = msg.value.content |
457 | if(c.type === 'issue' || c.type === 'pull-request') { |
458 | var isOpen |
459 | var a = h('a', {href: '#', onclick: function (e) { |
460 | e.preventDefault() |
461 | api.message_confirm({ |
462 | type: 'issue-edit', |
463 | root: msg.key, |
464 | issues: [{ |
465 | link: msg.key, |
466 | open: !isOpen |
467 | }] |
468 | }, function (err, msg) { |
469 | if(err) return alert(err) |
470 | if(!msg) return |
471 | isOpen = msg.value.content.open |
472 | update() |
473 | }) |
474 | }}) |
475 | getIssueState(msg.key, function (err, state) { |
476 | if (err) return console.error(err) |
477 | isOpen = state === 'open' |
478 | update() |
479 | }) |
480 | function update() { |
481 | a.textContent = c.type === 'pull-request' |
482 | ? isOpen ? 'Close Pull Request' : 'Reopen Pull Request' |
483 | : isOpen ? 'Close Issue' : 'Reopen Issue' |
484 | } |
485 | return a |
486 | } |
487 | } |
488 | } |
489 | } |
490 | |
491 | |
492 |
Built with git-ssb-web