git ssb

0+

ev / minbase



Tree: af61e933b76582db7ede664f53deca0cd2bd5dcf

Files: af61e933b76582db7ede664f53deca0cd2bd5dcf / modules / git.js

14679 bytesRaw
1var h = require('hyperscript')
2var pull = require('pull-stream')
3var paramap = require('pull-paramap')
4var cat = require('pull-cat')
5var human = require('human-time')
6var combobox = require('hypercombo')
7var KVGraph = require('kvgraph')
8var mergeRepo = require('ssb-git/merge')
9var self_id = require('../keys').id
10var messageLink = require('./helpers').message_link
11var markdown = require('./helpers').markdown
12var links = require('./scuttlebot').links
13var get = require('./scuttlebot').get
14var avatar = require('./avatar')
15
16
17exports.needs = {
18 message_confirm: 'first',
19 message_compose: 'first'
20}
21
22exports.gives = {
23 message_action: true,
24 message_meta: true,
25 message_content: true
26}
27
28
29function shortRefName(ref) {
30 return ref.replace(/^refs\/(heads|tags)\//, '')
31}
32
33exports.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