git ssb

16+

Dominic / patchbay



Tree: 7830521a20a57b285da7357ef05f53b3dd072424

Files: 7830521a20a57b285da7357ef05f53b3dd072424 / modules / git.js

13605 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, get: sbot_get}, self_id, id,
83 function (err, avatar) {
84 if(err) return console.error(err)
85 el.textContent = avatar.name
86 })
87 return el
88}
89
90function getIssueState(id, cb) {
91 pull(
92 sbot_links({dest: id, rel: 'issues', values: true, reverse: true}),
93 pull.map(function (msg) {
94 return msg.value.content.issues
95 }),
96 pull.flatten(),
97 pull.filter(function (issue) {
98 return issue.link === id
99 }),
100 pull.map(function (issue) {
101 return issue.merged ? 'merged' : issue.open ? 'open' : 'closed'
102 }),
103 pull.take(1),
104 pull.collect(function (err, updates) {
105 cb(err, updates && updates[0] || 'open')
106 })
107 )
108}
109
110//todo:
111function messageTimestampLink(msg) {
112 var date = new Date(msg.value.timestamp)
113 return h('a.timestamp', {
114 timestamp: msg.value.timestamp,
115 title: date,
116 href: '#'+msg.key
117 }, human(date))
118}
119
120// a thead+tbody where the thead only is added when the first row is added
121function tableRows(headerRow) {
122 var thead = h('thead'), tbody = h('tbody')
123 var first = true
124 var t = [thead, tbody]
125 t.append = function (row) {
126 if (first) {
127 first = false
128 thead.appendChild(headerRow)
129 }
130 tbody.appendChild(row)
131 }
132 return t
133}
134
135function repoName(id, link) {
136 var el = link
137 ? h('a', {href: '#'+id}, id.substr(0, 8) + '…')
138 : h('ins', id.substr(0, 8) + '…')
139 getAvatar({links: sbot_links}, self_id, id, function (err, avatar) {
140 if(err) return console.error(err)
141 el.textContent = avatar.name
142 })
143 return el
144}
145
146function renderIssueEdit(c) {
147 var id = c.issue || c.link
148 return [
149 c.title ? h('p', 'renamed issue ', message_link(id),
150 ' to ', h('ins', c.title)) : null,
151 c.open === false ? h('p', 'closed issue ', message_link(id)) : null,
152 c.open === true ? h('p', 'reopened issue ', message_link(id)) : null]
153}
154
155exports.message_content = function (msg, sbot) {
156 var c = msg.value.content
157
158 if(c.type === 'git-repo') {
159 var branchesT, tagsT, openIssuesT, closedIssuesT, openPRsT, closedPRsT
160 var forksT
161 var div = h('div',
162 h('p', 'git repo ', repoName(msg.key)),
163 c.upstream ? h('p', 'fork of ', repoName(c.upstream, true)) : '',
164 h('p', h('code', 'ssb://' + msg.key)),
165 h('div.git-table-wrapper', {style: {'max-height': '12em'}},
166 h('table',
167 branchesT = tableRows(h('tr',
168 h('th', 'branch'),
169 h('th', 'commit'),
170 h('th', 'last update'))),
171 tagsT = tableRows(h('tr',
172 h('th', 'tag'),
173 h('th', 'commit'),
174 h('th', 'last update'))))),
175 h('div.git-table-wrapper', {style: {'max-height': '16em'}},
176 h('table',
177 openIssuesT = tableRows(h('tr',
178 h('th', 'open issues'))),
179 closedIssuesT = tableRows(h('tr',
180 h('th', 'closed issues'))))),
181 h('div.git-table-wrapper', {style: {'max-height': '16em'}},
182 h('table',
183 openPRsT = tableRows(h('tr',
184 h('th', 'open pull requests'))),
185 closedPRsT = tableRows(h('tr',
186 h('th', 'closed pull requests'))))),
187 h('div.git-table-wrapper',
188 h('table',
189 forksT = tableRows(h('tr',
190 h('th', 'forks'))))),
191 h('div', h('a', {href: '#', onclick: function () {
192 this.parentNode.replaceChild(issueForm(msg), this)
193 }}, 'New Issue…')),
194 h('div', h('a', {href: '#', onclick: function () {
195 this.parentNode.replaceChild(pullRequestForm(msg), this)
196 }}, 'New Pull Request…')))
197
198 pull(getRefs(msg), pull.drain(function (ref) {
199 var parts = /^refs\/(heads|tags)\/(.*)$/.exec(ref.name) || []
200 var t
201 if(parts[1] === 'heads') t = branchesT
202 else if(parts[1] === 'tags') t = tagsT
203 if(t) t.append(h('tr',
204 h('td', parts[2]),
205 h('td', h('code', ref.rev)),
206 h('td', messageTimestampLink(ref.link))))
207 }, function (err) {
208 if(err) console.error(err)
209 }))
210
211 // list issues and pull requests
212 pull(
213 sbot_links({
214 reverse: true,
215 dest: msg.key,
216 rel: 'project',
217 values: true
218 }),
219 paramap(function (link, cb) {
220 getIssueState(link.key, function (err, state) {
221 if(err) return cb(err)
222 link.state = state
223 cb(null, link)
224 })
225 }),
226 pull.drain(function (link) {
227 var c = link.value.content
228 var title = c.title || (c.text ? c.text.length > 70
229 ? c.text.substr(0, 70) + '…'
230 : c.text : link.key)
231 var author = link.value.author
232 var t = c.type === 'pull-request'
233 ? link.state === 'open' ? openPRsT : closedPRsT
234 : link.state === 'open' ? openIssuesT : closedIssuesT
235 t.append(h('tr',
236 h('td',
237 h('a', {href: '#'+link.key}, title), h('br'),
238 h('small',
239 'opened ', messageTimestampLink(link),
240 ' by ', h('a', {href: '#'+author}, avatar_name(author))))))
241 }, function (err) {
242 if (err) console.error(err)
243 })
244 )
245
246 // list forks
247 pull(
248 getForks(msg.key),
249 pull.drain(function (fork) {
250 forksT.append(h('tr', h('td',
251 repoName(fork.id, true),
252 ' by ', h('a', {href: '#'+fork.author}, avatar_name(fork.author)))))
253 }, function (err) {
254 if (err) console.error(err)
255 })
256 )
257
258 return div
259 }
260
261 if(c.type === 'git-update') {
262 return [
263 h('p', 'pushed to ', 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 h('p', 'Merged ', message_link(issue.link), ' in ',
285 h('code', issue.object), ' ', h('q', issue.label))
286 if (issue.open === false)
287 return h('p', '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