git ssb

16+

Dominic / patchbay



Tree: d7acb5062b990c4d876df857fbd8a3b29bf7d2cb

Files: d7acb5062b990c4d876df857fbd8a3b29bf7d2cb / modules_extra / git.js

13875 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 if (avatar.name[0] !== '%') avatar.name = '%' + avatar.name
83 text.nodeValue = avatar.name
84 })
85 return text
86}
87
88function repoLink(id) {
89 return h('a', {href: '#'+id}, repoText(id))
90}
91
92function repoName(id) {
93 return h('ins', repoText(id))
94}
95
96function getIssueState(id, cb) {
97 pull(
98 sbot_links({dest: id, rel: 'issues', values: true, reverse: true}),
99 pull.map(function (msg) {
100 return msg.value.content.issues
101 }),
102 pull.flatten(),
103 pull.filter(function (issue) {
104 return issue.link === id
105 }),
106 pull.map(function (issue) {
107 return issue.merged ? 'merged' : issue.open ? 'open' : 'closed'
108 }),
109 pull.take(1),
110 pull.collect(function (err, updates) {
111 cb(err, updates && updates[0] || 'open')
112 })
113 )
114}
115
116//todo:
117function messageTimestampLink(msg) {
118 var date = new Date(msg.value.timestamp)
119 return h('a.timestamp', {
120 timestamp: msg.value.timestamp,
121 title: date,
122 href: '#'+msg.key
123 }, human(date))
124}
125
126// a thead+tbody where the thead only is added when the first row is added
127function tableRows(headerRow) {
128 var thead = h('thead'), tbody = h('tbody')
129 var first = true
130 var t = [thead, tbody]
131 t.append = function (row) {
132 if (first) {
133 first = false
134 thead.appendChild(headerRow)
135 }
136 tbody.appendChild(row)
137 }
138 return t
139}
140
141function renderIssueEdit(c) {
142 var id = c.issue || c.link
143 return [
144 c.title ? h('p', 'renamed issue ', message_link(id),
145 ' to ', h('ins', c.title)) : null,
146 c.open === false ? h('p', 'closed issue ', message_link(id)) : null,
147 c.open === true ? h('p', 'reopened issue ', message_link(id)) : null]
148}
149
150exports.message_content = function (msg, sbot) {
151 var c = msg.value.content
152
153 if(c.type === 'git-repo') {
154 var branchesT, tagsT, openIssuesT, closedIssuesT, openPRsT, closedPRsT
155 var forksT
156 var div = h('div',
157 h('p', 'git repo ', repoName(msg.key)),
158 c.upstream ? h('p', 'fork of ', repoLink(c.upstream)) : '',
159 h('p', h('code', 'ssb://' + msg.key)),
160 h('div.git-table-wrapper', {style: {'max-height': '12em'}},
161 h('table',
162 branchesT = tableRows(h('tr',
163 h('th', 'branch'),
164 h('th', 'commit'),
165 h('th', 'last update'))),
166 tagsT = tableRows(h('tr',
167 h('th', 'tag'),
168 h('th', 'commit'),
169 h('th', 'last update'))))),
170 h('div.git-table-wrapper', {style: {'max-height': '16em'}},
171 h('table',
172 openIssuesT = tableRows(h('tr',
173 h('th', 'open issues'))),
174 closedIssuesT = tableRows(h('tr',
175 h('th', 'closed issues'))))),
176 h('div.git-table-wrapper', {style: {'max-height': '16em'}},
177 h('table',
178 openPRsT = tableRows(h('tr',
179 h('th', 'open pull requests'))),
180 closedPRsT = tableRows(h('tr',
181 h('th', 'closed pull requests'))))),
182 h('div.git-table-wrapper',
183 h('table',
184 forksT = tableRows(h('tr',
185 h('th', 'forks'))))),
186 h('div', h('a', {href: '#', onclick: function (e) {
187 e.preventDefault()
188 this.parentNode.replaceChild(issueForm(msg), this)
189 }}, 'New Issue…')),
190 newPullRequestButton.call(this, msg)
191 )
192
193 pull(getRefs(msg), pull.drain(function (ref) {
194 var name = ref.realname || ref.name
195 var author = ref.link && ref.link.value.author
196 var parts = /^refs\/(heads|tags)\/(.*)$/.exec(name) || []
197 var shortName = parts[2]
198 var t
199 if(parts[1] === 'heads') t = branchesT
200 else if(parts[1] === 'tags') t = tagsT
201 if(t) t.append(h('tr',
202 h('td', shortName,
203 ref.conflict ? [
204 h('br'),
205 h('a', {href: '#'+author}, avatar_name(author))
206 ] : ''),
207 h('td', h('code', ref.hash)),
208 h('td', messageTimestampLink(ref.link))))
209 }, function (err) {
210 if(err) console.error(err)
211 }))
212
213 // list issues and pull requests
214 pull(
215 sbot_links({
216 reverse: true,
217 dest: msg.key,
218 rel: 'project',
219 values: true
220 }),
221 paramap(function (link, cb) {
222 getIssueState(link.key, function (err, state) {
223 if(err) return cb(err)
224 link.state = state
225 cb(null, link)
226 })
227 }),
228 pull.drain(function (link) {
229 var c = link.value.content
230 var title = c.title || (c.text ? c.text.length > 70
231 ? c.text.substr(0, 70) + '…'
232 : c.text : link.key)
233 var author = link.value.author
234 var t = c.type === 'pull-request'
235 ? link.state === 'open' ? openPRsT : closedPRsT
236 : link.state === 'open' ? openIssuesT : closedIssuesT
237 t.append(h('tr',
238 h('td',
239 h('a', {href: '#'+link.key}, title), h('br'),
240 h('small',
241 'opened ', messageTimestampLink(link),
242 ' by ', h('a', {href: '#'+author}, avatar_name(author))))))
243 }, function (err) {
244 if (err) console.error(err)
245 })
246 )
247
248 // list forks
249 pull(
250 getForks(msg.key),
251 pull.drain(function (fork) {
252 forksT.append(h('tr', h('td',
253 repoLink(fork.id),
254 ' by ', h('a', {href: '#'+fork.author}, avatar_name(fork.author)))))
255 }, function (err) {
256 if (err) console.error(err)
257 })
258 )
259
260 return div
261 }
262
263 if(c.type === 'git-update') {
264 return [
265 h('p', 'pushed to ', repoLink(c.repo)),
266 c.refs ? h('ul', Object.keys(c.refs).map(function (ref) {
267 var rev = c.refs[ref]
268 return h('li',
269 shortRefName(ref) + ': ',
270 rev ? h('code', rev) : h('em', 'deleted'))
271 })) : null,
272 Array.isArray(c.commits) ? [
273 h('ul',
274 c.commits.map(function (commit) {
275 return h('li',
276 typeof commit.sha1 === 'string' ?
277 [h('code', commit.sha1.substr(0, 8)), ' '] : null,
278 commit.title ?
279 h('q', commit.title) : null)
280 }),
281 c.commits_more > 0 ?
282 h('li', '+ ', c.commits_more, ' more') : null)
283 ] : null,
284 Array.isArray(c.issues) ? c.issues.map(function (issue) {
285 if (issue.merged === true)
286 return h('p', 'Merged ', message_link(issue.link), ' in ',
287 h('code', issue.object), ' ', h('q', issue.label))
288 if (issue.open === false)
289 return h('p', 'Closed ', message_link(issue.link), ' in ',
290 h('code', issue.object), ' ', h('q', issue.label))
291 }) : null,
292 newPullRequestButton.call(this, msg)
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 newPullRequestButton(msg) {
386 return h('div', [
387 h('a', {
388 href: '#',
389 onclick: function (e) {
390 e.preventDefault()
391 this.parentNode.replaceChild(pullRequestForm(msg), this)
392 }},
393 'New Pull Request…'
394 )
395 ])
396}
397
398function pullRequestForm(msg) {
399 var headRepoInput
400 var headBranchInput = branchMenu()
401 var branchInput = branchMenu(msg)
402 var form = h('form',
403 h('strong', 'New Pull Request:'),
404 h('div',
405 'from ',
406 headRepoInput = combobox({
407 style: {'max-width': '26ex'},
408 onchange: function () {
409 // list branches for selected repo
410 var repoId = this.value
411 if(repoId) sbot_get(repoId, function (err, value) {
412 if(err) console.error(err)
413 var msg = value && {key: repoId, value: value}
414 headBranchInput = headBranchInput.swap(branchMenu(msg, true))
415 })
416 else headBranchInput = headBranchInput.swap(branchMenu())
417 },
418 read: pull(cat([
419 pull.once({id: msg.key, author: msg.value.author}),
420 getForks(msg.key)
421 ]), pull.map(function (fork) {
422 return h('option', {value: fork.id},
423 repoLink(fork.id), ' by ', avatar_name(fork.author))
424 }))
425 }),
426 ':',
427 headBranchInput,
428 ' to ',
429 repoName(msg.key),
430 ':',
431 branchInput),
432 message_compose(
433 {
434 type: 'pull-request',
435 project: msg.key,
436 repo: msg.key,
437 },
438 function (value) {
439 value.branch = branchInput.value
440 value.head_repo = headRepoInput.value
441 value.head_branch = headBranchInput.value
442 return value
443 },
444 function (err, issue) {
445 if(err) return alert(err)
446 if(!issue) return
447 var title = issue.value.content.text
448 if(title.length > 70) title = title.substr(0, 70) + '…'
449 form.appendChild(h('div',
450 h('a', {href: '#'+issue.key}, title)
451 ))
452 }
453 )
454 )
455 return form
456}
457
458exports.message_action = function (msg, sbot) {
459 var c = msg.value.content
460 if(c.type === 'issue' || c.type === 'pull-request') {
461 var isOpen
462 var a = h('a', {href: '#', onclick: function (e) {
463 e.preventDefault()
464 message_confirm({
465 type: 'issue-edit',
466 root: msg.key,
467 issues: [{
468 link: msg.key,
469 open: !isOpen
470 }]
471 }, function (err, msg) {
472 if(err) return alert(err)
473 if(!msg) return
474 isOpen = msg.value.content.open
475 update()
476 })
477 }})
478 getIssueState(msg.key, function (err, state) {
479 if (err) return console.error(err)
480 isOpen = state === 'open'
481 update()
482 })
483 function update() {
484 a.textContent = c.type === 'pull-request'
485 ? isOpen ? 'Close Pull Request' : 'Reopen Pull Request'
486 : isOpen ? 'Close Issue' : 'Reopen Issue'
487 }
488 return a
489 }
490}
491
492

Built with git-ssb-web