git ssb

16+

Dominic / patchbay



Tree: 81ca6a35c2987aabfa0164ebca0408a78e413dcf

Files: 81ca6a35c2987aabfa0164ebca0408a78e413dcf / modules / git.js

13577 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 h('p',
262 'pushed to ',
263 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 ['Merged ', message_link(issue.link), ' in ',
285 h('code', issue.object), ' ', h('q', issue.label)]
286 if (issue.open === false)
287 return ['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
318exports.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
331function findMessageContent(el) {
332 for(; el; el = el.parentNode) {
333 if(el.classList.contains('message')) {
334 return el.querySelector('.message_content')
335 }
336 }
337}
338
339function 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
358function 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
380function 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
440exports.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