git ssb

16+

Dominic / patchbay



Tree: 287cfa70a645df435a34f56c682ad34f826eba00

Files: 287cfa70a645df435a34f56c682ad34f826eba00 / modules / git.js

13599 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 return h('div',
298 c.issue ? renderIssueEdit(c) : null,
299 c.issues ? c.issues.map(renderIssueEdit) : null)
300 }
301
302 if(c.type === 'issue') {
303 return h('div',
304 h('p', 'opened issue on ', repoLink(c.project)),
305 c.title ? h('h4', c.title) : '',
306 markdown(c)
307 )
308 }
309
310 if(c.type === 'pull-request') {
311 return h('div',
312 h('p', 'opened pull-request ',
313 'to ', repoLink(c.repo), ':', c.branch, ' ',
314 'from ', repoLink(c.head_repo), ':', c.head_branch),
315 c.title ? h('h4', c.title) : '',
316 markdown(c)
317 )
318 }
319}
320
321exports.message_meta = function (msg, sbot) {
322 var type = msg.value.content.type
323 if (type === 'issue' || type === 'pull-request') {
324 var el = h('em', '...')
325 // TODO: update if issue is changed
326 getIssueState(msg.key, function (err, state) {
327 if (err) return console.error(err)
328 el.textContent = state
329 })
330 return el
331 }
332}
333
334function findMessageContent(el) {
335 for(; el; el = el.parentNode) {
336 if(el.classList.contains('message')) {
337 return el.querySelector('.message_content')
338 }
339 }
340}
341
342function issueForm(msg, contentEl) {
343 var form = h('form',
344 h('strong', 'New Issue:'),
345 message_compose(
346 {type: 'issue', project: msg.key},
347 function (value) { return value },
348 function (err, issue) {
349 if(err) return alert(err)
350 if(!issue) return
351 var title = issue.value.content.text
352 if(title.length > 70) title = title.substr(0, 70) + '…'
353 form.appendChild(h('div',
354 h('a', {href: '#'+issue.key}, title)
355 ))
356 }
357 )
358 )
359 return form
360}
361
362function branchMenu(msg, full) {
363 return combobox({
364 style: {'max-width': '14ex'},
365 placeholder: 'branch…',
366 default: 'master',
367 read: msg && pull(getRefs(msg), pull.map(function (ref) {
368 var m = /^refs\/heads\/(.*)$/.exec(ref.name)
369 if(!m) return
370 var branch = m[1]
371 var label = branch
372 if(full) {
373 var updated = new Date(ref.link.value.timestamp)
374 label = branch +
375 ' · ' + human(updated) +
376 ' · ' + ref.hash.substr(1, 8) +
377 (ref.title ? ' · "' + ref.title + '"' : '')
378 }
379 return h('option', {value: branch}, label)
380 }))
381 })
382}
383
384function pullRequestForm(msg) {
385 var headRepoInput
386 var headBranchInput = branchMenu()
387 var branchInput = branchMenu(msg)
388 var form = h('form',
389 h('strong', 'New Pull Request:'),
390 h('div',
391 'from ',
392 headRepoInput = combobox({
393 style: {'max-width': '26ex'},
394 onchange: function () {
395 // list branches for selected repo
396 var repoId = this.value
397 if(repoId) sbot_get(repoId, function (err, value) {
398 if(err) console.error(err)
399 var msg = value && {key: repoId, value: value}
400 headBranchInput = headBranchInput.swap(branchMenu(msg, true))
401 })
402 else headBranchInput = headBranchInput.swap(branchMenu())
403 },
404 read: pull(cat([
405 pull.once({id: msg.key, author: msg.value.author}),
406 getForks(msg.key)
407 ]), pull.map(function (fork) {
408 return h('option', {value: fork.id},
409 repoLink(fork.id), ' by ', avatar_name(fork.author))
410 }))
411 }),
412 ':',
413 headBranchInput,
414 ' to ',
415 repoName(msg.key),
416 ':',
417 branchInput),
418 message_compose(
419 {
420 type: 'pull-request',
421 project: msg.key,
422 repo: msg.key,
423 },
424 function (value) {
425 value.branch = branchInput.value
426 value.head_repo = headRepoInput.value
427 value.head_branch = headBranchInput.value
428 return value
429 },
430 function (err, issue) {
431 if(err) return alert(err)
432 if(!issue) return
433 var title = issue.value.content.text
434 if(title.length > 70) title = title.substr(0, 70) + '…'
435 form.appendChild(h('div',
436 h('a', {href: '#'+issue.key}, title)
437 ))
438 }
439 )
440 )
441 return form
442}
443
444exports.message_action = function (msg, sbot) {
445 var c = msg.value.content
446 if(c.type === 'issue' || c.type === 'pull-request') {
447 var isOpen
448 var a = h('a', {href: '#', onclick: function (e) {
449 e.preventDefault()
450 message_confirm({
451 type: 'issue-edit',
452 root: msg.key,
453 issues: [{
454 link: msg.key,
455 open: !isOpen
456 }]
457 }, function (err, msg) {
458 if(err) return alert(err)
459 if(!msg) return
460 isOpen = msg.value.content.open
461 update()
462 })
463 }})
464 getIssueState(msg.key, function (err, state) {
465 if (err) return console.error(err)
466 isOpen = state === 'open'
467 update()
468 })
469 function update() {
470 a.textContent = c.type === 'pull-request'
471 ? isOpen ? 'Close Pull Request' : 'Reopen Pull Request'
472 : isOpen ? 'Close Issue' : 'Reopen Issue'
473 }
474 return a
475 }
476}
477
478

Built with git-ssb-web