git ssb

16+

Dominic / patchbay



Tree: 7be27d2e405a2a962e2b8b3743cb328dca84c297

Files: 7be27d2e405a2a962e2b8b3743cb328dca84c297 / modules / git.js

13430 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 () {
189 this.parentNode.replaceChild(issueForm(msg), this)
190 }}, 'New Issue…')),
191 h('div', h('a', {href: '#', onclick: function () {
192 this.parentNode.replaceChild(pullRequestForm(msg), this)
193 }}, 'New Pull Request…')))
194
195 pull(getRefs(msg), pull.drain(function (ref) {
196 var parts = /^refs\/(heads|tags)\/(.*)$/.exec(ref.name) || []
197 var t
198 if(parts[1] === 'heads') t = branchesT
199 else if(parts[1] === 'tags') t = tagsT
200 if(t) t.append(h('tr',
201 h('td', parts[2]),
202 h('td', h('code', ref.rev)),
203 h('td', messageTimestampLink(ref.link))))
204 }, function (err) {
205 if(err) console.error(err)
206 }))
207
208 // list issues and pull requests
209 pull(
210 sbot_links({
211 reverse: true,
212 dest: msg.key,
213 rel: 'project',
214 values: true
215 }),
216 paramap(function (link, cb) {
217 getIssueState(link.key, function (err, state) {
218 if(err) return cb(err)
219 link.state = state
220 cb(null, link)
221 })
222 }),
223 pull.drain(function (link) {
224 var c = link.value.content
225 var title = c.title || (c.text ? c.text.length > 70
226 ? c.text.substr(0, 70) + '…'
227 : c.text : link.key)
228 var author = link.value.author
229 var t = c.type === 'pull-request'
230 ? link.state === 'open' ? openPRsT : closedPRsT
231 : link.state === 'open' ? openIssuesT : closedIssuesT
232 t.append(h('tr',
233 h('td',
234 h('a', {href: '#'+link.key}, title), h('br'),
235 h('small',
236 'opened ', messageTimestampLink(link),
237 ' by ', h('a', {href: '#'+author}, avatar_name(author))))))
238 }, function (err) {
239 if (err) console.error(err)
240 })
241 )
242
243 // list forks
244 pull(
245 getForks(msg.key),
246 pull.drain(function (fork) {
247 forksT.append(h('tr', h('td',
248 repoLink(fork.id),
249 ' by ', h('a', {href: '#'+fork.author}, avatar_name(fork.author)))))
250 }, function (err) {
251 if (err) console.error(err)
252 })
253 )
254
255 return div
256 }
257
258 if(c.type === 'git-update') {
259 return [
260 h('p', 'pushed to ', repoLink(c.repo)),
261 c.refs ? h('ul', Object.keys(c.refs).map(function (ref) {
262 var rev = c.refs[ref]
263 return h('li',
264 shortRefName(ref) + ': ',
265 rev ? h('code', rev) : h('em', 'deleted'))
266 })) : null,
267 Array.isArray(c.commits) ? [
268 h('ul',
269 c.commits.map(function (commit) {
270 return h('li',
271 typeof commit.sha1 === 'string' ?
272 [h('code', commit.sha1.substr(0, 8)), ' '] : null,
273 commit.title ?
274 h('q', commit.title) : null)
275 }),
276 c.commits_more > 0 ?
277 h('li', '+ ', c.commits_more, ' more') : null)
278 ] : null,
279 Array.isArray(c.issues) ? c.issues.map(function (issue) {
280 if (issue.merged === true)
281 return h('p', 'Merged ', message_link(issue.link), ' in ',
282 h('code', issue.object), ' ', h('q', issue.label))
283 if (issue.open === false)
284 return h('p', 'Closed ', message_link(issue.link), ' in ',
285 h('code', issue.object), ' ', h('q', issue.label))
286 }) : null
287 ]
288 }
289
290 if(c.type === 'issue-edit') {
291 return h('div',
292 c.issue ? renderIssueEdit(c) : null,
293 c.issues ? c.issues.map(renderIssueEdit) : null)
294 }
295
296 if(c.type === 'issue') {
297 return h('div',
298 h('p', 'opened issue on ', repoLink(c.project)),
299 c.title ? h('h4', c.title) : '',
300 markdown(c)
301 )
302 }
303
304 if(c.type === 'pull-request') {
305 return h('div',
306 h('p', 'opened pull-request ',
307 'to ', repoLink(c.repo), ':', c.branch, ' ',
308 'from ', repoLink(c.head_repo), ':', c.head_branch),
309 c.title ? h('h4', c.title) : '',
310 markdown(c)
311 )
312 }
313}
314
315exports.message_meta = function (msg, sbot) {
316 var type = msg.value.content.type
317 if (type === 'issue' || type === 'pull-request') {
318 var el = h('em', '...')
319 // TODO: update if issue is changed
320 getIssueState(msg.key, function (err, state) {
321 if (err) return console.error(err)
322 el.textContent = state
323 })
324 return el
325 }
326}
327
328function findMessageContent(el) {
329 for(; el; el = el.parentNode) {
330 if(el.classList.contains('message')) {
331 return el.querySelector('.message_content')
332 }
333 }
334}
335
336function issueForm(msg, contentEl) {
337 return h('form',
338 h('strong', 'New Issue:'),
339 message_compose(
340 {type: 'issue', project: msg.key},
341 function (value) { return value },
342 function (err, issue) {
343 if(err) return alert(err)
344 if(!issue) return
345 var title = issue.value.content.text
346 if(title.length > 70) title = title.substr(0, 70) + '…'
347 form.appendChild(h('div',
348 h('a', {href: '#'+issue.key}, title)
349 ))
350 }
351 )
352 )
353}
354
355function branchMenu(msg, full) {
356 return combobox({
357 style: {'max-width': '14ex'},
358 placeholder: 'branch…',
359 default: 'master',
360 read: msg && pull(getRefs(msg), pull.map(function (ref) {
361 var m = /^refs\/heads\/(.*)$/.exec(ref.name)
362 if(!m) return
363 var branch = m[1]
364 var label = branch
365 if(full) {
366 var updated = new Date(ref.link.value.timestamp)
367 label = branch +
368 ' · ' + human(updated) +
369 ' · ' + ref.rev.substr(1, 8) +
370 (ref.title ? ' · "' + ref.title + '"' : '')
371 }
372 return h('option', {value: branch}, label)
373 }))
374 })
375}
376
377function pullRequestForm(msg) {
378 var headRepoInput
379 var headBranchInput = branchMenu()
380 var branchInput = branchMenu(msg)
381 var form = h('form',
382 h('strong', 'New Pull Request:'),
383 h('div',
384 'from ',
385 headRepoInput = combobox({
386 style: {'max-width': '26ex'},
387 onchange: function () {
388 // list branches for selected repo
389 var repoId = this.value
390 if(repoId) sbot_get(repoId, function (err, value) {
391 if(err) console.error(err)
392 var msg = value && {key: repoId, value: value}
393 headBranchInput = headBranchInput.swap(branchMenu(msg, true))
394 })
395 else headBranchInput = headBranchInput.swap(branchMenu())
396 },
397 read: pull(cat([
398 pull.once({id: msg.key, author: msg.value.author}),
399 getForks(msg.key)
400 ]), pull.map(function (fork) {
401 return h('option', {value: fork.id},
402 repoLink(fork.id), ' by ', avatar_name(fork.author))
403 }))
404 }),
405 ':',
406 headBranchInput,
407 ' to ',
408 repoName(msg.key),
409 ':',
410 branchInput),
411 message_compose(
412 {
413 type: 'pull-request',
414 project: msg.key,
415 repo: msg.key,
416 },
417 function (value) {
418 value.branch = branchInput.value
419 value.head_repo = headRepoInput.value
420 value.head_branch = headBranchInput.value
421 return value
422 },
423 function (err, issue) {
424 if(err) return alert(err)
425 if(!issue) return
426 var title = issue.value.content.text
427 if(title.length > 70) title = title.substr(0, 70) + '…'
428 form.appendChild(h('div',
429 h('a', {href: '#'+issue.key}, title)
430 ))
431 }
432 )
433 )
434 return form
435}
436
437exports.message_action = function (msg, sbot) {
438 var c = msg.value.content
439 if(c.type === 'issue' || c.type === 'pull-request') {
440 var isOpen
441 var a = h('a', {href: '#', onclick: function () {
442 message_confirm({
443 type: 'issue-edit',
444 root: msg.key,
445 issues: [{
446 link: msg.key,
447 open: !isOpen
448 }]
449 }, function (err, msg) {
450 if(err) return alert(err)
451 if(!msg) return
452 isOpen = msg.value.content.open
453 update()
454 })
455 }})
456 getIssueState(msg.key, function (err, state) {
457 if (err) return console.error(err)
458 isOpen = state === 'open'
459 update()
460 })
461 function update() {
462 a.textContent = c.type === 'pull-request'
463 ? isOpen ? 'Close Pull Request' : 'Reopen Pull Request'
464 : isOpen ? 'Close Issue' : 'Reopen Issue'
465 }
466 return a
467 }
468}
469
470

Built with git-ssb-web