git ssb

16+

Dominic / patchbay



Tree: 9916a3c063184f892f6b76d2865ecd6b8b3af8b8

Files: 9916a3c063184f892f6b76d2865ecd6b8b3af8b8 / modules / git.js

13530 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 repoText(id) {
81 var text = document.createTextNode(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 text.nodeValue = avatar.name
86 })
87 return text
88}
89
90function repoLink(id) {
91 return h('a', {href: '#'+id}, repoText(id))
92}
93
94function repoName(id) {
95 return h('ins', repoText(id))
96}
97
98function getIssueState(id, cb) {
99 pull(
100 sbot_links({dest: id, rel: 'issues', values: true, reverse: true}),
101 pull.map(function (msg) {
102 return msg.value.content.issues
103 }),
104 pull.flatten(),
105 pull.filter(function (issue) {
106 return issue.link === id
107 }),
108 pull.map(function (issue) {
109 return issue.merged ? 'merged' : issue.open ? 'open' : 'closed'
110 }),
111 pull.take(1),
112 pull.collect(function (err, updates) {
113 cb(err, updates && updates[0] || 'open')
114 })
115 )
116}
117
118//todo:
119function messageTimestampLink(msg) {
120 var date = new Date(msg.value.timestamp)
121 return h('a.timestamp', {
122 timestamp: msg.value.timestamp,
123 title: date,
124 href: '#'+msg.key
125 }, human(date))
126}
127
128// a thead+tbody where the thead only is added when the first row is added
129function tableRows(headerRow) {
130 var thead = h('thead'), tbody = h('tbody')
131 var first = true
132 var t = [thead, tbody]
133 t.append = function (row) {
134 if (first) {
135 first = false
136 thead.appendChild(headerRow)
137 }
138 tbody.appendChild(row)
139 }
140 return t
141}
142
143function renderIssueEdit(c) {
144 var id = c.issue || c.link
145 return [
146 c.title ? h('p', 'renamed issue ', message_link(id),
147 ' to ', h('ins', c.title)) : null,
148 c.open === false ? h('p', 'closed issue ', message_link(id)) : null,
149 c.open === true ? h('p', 'reopened issue ', message_link(id)) : null]
150}
151
152exports.message_content = function (msg, sbot) {
153 var c = msg.value.content
154
155 if(c.type === 'git-repo') {
156 var branchesT, tagsT, openIssuesT, closedIssuesT, openPRsT, closedPRsT
157 var forksT
158 var div = h('div',
159 h('p', 'git repo ', repoName(msg.key)),
160 c.upstream ? h('p', 'fork of ', repoLink(c.upstream)) : '',
161 h('p', h('code', 'ssb://' + msg.key)),
162 h('div.git-table-wrapper', {style: {'max-height': '12em'}},
163 h('table',
164 branchesT = tableRows(h('tr',
165 h('th', 'branch'),
166 h('th', 'commit'),
167 h('th', 'last update'))),
168 tagsT = tableRows(h('tr',
169 h('th', 'tag'),
170 h('th', 'commit'),
171 h('th', 'last update'))))),
172 h('div.git-table-wrapper', {style: {'max-height': '16em'}},
173 h('table',
174 openIssuesT = tableRows(h('tr',
175 h('th', 'open issues'))),
176 closedIssuesT = tableRows(h('tr',
177 h('th', 'closed issues'))))),
178 h('div.git-table-wrapper', {style: {'max-height': '16em'}},
179 h('table',
180 openPRsT = tableRows(h('tr',
181 h('th', 'open pull requests'))),
182 closedPRsT = tableRows(h('tr',
183 h('th', 'closed pull requests'))))),
184 h('div.git-table-wrapper',
185 h('table',
186 forksT = tableRows(h('tr',
187 h('th', 'forks'))))),
188 h('div', h('a', {href: '#', onclick: function (e) {
189 e.preventDefault()
190 this.parentNode.replaceChild(issueForm(msg), this)
191 }}, 'New Issue…')),
192 h('div', h('a', {href: '#', onclick: function (e) {
193 e.preventDefault()
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 repoLink(fork.id),
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 var form = 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 return form
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 repoLink(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 (e) {
445 e.preventDefault()
446 message_confirm({
447 type: 'issue-edit',
448 root: msg.key,
449 issues: [{
450 link: msg.key,
451 open: !isOpen
452 }]
453 }, function (err, msg) {
454 if(err) return alert(err)
455 if(!msg) return
456 isOpen = msg.value.content.open
457 update()
458 })
459 }})
460 getIssueState(msg.key, function (err, state) {
461 if (err) return console.error(err)
462 isOpen = state === 'open'
463 update()
464 })
465 function update() {
466 a.textContent = c.type === 'pull-request'
467 ? isOpen ? 'Close Pull Request' : 'Reopen Pull Request'
468 : isOpen ? 'Close Issue' : 'Reopen Issue'
469 }
470 return a
471 }
472}
473
474

Built with git-ssb-web