git ssb

0+

ev / microbay



forked from Dominic / patchbay

Tree: f5bb452e8bc993b0fd216fb1d6c913481760ad45

Files: f5bb452e8bc993b0fd216fb1d6c913481760ad45 / modules / git.js

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

Built with git-ssb-web