git ssb

16+

Dominic / patchbay



Tree: 751c7231c9c941924eb9b30d3e883cce71645f67

Files: 751c7231c9c941924eb9b30d3e883cce71645f67 / modules / git.js

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

Built with git-ssb-web