git ssb

16+

Dominic / patchbay



Tree: b66d3b80c9607c7f76b89ae35e24a2a704d0d563

Files: b66d3b80c9607c7f76b89ae35e24a2a704d0d563 / modules_extra / git.js

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

Built with git-ssb-web