@ok-ggwp/git-gloss 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -3
- package/dist/ary/browse.js +80 -0
- package/dist/ary/client.js +401 -0
- package/dist/ary/index.html +13 -0
- package/dist/ary/server.js +101 -0
- package/dist/ary/style.css +115 -0
- package/dist/attention.js +34 -0
- package/dist/carry.js +4 -1
- package/dist/cli.js +3 -1
- package/dist/commands/add.js +17 -1
- package/dist/commands/ary.js +35 -0
- package/dist/commands/html.js +1 -1
- package/dist/commands/hunks.js +8 -1
- package/dist/commands/init.js +6 -1
- package/dist/commands/show.js +44 -16
- package/dist/commands/signals.js +88 -0
- package/dist/config.js +192 -0
- package/dist/floor.js +148 -0
- package/dist/git.js +13 -2
- package/dist/glob.js +76 -0
- package/dist/html/client.js +1016 -296
- package/dist/html/data.js +26 -1
- package/dist/html/page.js +1 -1
- package/dist/html/style.css +284 -164
- package/dist/loader.js +1 -1
- package/dist/merge.js +5 -0
- package/dist/render.js +51 -3
- package/dist/safe-match.js +28 -0
- package/dist/schema.js +46 -2
- package/dist/signals.js +292 -0
- package/dist/validate.js +6 -0
- package/package.json +1 -1
- package/skills/gloss/SKILL.md +31 -1
package/dist/html/client.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
// Renders the gloss page from the embedded JSON
|
|
2
|
-
//
|
|
1
|
+
// Renders the gloss page (the "review desk") from the embedded JSON: a rail of
|
|
2
|
+
// groups, or of commits for a range, and a detail pane for the selection.
|
|
3
|
+
// All text goes through textContent; nothing from the data is ever parsed as
|
|
4
|
+
// HTML. Colours reach elements as custom properties set through the CSSOM,
|
|
5
|
+
// which the page's CSP allows (it blocks style attributes).
|
|
3
6
|
(function () {
|
|
4
7
|
'use strict'
|
|
5
8
|
var data = JSON.parse(document.getElementById('gloss-data').textContent)
|
|
6
9
|
var app = document.getElementById('app')
|
|
7
|
-
var
|
|
8
|
-
var multi =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
var commits = data.commits
|
|
11
|
+
var multi = commits.length > 1 || data.range !== null
|
|
12
|
+
var IMPACTS = data.impact.map(function (i) { return i.name })
|
|
13
|
+
var LEVELS = ['skim', 'look', 'careful', 'must-read']
|
|
14
|
+
var FILTERS = [['all', 'All'], ['focus', 'Look first'], ['contract', 'Contract and above'], ['low', 'Local only']]
|
|
12
15
|
|
|
16
|
+
// What is shown: a commit (index) and in it a group, the overview or the
|
|
17
|
+
// unannotated changes. Plus the filters and whether diffs are open.
|
|
18
|
+
var booting = true // no scrolling while the first selection is made
|
|
19
|
+
var state = { ci: 0, kind: 'overview', gid: null, filter: 'all', hidden: new Set(), hot: true, stage: null, back: null }
|
|
20
|
+
|
|
21
|
+
// ---- helpers ----
|
|
13
22
|
function el(tag, cls, text) {
|
|
14
23
|
var e = document.createElement(tag)
|
|
15
24
|
if (cls) e.className = cls
|
|
@@ -20,38 +29,150 @@
|
|
|
20
29
|
for (var i = 1; i < arguments.length; i++) if (arguments[i]) parent.appendChild(arguments[i])
|
|
21
30
|
return parent
|
|
22
31
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
function text(s) { return document.createTextNode(s) }
|
|
33
|
+
function button(cls, label, onclick) {
|
|
34
|
+
var b = el('button', cls, label)
|
|
35
|
+
b.type = 'button'
|
|
36
|
+
if (onclick) b.addEventListener('click', onclick)
|
|
37
|
+
return b
|
|
26
38
|
}
|
|
27
39
|
function plural(n, word) { return n + ' ' + word + (n === 1 ? '' : 's') }
|
|
40
|
+
function sha7(c) { return c.sha.slice(0, 7) }
|
|
41
|
+
// Group ids are unique within a commit, not across a range, so a range keys
|
|
42
|
+
// groups by commit too: #g-<sha7>-<group>.
|
|
43
|
+
function key(c, id) { return multi ? sha7(c) + '-' + id : id }
|
|
28
44
|
function stats(added, removed) {
|
|
29
|
-
return add(el('span', 'stats'), el('span', 'plus', '+' + added),
|
|
45
|
+
return add(el('span', 'stats'), el('span', 'plus', '+' + added), text(' '), el('span', 'minus', '−' + removed))
|
|
46
|
+
}
|
|
47
|
+
function impactRank(level) { return IMPACTS.indexOf(level) + 1 }
|
|
48
|
+
function levelRank(level) { return LEVELS.indexOf(level) + 1 }
|
|
49
|
+
// Declared impact, else the one derived from the diff (only present where the
|
|
50
|
+
// diff puts a group above the baseline), else none.
|
|
51
|
+
function effImpact(g) { return g.impact || g.impact_derived || null }
|
|
52
|
+
function find(list, fn) { for (var i = 0; i < list.length; i++) if (fn(list[i])) return list[i]; return null }
|
|
53
|
+
function groupOf(c, id) { return find(c.groups, function (g) { return g.id === id }) }
|
|
54
|
+
function orderedGroups(c) { return c.order.map(function (id) { return groupOf(c, id) }).filter(Boolean) }
|
|
55
|
+
function isFocus(c, g) { return !!(c.attention && c.attention.focus && c.attention.focus.indexOf(g.id) >= 0) }
|
|
56
|
+
|
|
57
|
+
// The level of a commit as the reader should treat it: declared, else derived.
|
|
58
|
+
function levelOf(c) {
|
|
59
|
+
if (!c.glossed) return { level: null, source: 'none' }
|
|
60
|
+
if (c.attention) return { level: c.attention.level, source: 'declared' }
|
|
61
|
+
if (c.computed) return { level: c.computed.derived.level, source: 'derived' }
|
|
62
|
+
return { level: null, source: 'unrated' }
|
|
63
|
+
}
|
|
64
|
+
function fileCount(c) {
|
|
65
|
+
var seen = {}
|
|
66
|
+
c.groups.forEach(function (g) { g.hunks.forEach(function (h) { seen[h.file] = true }) })
|
|
67
|
+
c.unannotated.forEach(function (h) { seen[h.file] = true })
|
|
68
|
+
return Object.keys(seen).length
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- read state, kept per commit in sessionStorage (this tab only) when it is available ----
|
|
72
|
+
var readCache = {}
|
|
73
|
+
function readSet(c) {
|
|
74
|
+
if (!readCache[c.sha]) {
|
|
75
|
+
var s = {}
|
|
76
|
+
try {
|
|
77
|
+
var raw = window.sessionStorage.getItem('git-gloss:read:' + c.sha)
|
|
78
|
+
if (raw) JSON.parse(raw).forEach(function (id) { s[id] = true })
|
|
79
|
+
} catch (e) { /* storage blocked or unreadable: marks are kept in memory for this visit only */ }
|
|
80
|
+
readCache[c.sha] = s
|
|
81
|
+
}
|
|
82
|
+
return readCache[c.sha]
|
|
83
|
+
}
|
|
84
|
+
function setRead(c, id, on) {
|
|
85
|
+
var s = readSet(c)
|
|
86
|
+
if (on) s[id] = true; else delete s[id]
|
|
87
|
+
try { window.sessionStorage.setItem('git-gloss:read:' + c.sha, JSON.stringify(Object.keys(s))) } catch (e) { /* not persisted */ }
|
|
88
|
+
}
|
|
89
|
+
function readCounts() {
|
|
90
|
+
var read = 0, total = 0
|
|
91
|
+
commits.forEach(function (c) {
|
|
92
|
+
var s = readSet(c)
|
|
93
|
+
c.groups.forEach(function (g) { total++; if (s[g.id]) read++ })
|
|
94
|
+
})
|
|
95
|
+
return { read: read, total: total }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- small widgets ----
|
|
99
|
+
function meter(level) {
|
|
100
|
+
var m = el('span', level ? 'meter' : 'meter unrated')
|
|
101
|
+
m.setAttribute('aria-hidden', 'true')
|
|
102
|
+
var n = impactRank(level)
|
|
103
|
+
for (var i = 1; i <= 5; i++) m.appendChild(el('i', i <= n ? 'on' : null))
|
|
104
|
+
if (level) m.style.setProperty('--m', 'var(--impact-' + level + ')')
|
|
105
|
+
return m
|
|
106
|
+
}
|
|
107
|
+
function gauge(level, small) {
|
|
108
|
+
var g = el('span', small ? 'gauge small' : 'gauge')
|
|
109
|
+
g.setAttribute('aria-hidden', 'true')
|
|
110
|
+
for (var i = 1; i <= 4; i++) g.appendChild(el('i', i <= levelRank(level) ? 'on' : null))
|
|
111
|
+
return g
|
|
112
|
+
}
|
|
113
|
+
function impactWord(g) {
|
|
114
|
+
var level = effImpact(g)
|
|
115
|
+
return level ? level + ' impact' + (g.impact ? '' : ' (derived)') : 'impact not rated'
|
|
30
116
|
}
|
|
31
117
|
function groupLink(c, id) {
|
|
32
118
|
var a = el('a', null, id)
|
|
33
119
|
a.href = '#g-' + key(c, id)
|
|
34
|
-
a.addEventListener('click', function (ev) { ev.preventDefault();
|
|
120
|
+
a.addEventListener('click', function (ev) { ev.preventDefault(); selectGroup(commits.indexOf(c), id) })
|
|
35
121
|
return a
|
|
36
122
|
}
|
|
123
|
+
function linkList(c, ids) {
|
|
124
|
+
var span = el('span')
|
|
125
|
+
ids.forEach(function (id, i) { if (i) span.appendChild(text(', ')); span.appendChild(groupLink(c, id)) })
|
|
126
|
+
return span
|
|
127
|
+
}
|
|
128
|
+
function commitMeta(c) {
|
|
129
|
+
return add(el('div', 'commit'), el('span', 'sha', c.sha.slice(0, 10)),
|
|
130
|
+
el('span', null, c.author), el('span', null, new Date(c.date).toLocaleString()))
|
|
131
|
+
}
|
|
132
|
+
function statement(parent, label, body) {
|
|
133
|
+
if (body) add(parent, add(el('p', 'statement'), el('span', 'label', label), text(body)))
|
|
134
|
+
}
|
|
135
|
+
function warnings(c, parent) {
|
|
136
|
+
c.warnings.forEach(function (w) { add(parent, el('div', 'warning', '⚠ ' + w)) })
|
|
137
|
+
}
|
|
37
138
|
|
|
38
139
|
// ---- diff rendering ----
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
140
|
+
// Each hunk folds on its own; the header row stays. Open/closed lives in
|
|
141
|
+
// memory for the page's lifetime (not in storage), keyed by commit, scope
|
|
142
|
+
// (group id or 'un') and the hunk's reference. The DOM is the source of truth
|
|
143
|
+
// for what is open: syncFold() reads it to set the Expand/Collapse buttons.
|
|
144
|
+
var hunkClosed = {}
|
|
145
|
+
function setHunkOpen(wrap, open) {
|
|
146
|
+
var toggle = wrap.querySelector('.htoggle')
|
|
147
|
+
if (!toggle) return
|
|
148
|
+
if (open) delete hunkClosed[wrap.getAttribute('data-fold')]; else hunkClosed[wrap.getAttribute('data-fold')] = true
|
|
149
|
+
wrap.querySelector('.scroll').hidden = !open
|
|
150
|
+
toggle.setAttribute('aria-expanded', String(open))
|
|
151
|
+
toggle.firstChild.textContent = open ? '▾' : '▸'
|
|
152
|
+
wrap.classList.toggle('closed', !open)
|
|
153
|
+
}
|
|
154
|
+
function hunkView(c, h, scope) {
|
|
155
|
+
var name = h.oldFile ? h.oldFile + ' → ' + h.file : h.file
|
|
156
|
+
var head = el('div', 'hunk-head')
|
|
157
|
+
var wrap = el('div', 'hunk')
|
|
158
|
+
var foldKey = c.sha + '|' + scope + '|' + h.ref
|
|
159
|
+
wrap.setAttribute('data-fold', foldKey)
|
|
160
|
+
var toggle = null
|
|
161
|
+
if (h.status === 'missing') add(head, el('span', 'file', name))
|
|
162
|
+
else {
|
|
163
|
+
var chev = el('span', 'chev', '▾')
|
|
164
|
+
chev.setAttribute('aria-hidden', 'true')
|
|
165
|
+
toggle = add(button('htoggle', null), chev, el('span', 'file', name))
|
|
166
|
+
toggle.setAttribute('data-fid', 'hunk:' + foldKey)
|
|
167
|
+
toggle.addEventListener('click', function () { setHunkOpen(wrap, toggle.getAttribute('aria-expanded') !== 'true'); syncFold() })
|
|
168
|
+
}
|
|
169
|
+
add(head, toggle, el('span', 'range', h.ref), el('span', 'range', h.range), stats(h.added, h.removed))
|
|
45
170
|
if (h.status === 'changed') add(head, el('span', 'stale', 'changed since glossed'))
|
|
46
171
|
if (h.status === 'missing') {
|
|
47
172
|
add(head, el('span', 'stale', 'no longer in this commit'))
|
|
48
|
-
return add(
|
|
49
|
-
}
|
|
50
|
-
if (h.alsoIn.length) {
|
|
51
|
-
var also = add(el('span', 'also'), document.createTextNode('shares lines with '))
|
|
52
|
-
h.alsoIn.forEach(function (id, i) { if (i) also.appendChild(document.createTextNode(', ')); also.appendChild(groupLink(c, id)) })
|
|
53
|
-
add(head, also)
|
|
173
|
+
return add(wrap, head)
|
|
54
174
|
}
|
|
175
|
+
if (h.alsoIn.length) add(head, add(el('span', 'also'), text('shares lines with '), linkList(c, h.alsoIn)))
|
|
55
176
|
var table = el('table', 'diff')
|
|
56
177
|
h.lines.forEach(function (l) {
|
|
57
178
|
var tr = el('tr', l.kind)
|
|
@@ -68,316 +189,915 @@
|
|
|
68
189
|
}
|
|
69
190
|
table.appendChild(tr)
|
|
70
191
|
})
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
var
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
var facts = el('dl', 'facts')
|
|
90
|
-
function fact(label, value) {
|
|
91
|
-
if (!value) return
|
|
92
|
-
add(facts, el('dt', null, label), typeof value === 'string' ? el('dd', null, value) : add(el('dd'), value))
|
|
93
|
-
}
|
|
94
|
-
if (g.serves[0] !== 'task') {
|
|
95
|
-
var links = el('span')
|
|
96
|
-
g.serves.forEach(function (id, i) { if (i) links.appendChild(document.createTextNode(', ')); links.appendChild(groupLink(c, id)) })
|
|
97
|
-
fact('serves', links)
|
|
98
|
-
}
|
|
99
|
-
fact('if removed', g.removal_impact)
|
|
100
|
-
fact('evidence', g.evidence)
|
|
101
|
-
fact('notes', g.notes)
|
|
102
|
-
|
|
103
|
-
add(card, add(el('div', 'group-head'), title), facts)
|
|
104
|
-
|
|
105
|
-
if (g.hunks.length) {
|
|
106
|
-
var code = el('div', 'code')
|
|
107
|
-
code.hidden = true
|
|
108
|
-
var toggle = el('button', 'code-toggle')
|
|
109
|
-
toggle.type = 'button'
|
|
110
|
-
var rendered = false
|
|
111
|
-
function setOpen(open) {
|
|
112
|
-
if (open && !rendered) { g.hunks.forEach(function (h) { code.appendChild(hunkView(c, h)) }); rendered = true }
|
|
113
|
-
code.hidden = !open
|
|
114
|
-
toggle.textContent = (open ? '▾ Hide code' : '▸ Show code') + ' · ' + plural(g.hunks.length, 'hunk')
|
|
115
|
-
toggle.setAttribute('aria-expanded', String(open))
|
|
116
|
-
}
|
|
117
|
-
toggle.addEventListener('click', function () { setOpen(code.hidden) })
|
|
118
|
-
setOpen(false)
|
|
119
|
-
card.setOpen = setOpen
|
|
120
|
-
card.isOpen = function () { return !code.hidden }
|
|
121
|
-
add(card, toggle, code)
|
|
192
|
+
add(wrap, head, add(el('div', 'scroll'), table))
|
|
193
|
+
setHunkOpen(wrap, !hunkClosed[foldKey])
|
|
194
|
+
return wrap
|
|
195
|
+
}
|
|
196
|
+
// "Expand all hunks" / "Collapse all hunks" act on the hunks of the pane; the
|
|
197
|
+
// one that is already true is disabled (focus moves to the other one).
|
|
198
|
+
function syncFold() {
|
|
199
|
+
var expand = mainEl.querySelector('[data-fid="code:expand"]')
|
|
200
|
+
var collapse = mainEl.querySelector('[data-fid="code:collapse"]')
|
|
201
|
+
if (!expand) return
|
|
202
|
+
var toggles = mainEl.querySelectorAll('.htoggle')
|
|
203
|
+
var open = mainEl.querySelectorAll('.htoggle[aria-expanded="true"]').length
|
|
204
|
+
var a = document.activeElement
|
|
205
|
+
expand.disabled = open === toggles.length
|
|
206
|
+
collapse.disabled = open === 0
|
|
207
|
+
if (a && a.disabled && (a === expand || a === collapse)) {
|
|
208
|
+
var other = a === expand ? collapse : expand
|
|
209
|
+
if (!other.disabled) other.focus({ preventScroll: true })
|
|
122
210
|
}
|
|
123
|
-
|
|
124
|
-
|
|
211
|
+
}
|
|
212
|
+
function setCode(open) {
|
|
213
|
+
var wraps = mainEl.querySelectorAll('.hunk')
|
|
214
|
+
for (var i = 0; i < wraps.length; i++) setHunkOpen(wraps[i], open)
|
|
215
|
+
syncFold()
|
|
125
216
|
}
|
|
126
217
|
|
|
127
|
-
// ----
|
|
128
|
-
|
|
129
|
-
|
|
218
|
+
// ---- attention ----
|
|
219
|
+
// The declared attention (or the one derived from the diff, marked as such),
|
|
220
|
+
// the computed floor, the reasons and links to the groups to read first;
|
|
221
|
+
// "unrated" when nothing is declared and no signals exist.
|
|
222
|
+
function attentionCard(c) {
|
|
223
|
+
var a = c.attention, k = c.computed
|
|
224
|
+
var level = levelOf(c).level
|
|
225
|
+
var box = el('section', 'att')
|
|
226
|
+
box.setAttribute('aria-label', 'Attention')
|
|
227
|
+
if (level) box.style.setProperty('--lv', 'var(--attention-' + level + ')')
|
|
228
|
+
var lvl = add(el('div', 'lvl'), el('span', null, level ? level.toUpperCase() : 'UNRATED'), level ? gauge(level) : null)
|
|
229
|
+
if (a) add(lvl, el('span', 'src', 'declared'))
|
|
230
|
+
else if (k) {
|
|
231
|
+
var d = el('span', 'src', 'derived')
|
|
232
|
+
d.title = 'Derived from the diff, not declared in the gloss'
|
|
233
|
+
add(lvl, d)
|
|
234
|
+
}
|
|
235
|
+
if (k) add(lvl, el('span', 'src', 'floor ' + k.floor))
|
|
236
|
+
add(box, lvl)
|
|
237
|
+
var reasons = a ? a.reasons : k ? k.derived.reasons : []
|
|
238
|
+
if (reasons && reasons.length) {
|
|
239
|
+
var ul = el('ul')
|
|
240
|
+
reasons.forEach(function (r) { ul.appendChild(el('li', null, r)) })
|
|
241
|
+
add(box, ul)
|
|
242
|
+
}
|
|
243
|
+
if (c.risks.length) {
|
|
244
|
+
var rb = button('linkbtn', plural(c.risks.length, 'risk') + ' flagged', function () { selectRisks(commits.indexOf(c)) })
|
|
245
|
+
add(box, add(el('p', 'look'), rb))
|
|
246
|
+
}
|
|
247
|
+
if (a && k && k.below) add(box, el('p', 'override mismatch', 'Declared ' + a.level + ', below the computed floor ' + k.floor + (a.override_reason ? '. Reason given: ' + a.override_reason : ' (no reason given).')))
|
|
248
|
+
else if (a && a.override_reason) add(box, el('p', 'override', 'Rated below what the change suggests: ' + a.override_reason))
|
|
249
|
+
return box
|
|
130
250
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
251
|
+
|
|
252
|
+
// ---- reviews: whether each stage reviewed the commit as it is now ----
|
|
253
|
+
function reviewClass(r) {
|
|
254
|
+
return r.state !== 'seen' || r.verdict === 'issues' ? 'stale' : r.verdict === 'clean' ? 'review-ok' : 'review-none'
|
|
255
|
+
}
|
|
256
|
+
// A stage's verdict and how current it is, short enough for a rail row.
|
|
257
|
+
function reviewShort(r) {
|
|
258
|
+
var v = r.verdict === 'issues' ? 'issues found' : r.verdict === 'clean' ? 'no issues' : 'reviewed'
|
|
259
|
+
var f = r.state === 'seen' ? 'current' : r.state === 'code-changed' ? 'code changed since review' : 'gloss changed since review'
|
|
260
|
+
return v + ' · ' + f
|
|
261
|
+
}
|
|
262
|
+
// One compact row per stage; each opens the Reviews view in the main pane.
|
|
263
|
+
function reviewStatus(c) {
|
|
264
|
+
var ci = commits.indexOf(c)
|
|
265
|
+
var box = el('div', 'reviews')
|
|
266
|
+
if (!c.reviews.length) return add(box, el('span', 'review-none', 'No reviews recorded'))
|
|
267
|
+
c.reviews.forEach(function (r) {
|
|
268
|
+
var cls = reviewClass(r)
|
|
269
|
+
var b = button('rvrow', null, function () { selectReviews(ci, r.stage) })
|
|
270
|
+
b.setAttribute('data-fid', 'rv:' + ci + ':' + r.stage)
|
|
271
|
+
b.title = r.phrase
|
|
272
|
+
add(b, el('span', 'rvdot ' + (cls === 'review-ok' ? 'ok' : cls === 'stale' ? 'warn' : 'none')), el('span', 'rvstage', r.stage), el('span', 'rvtxt', reviewShort(r)),
|
|
273
|
+
el('span', 'rvn', r.entries.length))
|
|
274
|
+
box.appendChild(b)
|
|
275
|
+
})
|
|
276
|
+
return box
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---- markdown: a small subset, built as DOM nodes (never parsed as HTML) ----
|
|
280
|
+
var ALNUM = /[A-Za-z0-9]/
|
|
281
|
+
function inline(parent, s) {
|
|
282
|
+
var i = 0, buf = ''
|
|
283
|
+
function flush() { if (buf) { parent.appendChild(text(buf)); buf = '' } }
|
|
284
|
+
while (i < s.length) {
|
|
285
|
+
var ch = s.charAt(i), j, m
|
|
286
|
+
if (ch === '`') {
|
|
287
|
+
j = s.indexOf('`', i + 1)
|
|
288
|
+
if (j > i + 1) { flush(); parent.appendChild(el('code', null, s.slice(i + 1, j))); i = j + 1; continue }
|
|
289
|
+
} else if (ch === '*' && s.charAt(i + 1) === '*') {
|
|
290
|
+
j = s.indexOf('**', i + 2)
|
|
291
|
+
if (j > i + 2) { flush(); parent.appendChild(inline(el('strong'), s.slice(i + 2, j))); i = j + 2; continue }
|
|
292
|
+
} else if (ch === '*' && s.charAt(i + 1) && !/[\s*]/.test(s.charAt(i + 1)) && (i === 0 || !ALNUM.test(s.charAt(i - 1)))) {
|
|
293
|
+
for (j = i + 2; j < s.length; j++) {
|
|
294
|
+
if (s.charAt(j) === '*' && !/\s/.test(s.charAt(j - 1)) && !ALNUM.test(s.charAt(j + 1)) && s.charAt(j + 1) !== '*') break
|
|
295
|
+
}
|
|
296
|
+
if (j < s.length) { flush(); parent.appendChild(inline(el('em'), s.slice(i + 1, j))); i = j + 1; continue }
|
|
297
|
+
} else if (ch === '[') {
|
|
298
|
+
m = /^\[([^\]]+)\]\(([^)\s]+)\)/.exec(s.slice(i))
|
|
299
|
+
if (m) { flush(); inline(parent, m[1]); parent.appendChild(el('span', 'muted', ' (' + m[2] + ')')); i += m[0].length; continue }
|
|
300
|
+
}
|
|
301
|
+
buf += ch
|
|
302
|
+
i++
|
|
303
|
+
}
|
|
304
|
+
flush()
|
|
305
|
+
return parent
|
|
306
|
+
}
|
|
307
|
+
// Headings # to ####, paragraphs, bullet and numbered lists (nested by
|
|
308
|
+
// indentation), fenced code. Anything else is a paragraph of plain text.
|
|
309
|
+
function markdown(src) {
|
|
310
|
+
var root = el('div', 'md')
|
|
311
|
+
var lines = String(src || '').replace(/\r/g, '').split('\n')
|
|
312
|
+
var para = [], stack = [] // stack of { indent, list, li }
|
|
313
|
+
function endPara() { if (para.length) { root.appendChild(inline(el('p'), para.join(' '))); para = [] } }
|
|
314
|
+
for (var i = 0; i < lines.length; i++) {
|
|
315
|
+
var line = lines[i], m
|
|
316
|
+
if (/^\s*```/.test(line)) {
|
|
317
|
+
endPara(); stack = []
|
|
318
|
+
var code = []
|
|
319
|
+
for (i++; i < lines.length && !/^\s*```/.test(lines[i]); i++) code.push(lines[i])
|
|
320
|
+
root.appendChild(add(el('pre'), el('code', null, code.join('\n'))))
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
if (!line.trim()) { endPara(); continue }
|
|
324
|
+
if ((m = /^(#{1,4})\s+(.*)$/.exec(line))) {
|
|
325
|
+
endPara(); stack = []
|
|
326
|
+
root.appendChild(inline(el('h' + (m[1].length + 2)), m[2]))
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
if ((m = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line))) {
|
|
330
|
+
endPara()
|
|
331
|
+
var indent = m[1].length, tag = /^\d/.test(m[2]) ? 'ol' : 'ul'
|
|
332
|
+
while (stack.length && stack[stack.length - 1].indent > indent) stack.pop()
|
|
333
|
+
var top = stack[stack.length - 1]
|
|
334
|
+
if (!top || top.indent < indent || top.list.tagName.toLowerCase() !== tag) {
|
|
335
|
+
if (top && top.indent === indent) stack.pop()
|
|
336
|
+
var list = el(tag)
|
|
337
|
+
if (stack.length && stack[stack.length - 1].li) stack[stack.length - 1].li.appendChild(list); else root.appendChild(list)
|
|
338
|
+
top = { indent: indent, list: list, li: null }
|
|
339
|
+
stack.push(top)
|
|
340
|
+
}
|
|
341
|
+
top.li = inline(el('li'), m[3])
|
|
342
|
+
top.list.appendChild(top.li)
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
if (stack.length && /^\s/.test(line)) { // a wrapped list item
|
|
346
|
+
var cur = stack[stack.length - 1]
|
|
347
|
+
cur.li.appendChild(text(' '))
|
|
348
|
+
inline(cur.li, line.trim())
|
|
349
|
+
continue
|
|
350
|
+
}
|
|
351
|
+
stack = []
|
|
352
|
+
para.push(line.trim())
|
|
353
|
+
}
|
|
354
|
+
endPara()
|
|
355
|
+
return root
|
|
134
356
|
}
|
|
135
357
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
358
|
+
// ---- the Reviews view in the main pane ----
|
|
359
|
+
function verdictOf(findings) {
|
|
360
|
+
var m = /VERDICT:\W*(CLEAN|ISSUES)/i.exec(String(findings).slice(-400))
|
|
361
|
+
return m ? m[1].toUpperCase() : null
|
|
362
|
+
}
|
|
363
|
+
function entryHead(e) {
|
|
364
|
+
var head = el('div', 'ehead')
|
|
365
|
+
add(head, el('strong', null, e.agent + (e.model ? ':' + e.model : '') + (e.mode ? ' (' + e.mode + ')' : '')),
|
|
366
|
+
el('span', 'muted', e.at.slice(0, 16).replace('T', ' ') + ', of ' + e.target))
|
|
367
|
+
var v = verdictOf(e.findings)
|
|
368
|
+
if (v) add(head, el('span', 'chip ' + (v === 'ISSUES' ? 'bad' : 'good'), v === 'ISSUES' ? 'issues found' : 'no issues'))
|
|
369
|
+
return head
|
|
370
|
+
}
|
|
371
|
+
function reviewsPane(c, stage) {
|
|
372
|
+
var pane = el('div')
|
|
373
|
+
add(pane, button('btn small back', '‹ Back', goBack))
|
|
374
|
+
add(pane, commitMeta(c), el('h1', 'title', 'Reviews: ' + c.subject))
|
|
375
|
+
if (!c.reviews.length) return add(pane, el('p', 'muted', 'No reviews recorded for this commit.'))
|
|
376
|
+
c.reviews.forEach(function (r) {
|
|
377
|
+
var sec = el('section', 'rvstagepane')
|
|
378
|
+
sec.id = 'rv-' + r.stage
|
|
379
|
+
add(sec, el('h2', null, r.stage), add(el('p', 'rvsum'), el('span', reviewClass(r), r.phrase)))
|
|
380
|
+
var entries = r.entries.slice().sort(function (a, b) { return a.at < b.at ? 1 : a.at > b.at ? -1 : 0 })
|
|
381
|
+
entries.forEach(function (e, i) {
|
|
382
|
+
if (i === 0) add(sec, add(el('div', 'entry'), entryHead(e), markdown(e.findings)))
|
|
383
|
+
else {
|
|
384
|
+
var d = el('details', 'entry old')
|
|
385
|
+
add(d, add(el('summary'), entryHead(e)), markdown(e.findings))
|
|
386
|
+
add(sec, d)
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
add(pane, sec)
|
|
151
390
|
})
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
391
|
+
return pane
|
|
392
|
+
}
|
|
393
|
+
function goBack() {
|
|
394
|
+
var b = state.back
|
|
395
|
+
if (b && b.kind === 'group') selectGroup(b.ci, b.gid)
|
|
396
|
+
else if (b) selectKind(b.ci, b.kind)
|
|
397
|
+
else selectKind(state.ci, 'overview')
|
|
398
|
+
}
|
|
399
|
+
function selectReviews(ci, stage) {
|
|
400
|
+
if (state.kind !== 'reviews' && state.kind !== 'risks') state.back = { ci: state.ci, kind: state.kind, gid: state.gid }
|
|
401
|
+
state.ci = ci
|
|
402
|
+
state.kind = 'reviews'
|
|
403
|
+
state.gid = null
|
|
404
|
+
state.stage = stage || null
|
|
405
|
+
show(!stage)
|
|
406
|
+
var t = stage && document.getElementById('rv-' + stage)
|
|
407
|
+
if (t) t.scrollIntoView({ block: 'start' }) // deliberately also at load: #reviews-<stage> lands on that stage
|
|
408
|
+
}
|
|
409
|
+
function honestyLabel(c) {
|
|
410
|
+
var honesty = find(c.reviews, function (r) { return r.stage === 'honesty' })
|
|
411
|
+
if (!honesty) return ['stale', 'not reviewed']
|
|
412
|
+
var label = honesty.state !== 'seen' ? 'changed since review'
|
|
413
|
+
: honesty.verdict === 'issues' ? 'honesty: issues found'
|
|
414
|
+
: honesty.verdict === 'clean' ? 'honesty: no issues' : 'honesty-reviewed'
|
|
415
|
+
return [reviewClass(honesty), label]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ---- filters ----
|
|
419
|
+
var contractRank = IMPACTS.indexOf('contract') + 1
|
|
420
|
+
var passes = {
|
|
421
|
+
all: function () { return true },
|
|
422
|
+
focus: function (g, c) { return isFocus(c, g) },
|
|
423
|
+
contract: function (g) { return impactRank(effImpact(g)) >= contractRank },
|
|
424
|
+
low: function (g) { return effImpact(g) === 'local' }
|
|
425
|
+
}
|
|
426
|
+
function visibleGroups(c) {
|
|
427
|
+
var p = passes[state.filter]
|
|
428
|
+
return orderedGroups(c).filter(function (g) { return p(g, c) && !state.hidden.has(g.necessity) })
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---- the order commits are listed in ----
|
|
432
|
+
function commitOrder() {
|
|
433
|
+
// data.commits is oldest first; the rail lists newest first, like git log.
|
|
434
|
+
var idx = commits.map(function (c, i) { return i }).reverse()
|
|
435
|
+
if (multi && state.hot) {
|
|
436
|
+
idx.sort(function (a, b) { return (levelRank(levelOf(commits[b]).level) - levelRank(levelOf(commits[a]).level)) || (b - a) })
|
|
437
|
+
}
|
|
438
|
+
return idx
|
|
439
|
+
}
|
|
440
|
+
// The groups the reader steps through: those of the open commit, in rail order.
|
|
441
|
+
function neighbor(dir) {
|
|
442
|
+
var all = commits[state.ci].order
|
|
443
|
+
var vis = visibleGroups(commits[state.ci]).map(function (g) { return g.id })
|
|
444
|
+
if (!vis.length) return null
|
|
445
|
+
var i = state.kind === 'group' ? all.indexOf(state.gid) : -1
|
|
446
|
+
if (i < 0) return vis[dir > 0 ? 0 : vis.length - 1]
|
|
447
|
+
// The nearest visible group after / before this one in the full order, even if the filter hides this one.
|
|
448
|
+
for (var j = i + dir; j >= 0 && j < all.length; j += dir) if (vis.indexOf(all[j]) >= 0) return all[j]
|
|
449
|
+
return null
|
|
450
|
+
}
|
|
451
|
+
// j/k and Previous/Next: groups when one is open (or on a single commit's
|
|
452
|
+
// overview), commits on a range's commit list.
|
|
453
|
+
function stepTo(dir) {
|
|
454
|
+
if (!multi && state.kind === 'overview' && dir < 0) return
|
|
455
|
+
if (!multi && state.kind === 'group' && dir < 0 && !neighbor(-1)) { selectKind(0, 'overview'); return }
|
|
456
|
+
if (multi && state.kind !== 'group') {
|
|
457
|
+
var order = commitOrder()
|
|
458
|
+
var next = order[order.indexOf(state.ci) + dir]
|
|
459
|
+
if (next !== undefined) selectKind(next, 'overview')
|
|
460
|
+
return
|
|
167
461
|
}
|
|
462
|
+
var id = neighbor(dir)
|
|
463
|
+
if (id) selectGroup(state.ci, id)
|
|
464
|
+
}
|
|
465
|
+
function resetFilters() { state.filter = 'all'; state.hidden.clear() }
|
|
466
|
+
|
|
467
|
+
// ---- the detail pane ----
|
|
468
|
+
var topEl = el('header', 'top')
|
|
469
|
+
var railEl = el('aside', 'rail')
|
|
470
|
+
railEl.setAttribute('aria-label', multi ? 'Commits in the range' : 'Groups in review order')
|
|
471
|
+
var mainEl = el('main', 'main')
|
|
472
|
+
var body = add(el('div', multi ? 'body range' : 'body'), railEl, mainEl)
|
|
473
|
+
|
|
474
|
+
// What the author flagged about the commit as a whole: it belongs to the
|
|
475
|
+
// Overview, not under any one group.
|
|
476
|
+
function risksSection(c, pane) {
|
|
168
477
|
if (c.risks.length) {
|
|
169
478
|
var rl = el('ul', 'list')
|
|
170
479
|
c.risks.forEach(function (r) { rl.appendChild(el('li', null, r)) })
|
|
171
|
-
|
|
480
|
+
var h = el('h2', null, 'Risks the author flagged (' + c.risks.length + ')')
|
|
481
|
+
h.id = 'risks'
|
|
482
|
+
add(pane, h, rl)
|
|
172
483
|
}
|
|
173
484
|
if (c.alternatives.length) {
|
|
174
485
|
var al = el('ul', 'list')
|
|
175
486
|
c.alternatives.forEach(function (x) {
|
|
176
487
|
al.appendChild(add(el('li'), el('strong', null, x.option), el('span', 'why', ' — ' + x.rejected_because)))
|
|
177
488
|
})
|
|
178
|
-
add(
|
|
179
|
-
}
|
|
180
|
-
return tocList
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function render() {
|
|
184
|
-
var layout = el('div', 'layout')
|
|
185
|
-
var toc = el('nav', 'toc')
|
|
186
|
-
var main = el('main')
|
|
187
|
-
add(layout, toc, main)
|
|
188
|
-
var allGroups = []
|
|
189
|
-
data.commits.forEach(function (c) { allGroups = allGroups.concat(c.groups) })
|
|
190
|
-
|
|
191
|
-
// Whether each commit as it is now was reviewed, per stage; each stage's
|
|
192
|
-
// reports open on demand. Findings are shown as text.
|
|
193
|
-
function reviewsBlock(c, target) {
|
|
194
|
-
var box = el('div', 'reviews')
|
|
195
|
-
if (!c.reviews.length) { add(target, add(box, el('span', 'review-none', 'No reviews recorded'))); return }
|
|
196
|
-
c.reviews.forEach(function (r) {
|
|
197
|
-
var d = el('details', 'review')
|
|
198
|
-
add(d, add(el('summary'), el('span', r.state === 'seen' && r.verdict !== 'issues' ? (r.verdict === 'clean' ? 'review-ok' : 'review-none') : 'stale', r.phrase)))
|
|
199
|
-
r.entries.forEach(function (e) {
|
|
200
|
-
add(d, el('div', 'note', r.stage + ' by ' + e.agent + (e.model ? ':' + e.model : '') + (e.mode ? ' (' + e.mode + ')' : '') +
|
|
201
|
-
', ' + e.at.slice(0, 16).replace('T', ' ') + ', of ' + e.target), el('pre', 'findings', e.findings))
|
|
202
|
-
})
|
|
203
|
-
add(box, d)
|
|
204
|
-
})
|
|
205
|
-
add(target, box)
|
|
489
|
+
add(pane, el('h2', null, 'Alternatives the author rejected (' + c.alternatives.length + ')'), al)
|
|
206
490
|
}
|
|
491
|
+
}
|
|
492
|
+
function riskCounts(c) {
|
|
493
|
+
var parts = []
|
|
494
|
+
if (c.risks.length) parts.push(plural(c.risks.length, 'risk'))
|
|
495
|
+
if (c.alternatives.length) parts.push(plural(c.alternatives.length, 'alternative'))
|
|
496
|
+
return parts.join(' · ')
|
|
497
|
+
}
|
|
498
|
+
// The row that opens the risks and alternatives page; none when there are neither.
|
|
499
|
+
function risksRow(ci) {
|
|
500
|
+
var c = commits[ci]
|
|
501
|
+
if (!c.risks.length && !c.alternatives.length) return null
|
|
502
|
+
var b = button('rvrow', null, function () { selectRisks(ci) })
|
|
503
|
+
b.setAttribute('data-fid', 'risks:' + ci)
|
|
504
|
+
return add(b, el('span', 'rvdot none'), el('span', 'rvstage', 'Risks and alternatives'), el('span', 'rvtxt', riskCounts(c)))
|
|
505
|
+
}
|
|
506
|
+
function risksPane(c) {
|
|
507
|
+
var pane = el('div')
|
|
508
|
+
add(pane, button('btn small back', '‹ Back', goBack))
|
|
509
|
+
add(pane, commitMeta(c), el('h1', 'title', 'Risks and alternatives: ' + c.subject))
|
|
510
|
+
risksSection(c, pane)
|
|
511
|
+
return pane
|
|
512
|
+
}
|
|
513
|
+
function selectRisks(ci) {
|
|
514
|
+
if (state.kind !== 'reviews' && state.kind !== 'risks') state.back = { ci: state.ci, kind: state.kind, gid: state.gid }
|
|
515
|
+
state.ci = ci
|
|
516
|
+
state.kind = 'risks'
|
|
517
|
+
state.gid = null
|
|
518
|
+
show(true)
|
|
519
|
+
}
|
|
207
520
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
var
|
|
250
|
-
var
|
|
521
|
+
// Previous / Next / mark read: rebuilt whenever the filters change, since
|
|
522
|
+
// which neighbours exist depends on the filter and the position only (not on
|
|
523
|
+
// read marks).
|
|
524
|
+
function navBar() {
|
|
525
|
+
var next = neighbor(1)
|
|
526
|
+
var nav = el('div', 'nav')
|
|
527
|
+
var pb = button('btn', 'Previous', function () { stepTo(-1) })
|
|
528
|
+
pb.setAttribute('data-fid', 'nav:prev')
|
|
529
|
+
var nb = button('btn', 'Next', function () { stepTo(1) })
|
|
530
|
+
nb.setAttribute('data-fid', 'nav:next')
|
|
531
|
+
pb.disabled = !neighbor(-1) && !(!multi && state.kind === 'group')
|
|
532
|
+
nb.disabled = !next
|
|
533
|
+
var rb = button('btn primary', next ? 'Mark as read and go to next' : 'Mark as read', function () { markRead(true); stepTo(1) })
|
|
534
|
+
rb.setAttribute('data-fid', 'nav:read')
|
|
535
|
+
return add(nav, pb, nb, rb, add(el('span', 'keys'), el('kbd', null, 'j'), text(' '), el('kbd', null, 'k'), text(multi ? ' step · ' : ' step (the overview comes before the first group) · '),
|
|
536
|
+
el('kbd', null, 'r'), text(' mark read · '), el('kbd', null, 'o'), text(' fold hunk')))
|
|
537
|
+
}
|
|
538
|
+
function refreshNav() {
|
|
539
|
+
var old = mainEl.querySelector('.nav')
|
|
540
|
+
if (old) old.parentNode.replaceChild(navBar(), old)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// The "Changes" heading with the buttons that fold every hunk of the pane.
|
|
544
|
+
function changesHead(title) {
|
|
545
|
+
var expand = button('btn small', 'Expand all hunks', function () { setCode(true) })
|
|
546
|
+
var collapse = button('btn small', 'Collapse all hunks', function () { setCode(false) })
|
|
547
|
+
expand.setAttribute('data-fid', 'code:expand')
|
|
548
|
+
collapse.setAttribute('data-fid', 'code:collapse')
|
|
549
|
+
return add(el('div', 'chead'), el('h2', null, title), add(el('span'), expand, text(' '), collapse))
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// On a range, a group or the unannotated changes open inside their commit.
|
|
553
|
+
function backLink(c, pane) {
|
|
554
|
+
if (!multi) return
|
|
555
|
+
var b = button('btn small back', '‹ ' + c.subject, function () { selectKind(commits.indexOf(c), 'overview') })
|
|
556
|
+
b.title = 'Back to the commit'
|
|
557
|
+
add(pane, b)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function groupPane(c, g) {
|
|
561
|
+
var pane = el('div')
|
|
562
|
+
var order = c.order
|
|
563
|
+
var hunkN = g.hunks.length
|
|
564
|
+
var files = []
|
|
565
|
+
var perFile = {}
|
|
566
|
+
g.hunks.forEach(function (h) {
|
|
567
|
+
if (!perFile[h.file]) { perFile[h.file] = 0; files.push(h.file) }
|
|
568
|
+
perFile[h.file]++
|
|
569
|
+
})
|
|
570
|
+
backLink(c, pane)
|
|
571
|
+
add(pane, commitMeta(c))
|
|
572
|
+
add(pane, el('div', 'crumbs', (multi ? sha7(c) + ' · ' : '') + 'Group ' + (order.indexOf(g.id) + 1) + ' of ' + order.length + ' · ' +
|
|
573
|
+
plural(hunkN, 'hunk') + ' in ' + plural(files.length, 'file')))
|
|
574
|
+
add(pane, el('h1', 'intent', g.intent))
|
|
575
|
+
|
|
576
|
+
var tags = el('div', 'tags')
|
|
577
|
+
add(tags, el('span', 'chip', g.necessity))
|
|
578
|
+
var level = effImpact(g)
|
|
579
|
+
var imp = add(el('span', 'imp' + (g.impact ? '' : ' derived')), meter(level), text(impactWord(g)))
|
|
580
|
+
if (level) imp.style.setProperty('--m', 'var(--impact-' + level + ')')
|
|
581
|
+
if (!g.impact && level) imp.title = 'Derived from the diff, not declared in the gloss'
|
|
582
|
+
add(tags, imp, el('span', 'chip mono', g.id))
|
|
583
|
+
if (g.serves[0] !== 'task') add(tags, add(el('span', 'chip'), text('serves '), linkList(c, g.serves)))
|
|
584
|
+
else add(tags, el('span', 'chip', 'serves task'))
|
|
585
|
+
if (isFocus(c, g)) add(tags, el('span', 'chip', 'look here first'))
|
|
586
|
+
if (g.stale) add(tags, el('span', 'stale', g.stale + ' of ' + plural(g.total, 'hunk') + ' changed or gone'))
|
|
587
|
+
add(tags, stats(g.added, g.removed))
|
|
588
|
+
add(pane, tags)
|
|
589
|
+
if (g.impact_reason) add(pane, el('p', 'reason', g.impact_reason))
|
|
590
|
+
warnings(c, pane)
|
|
591
|
+
|
|
592
|
+
var cards = el('div', 'cards')
|
|
593
|
+
function card(label, value) {
|
|
594
|
+
if (value) add(cards, add(el('div', 'card'), el('div', 'eyebrow', label), el('p', null, value)))
|
|
595
|
+
}
|
|
596
|
+
card('If removed', g.removal_impact)
|
|
597
|
+
card('Evidence', g.evidence)
|
|
598
|
+
card('Notes', g.notes)
|
|
599
|
+
add(pane, cards)
|
|
600
|
+
|
|
601
|
+
add(pane, changesHead('Changes'))
|
|
602
|
+
if (files.length) {
|
|
603
|
+
var fl = el('div', 'files')
|
|
604
|
+
files.forEach(function (f) { add(fl, add(el('div', 'frow'), el('span', 'mono', f), el('span', 'muted', plural(perFile[f], 'hunk')))) })
|
|
605
|
+
add(pane, fl)
|
|
606
|
+
}
|
|
607
|
+
g.hunks.forEach(function (h) { pane.appendChild(hunkView(c, h, g.id)) })
|
|
608
|
+
add(pane, navBar())
|
|
609
|
+
return pane
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function unannotatedBox(c) {
|
|
613
|
+
var box = add(el('div', 'unannotated'), el('p', null, c.glossed
|
|
614
|
+
? 'These changes are in the commit but no group explains them.'
|
|
615
|
+
: 'This commit has no gloss, so nothing explains these changes.'))
|
|
616
|
+
c.unannotated.forEach(function (h) { box.appendChild(hunkView(c, h, 'un')) })
|
|
617
|
+
return box
|
|
618
|
+
}
|
|
619
|
+
function unannotatedPane(c) {
|
|
620
|
+
var pane = el('div')
|
|
621
|
+
backLink(c, pane)
|
|
622
|
+
add(pane, commitMeta(c), el('h1', 'title', (c.glossed ? 'Unannotated changes' : 'Changes') + ' (' + c.unannotated.length + ')'))
|
|
623
|
+
warnings(c, pane)
|
|
624
|
+
add(pane, changesHead('Changes'), unannotatedBox(c))
|
|
625
|
+
return pane
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Share of changed lines per necessity, and the groups of the commit.
|
|
629
|
+
function necessityBar(c) {
|
|
630
|
+
var total = c.groups.reduce(function (s, g) { return s + g.weight }, 0) || 1
|
|
251
631
|
var bar = el('div', 'bar')
|
|
252
|
-
var
|
|
253
|
-
|
|
254
|
-
var
|
|
255
|
-
|
|
256
|
-
var
|
|
632
|
+
var legend = el('div', 'legend')
|
|
633
|
+
data.necessity.forEach(function (n) {
|
|
634
|
+
var gs = c.groups.filter(function (g) { return g.necessity === n.name })
|
|
635
|
+
if (!gs.length) return
|
|
636
|
+
var weight = gs.reduce(function (s, g) { return s + g.weight }, 0)
|
|
637
|
+
var seg = el('span')
|
|
638
|
+
seg.style.setProperty('--c', 'var(--n-' + n.name + ')')
|
|
257
639
|
seg.style.width = (100 * weight / total) + '%'
|
|
258
640
|
seg.title = n.name + ': ' + Math.round(100 * weight / total) + '% of changed lines'
|
|
259
641
|
bar.appendChild(seg)
|
|
642
|
+
var item = el('span', null, n.name + ' ' + gs.length)
|
|
643
|
+
item.title = n.help
|
|
644
|
+
var dot = el('span', 'dot')
|
|
645
|
+
dot.style.setProperty('--c', 'var(--n-' + n.name + ')')
|
|
646
|
+
item.insertBefore(dot, item.firstChild)
|
|
647
|
+
legend.appendChild(item)
|
|
648
|
+
})
|
|
649
|
+
return [bar, legend]
|
|
650
|
+
}
|
|
260
651
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
652
|
+
// The groups the author says to read first, as links; nothing when none are declared.
|
|
653
|
+
function focusSection(c, ci, pane) {
|
|
654
|
+
var focus = c.attention && c.attention.focus ? c.attention.focus : []
|
|
655
|
+
var groups = focus.map(function (id) { return groupOf(c, id) }).filter(Boolean)
|
|
656
|
+
if (!groups.length) return
|
|
657
|
+
var list = el('div', 'focuscards')
|
|
658
|
+
groups.forEach(function (g) {
|
|
659
|
+
var b = button('focuscard', null, function () { selectGroup(ci, g.id) })
|
|
660
|
+
var level = effImpact(g)
|
|
661
|
+
var imp = add(el('span', 'imp'), meter(level), text(impactWord(g)))
|
|
662
|
+
if (level) imp.style.setProperty('--m', 'var(--impact-' + level + ')')
|
|
663
|
+
add(b, el('span', 'fctitle', g.intent), add(el('span', 'fcmeta'), imp, el('span', 'chip', g.necessity)))
|
|
664
|
+
if (g.impact_reason) add(b, el('span', 'fcreason', g.impact_reason))
|
|
665
|
+
list.appendChild(b)
|
|
273
666
|
})
|
|
274
|
-
add(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
var
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
667
|
+
add(pane, el('h2', null, 'Look here first'), list)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function overviewPane(c) {
|
|
671
|
+
var ci = commits.indexOf(c)
|
|
672
|
+
var pane = el('div')
|
|
673
|
+
if (multi) add(pane, el('div', 'eyebrow', 'Commit ' + (ci + 1) + ' of ' + commits.length + ' · ' + new Date(c.date).toLocaleDateString()))
|
|
674
|
+
add(pane, el('h1', 'title', c.subject), commitMeta(c))
|
|
675
|
+
if (!c.glossed) {
|
|
676
|
+
var w = add(el('section', 'warnbox'), el('div', 't', 'No gloss on this commit'),
|
|
677
|
+
el('p', null, 'Its whole diff is unannotated, so there is no attention level, no groups and no reasons to read.'),
|
|
678
|
+
el('p', 'cmd', 'git gloss add ' + sha7(c)))
|
|
679
|
+
add(pane, w)
|
|
680
|
+
add(pane, changesHead('Changes (' + c.unannotated.length + ')'), unannotatedBox(c))
|
|
681
|
+
return pane
|
|
682
|
+
}
|
|
683
|
+
warnings(c, pane)
|
|
684
|
+
add(pane, attentionCard(c))
|
|
685
|
+
statement(pane, 'Task', c.task)
|
|
686
|
+
statement(pane, 'Summary', c.summary)
|
|
687
|
+
focusSection(c, ci, pane)
|
|
688
|
+
if (c.groups.length) {
|
|
689
|
+
var nb = necessityBar(c)
|
|
690
|
+
add(pane, el('h2', null, plural(c.groups.length, 'group')), nb[0], nb[1])
|
|
691
|
+
if (multi) groupList(ci, pane, true)
|
|
692
|
+
else {
|
|
693
|
+
var chips = el('div', 'chips')
|
|
694
|
+
orderedGroups(c).forEach(function (g) {
|
|
695
|
+
var b = button('chip link', null, function () { selectGroup(ci, g.id) })
|
|
696
|
+
add(b, el('b', null, g.id), text(' '), el('span', null, g.necessity))
|
|
697
|
+
chips.appendChild(b)
|
|
698
|
+
})
|
|
699
|
+
add(pane, chips)
|
|
299
700
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
var
|
|
303
|
-
if (
|
|
304
|
-
add(
|
|
305
|
-
}
|
|
701
|
+
}
|
|
702
|
+
if (multi) {
|
|
703
|
+
var rr = risksRow(ci)
|
|
704
|
+
if (rr) add(pane, el('h2', null, 'Risks and alternatives'), rr)
|
|
705
|
+
add(pane, el('h2', null, 'Reviews'), reviewStatus(c))
|
|
706
|
+
}
|
|
707
|
+
return pane
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function renderPane(scroll) {
|
|
711
|
+
var c = commits[state.ci]
|
|
712
|
+
var g = state.kind === 'group' ? groupOf(c, state.gid) : null
|
|
713
|
+
var pane = state.kind === 'reviews' ? reviewsPane(c, state.stage) : state.kind === 'risks' ? risksPane(c) : g ? groupPane(c, g) : state.kind === 'unannotated' && c.unannotated.length ? unannotatedPane(c) : overviewPane(c)
|
|
714
|
+
keepFocus(function () {
|
|
715
|
+
mainEl.textContent = ''
|
|
716
|
+
add(mainEl, pane)
|
|
717
|
+
add(mainEl, footer())
|
|
718
|
+
syncFold()
|
|
719
|
+
}, true)
|
|
720
|
+
if (scroll && !booting) {
|
|
721
|
+
if (window.matchMedia && window.matchMedia('(min-width: 900px)').matches) window.scrollTo(0, 0)
|
|
722
|
+
else mainEl.scrollIntoView({ block: 'start' })
|
|
723
|
+
}
|
|
724
|
+
}
|
|
306
725
|
|
|
726
|
+
function footer() {
|
|
307
727
|
var foot = el('footer')
|
|
308
|
-
add(foot,
|
|
309
|
-
|
|
310
|
-
el('kbd', null, 'e'),
|
|
311
|
-
|
|
728
|
+
add(foot, text('Keys: '), el('kbd', null, 'j'), text(' / '), el('kbd', null, 'k'),
|
|
729
|
+
text(multi ? ' next / previous group · ' : ' next / previous group (the overview comes before the first group) · '), el('kbd', null, 'o'), text(' fold or unfold the focused hunk (else the first) · '),
|
|
730
|
+
el('kbd', null, 'e'), text(' expand all hunks · '), el('kbd', null, 'c'), text(' collapse all hunks · '),
|
|
731
|
+
el('kbd', null, 'r'), text(' mark group read or unread.'))
|
|
312
732
|
add(foot, el('p', null, 'A snapshot generated by git-gloss ' + new Date(data.generated).toLocaleString() +
|
|
313
733
|
'. It does not update if the gloss or commit changes; regenerate with git gloss html ' +
|
|
314
|
-
(data.range ||
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
734
|
+
(data.range || commits[0].sha.slice(0, 10)) + '.'))
|
|
735
|
+
return foot
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ---- the top bar ----
|
|
739
|
+
function filterSeg(c) {
|
|
740
|
+
var seg = el('div', 'seg')
|
|
741
|
+
seg.setAttribute('role', 'group')
|
|
742
|
+
seg.setAttribute('aria-label', 'Show groups')
|
|
743
|
+
FILTERS.forEach(function (f) {
|
|
744
|
+
var n = c.groups.filter(function (g) { return passes[f[0]](g, c) }).length
|
|
745
|
+
var b = add(button(null, f[1]), el('span', 'n', n))
|
|
746
|
+
b.setAttribute('aria-pressed', String(state.filter === f[0]))
|
|
747
|
+
b.setAttribute('data-fid', 'seg:' + f[0])
|
|
748
|
+
b.addEventListener('click', function () { state.filter = f[0]; renderChrome() })
|
|
749
|
+
seg.appendChild(b)
|
|
750
|
+
})
|
|
751
|
+
return seg
|
|
752
|
+
}
|
|
753
|
+
function renderTop() {
|
|
754
|
+
var c = commits[state.ci]
|
|
755
|
+
topEl.textContent = ''
|
|
756
|
+
add(topEl, el('span', 'brand', 'git gloss'))
|
|
757
|
+
if (multi) {
|
|
758
|
+
var glossed = commits.filter(function (x) { return x.glossed }).length
|
|
759
|
+
var vetted = commits.filter(function (x) {
|
|
760
|
+
return x.glossed && x.reviews.some(function (r) { return r.stage === 'honesty' && r.state === 'seen' })
|
|
761
|
+
}).length
|
|
762
|
+
add(topEl, el('span', 'sha', data.range || 'commits'),
|
|
763
|
+
el('span', 'subject', plural(commits.length, 'commit') + ', ' + glossed + ' glossed' + (glossed ? ', ' + vetted + ' honesty-reviewed' : '')))
|
|
764
|
+
var order = el('div', 'seg')
|
|
765
|
+
order.setAttribute('role', 'group')
|
|
766
|
+
order.setAttribute('aria-label', 'Order commits')
|
|
767
|
+
;[[true, 'Hottest first'], [false, 'History order']].forEach(function (o) {
|
|
768
|
+
var b = button(null, o[1], function () { state.hot = o[0]; renderChrome() })
|
|
769
|
+
b.setAttribute('aria-pressed', String(state.hot === o[0]))
|
|
770
|
+
b.setAttribute('data-fid', 'order:' + o[1])
|
|
771
|
+
order.appendChild(b)
|
|
326
772
|
})
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
var
|
|
348
|
-
|
|
349
|
-
var
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
773
|
+
add(topEl, order)
|
|
774
|
+
} else {
|
|
775
|
+
var subject = el('span', 'subject', c.subject)
|
|
776
|
+
subject.title = c.subject
|
|
777
|
+
add(topEl, el('span', 'sha', c.sha.slice(0, 7)), subject)
|
|
778
|
+
}
|
|
779
|
+
var counts = readCounts()
|
|
780
|
+
var fill = el('i')
|
|
781
|
+
fill.style.width = (counts.total ? Math.round(100 * counts.read / counts.total) : 0) + '%'
|
|
782
|
+
var prog = add(el('div', 'prog'), el('span', null, counts.read + ' of ' + counts.total + ' read'), add(el('span', 'track'), fill))
|
|
783
|
+
prog.setAttribute('aria-live', 'polite')
|
|
784
|
+
if (!multi) add(topEl, filterSeg(c))
|
|
785
|
+
add(topEl, prog)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ---- the rail ----
|
|
789
|
+
function groupRow(ci, g) {
|
|
790
|
+
var c = commits[ci]
|
|
791
|
+
var k = key(c, g.id)
|
|
792
|
+
var sel = state.ci === ci && state.kind === 'group' && state.gid === g.id
|
|
793
|
+
var done = !!readSet(c)[g.id]
|
|
794
|
+
var row = el('div', 'row' + (sel ? ' sel' : '') + (done ? ' done' : ''))
|
|
795
|
+
var chk = button('check', null, function () { setRead(c, g.id, !readSet(c)[g.id]); renderChrome() })
|
|
796
|
+
chk.setAttribute('aria-pressed', String(done))
|
|
797
|
+
chk.setAttribute('aria-label', (done ? 'Mark unread: ' : 'Mark read: ') + g.id)
|
|
798
|
+
chk.setAttribute('data-fid', 'chk:' + k)
|
|
799
|
+
add(chk, el('span', null, done ? '✓' : ''))
|
|
800
|
+
var pick = button('pick', null, function () { selectGroup(ci, g.id) })
|
|
801
|
+
if (sel) pick.setAttribute('aria-current', 'true')
|
|
802
|
+
pick.setAttribute('data-fid', 'pick:' + k)
|
|
803
|
+
var sub = add(el('span', 'rsub'), el('span', 'mono', g.id), text(' · ' + g.necessity + ' · ' + impactWord(g)))
|
|
804
|
+
add(pick, meter(effImpact(g)), add(el('span', 'rtxt'), el('span', 'rtitle', g.intent), sub))
|
|
805
|
+
if (isFocus(c, g)) {
|
|
806
|
+
var flag = el('span', 'flag', 'first')
|
|
807
|
+
var lv = levelOf(c).level
|
|
808
|
+
if (lv) flag.style.setProperty('--lv', 'var(--attention-' + lv + ')')
|
|
809
|
+
add(pick, flag)
|
|
810
|
+
}
|
|
811
|
+
return add(row, chk, pick)
|
|
812
|
+
}
|
|
813
|
+
function extraRow(ci, kind, label, sub) {
|
|
814
|
+
var sel = state.ci === ci && state.kind === kind
|
|
815
|
+
var row = el('div', sel ? 'row sel' : 'row')
|
|
816
|
+
var pick = button('pick', null, function () { selectKind(ci, kind) })
|
|
817
|
+
if (sel) pick.setAttribute('aria-current', 'true')
|
|
818
|
+
pick.setAttribute('data-fid', 'extra:' + kind + ':' + ci)
|
|
819
|
+
add(pick, add(el('span', 'rtxt'), el('span', 'rtitle', label), el('span', 'rsub', sub)))
|
|
820
|
+
return add(row, pick)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// The group list of one commit: necessity filter chips, then the rows.
|
|
824
|
+
function groupList(ci, parent, withSeg) {
|
|
825
|
+
var c = commits[ci]
|
|
826
|
+
if (withSeg) add(parent, add(el('div', 'filters'), filterSeg(c)))
|
|
827
|
+
var present = data.necessity.filter(function (n) { return c.groups.some(function (g) { return g.necessity === n.name }) })
|
|
828
|
+
if (present.length > 1 || present.some(function (n) { return state.hidden.has(n.name) })) {
|
|
829
|
+
var necs = el('div', 'necs')
|
|
830
|
+
necs.setAttribute('role', 'group')
|
|
831
|
+
necs.setAttribute('aria-label', 'Show necessity')
|
|
832
|
+
present.forEach(function (n) {
|
|
833
|
+
var count = c.groups.filter(function (g) { return g.necessity === n.name }).length
|
|
834
|
+
var chip = add(button('chip', null, function () {
|
|
835
|
+
if (state.hidden.has(n.name)) state.hidden.delete(n.name); else state.hidden.add(n.name)
|
|
836
|
+
renderChrome()
|
|
837
|
+
}), el('span', null, n.name), el('span', 'count', count))
|
|
838
|
+
chip.title = n.help + ' (click to show or hide)'
|
|
839
|
+
chip.setAttribute('aria-pressed', String(!state.hidden.has(n.name)))
|
|
840
|
+
chip.setAttribute('data-fid', 'nec:' + n.name)
|
|
841
|
+
necs.appendChild(chip)
|
|
842
|
+
})
|
|
843
|
+
add(parent, necs)
|
|
844
|
+
}
|
|
845
|
+
var rows = el('div', 'rows')
|
|
846
|
+
var vis = visibleGroups(c)
|
|
847
|
+
vis.forEach(function (g) { rows.appendChild(groupRow(ci, g)) })
|
|
848
|
+
if (!vis.length && c.groups.length) {
|
|
849
|
+
var none = el('div', 'emptyrows', 'No groups match this filter. ')
|
|
850
|
+
add(none, button('btn small', 'Show all', function () { resetFilters(); renderChrome() }))
|
|
851
|
+
add(parent, none)
|
|
852
|
+
}
|
|
853
|
+
if (c.unannotated.length && c.glossed) rows.appendChild(extraRow(ci, 'unannotated', 'Unannotated changes (' + c.unannotated.length + ')', 'in the commit, explained by no group'))
|
|
854
|
+
add(parent, rows)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function commitRow(ci) {
|
|
858
|
+
var c = commits[ci]
|
|
859
|
+
var lv = levelOf(c)
|
|
860
|
+
var sel = state.ci === ci
|
|
861
|
+
var wrap = el('div', sel ? 'crow sel' : 'crow')
|
|
862
|
+
var pick = button('pick', null, function () { selectKind(ci, 'overview') })
|
|
863
|
+
if (sel) pick.setAttribute('aria-current', 'true')
|
|
864
|
+
pick.setAttribute('data-fid', 'commit:' + c.sha)
|
|
865
|
+
var level = el('span', 'lv')
|
|
866
|
+
if (lv.level) {
|
|
867
|
+
level.style.setProperty('--lv', 'var(--attention-' + lv.level + ')')
|
|
868
|
+
add(level, gauge(lv.level, true), el('span', null, lv.level.toUpperCase()))
|
|
869
|
+
} else add(level, el('span', null, c.glossed ? 'UNRATED' : 'NO GLOSS'))
|
|
870
|
+
var meta
|
|
871
|
+
if (c.glossed) {
|
|
872
|
+
var rs = readSet(c)
|
|
873
|
+
var done = c.groups.filter(function (g) { return rs[g.id] }).length
|
|
874
|
+
meta = plural(c.groups.length, 'group') + ' · ' + plural(fileCount(c), 'file') + (lv.source === 'unrated' ? '' : ' · ' + lv.source) + ' · ' + done + '/' + c.groups.length + ' read'
|
|
875
|
+
} else {
|
|
876
|
+
var added = 0, removed = 0
|
|
877
|
+
c.unannotated.forEach(function (h) { added += h.added; removed += h.removed })
|
|
878
|
+
meta = plural(fileCount(c), 'file') + ' · +' + added + ' −' + removed
|
|
879
|
+
}
|
|
880
|
+
var metaEl = el('span', 'cmeta', meta)
|
|
881
|
+
if (c.glossed) {
|
|
882
|
+
if (c.warnings.length) add(metaEl, text(' · '), el('span', 'stale', 'stale gloss'))
|
|
883
|
+
var hl = honestyLabel(c)
|
|
884
|
+
add(metaEl, text(' · '), el('span', hl[0], hl[1]))
|
|
885
|
+
} else add(metaEl, text(' · '), el('span', 'stale', 'no gloss'))
|
|
886
|
+
add(pick, add(el('span', 'rtop'), el('span', 'sha', sha7(c)), el('span', 'sha', new Date(c.date).toLocaleDateString()), level),
|
|
887
|
+
el('span', 'csubject', c.subject), metaEl)
|
|
888
|
+
add(wrap, pick)
|
|
889
|
+
return wrap
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function railSummary() {
|
|
893
|
+
var counts = {}
|
|
894
|
+
var strip = el('div', 'strip')
|
|
895
|
+
strip.setAttribute('aria-hidden', 'true')
|
|
896
|
+
var order = commitOrder()
|
|
897
|
+
order.forEach(function (ci) {
|
|
898
|
+
var lv = levelOf(commits[ci])
|
|
899
|
+
var name = lv.level || (commits[ci].glossed ? 'unrated' : 'no gloss')
|
|
900
|
+
counts[name] = (counts[name] || 0) + 1
|
|
901
|
+
var seg = el('i', lv.level ? null : 'none')
|
|
902
|
+
if (lv.level) seg.style.setProperty('--lv', 'var(--attention-' + lv.level + ')')
|
|
903
|
+
strip.appendChild(seg)
|
|
904
|
+
})
|
|
905
|
+
var line = el('div', 'line')
|
|
906
|
+
var first = true
|
|
907
|
+
LEVELS.slice().reverse().concat(['unrated', 'no gloss']).forEach(function (name) {
|
|
908
|
+
if (!counts[name]) return
|
|
909
|
+
if (!first) line.appendChild(text(' · '))
|
|
910
|
+
first = false
|
|
911
|
+
add(line, el('b', null, counts[name]), text(' ' + (name === 'no gloss' ? 'without a gloss' : name)))
|
|
912
|
+
})
|
|
913
|
+
var box = add(el('div', 'sum'), el('div', 'eyebrow', 'Range at a glance'), strip, line)
|
|
914
|
+
var top = commits[order[0]]
|
|
915
|
+
if (levelOf(top).level) {
|
|
916
|
+
var link = el('a', null, sha7(top) + ' ' + top.subject)
|
|
917
|
+
link.href = '#c-' + sha7(top)
|
|
918
|
+
link.addEventListener('click', function (ev) { ev.preventDefault(); selectKind(order[0], 'overview') })
|
|
919
|
+
add(box, add(el('div', 'line'), text(state.hot ? 'Read first: ' : 'Hottest: '), link))
|
|
920
|
+
}
|
|
921
|
+
return box
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function renderRail() {
|
|
925
|
+
railEl.textContent = ''
|
|
926
|
+
if (multi) {
|
|
927
|
+
add(railEl, railSummary(), add(el('div', 'railhead'), el('div', 'eyebrow', state.hot ? 'Hottest first, then newest' : 'Newest first')))
|
|
928
|
+
var list = el('div', 'commits')
|
|
929
|
+
commitOrder().forEach(function (ci) { list.appendChild(commitRow(ci)) })
|
|
930
|
+
add(railEl, list)
|
|
931
|
+
if (data.merges.length) {
|
|
932
|
+
var ml = add(el('div', 'mergelist'), el('div', 'eyebrow', 'Merges, not reviewed'))
|
|
933
|
+
data.merges.forEach(function (m) { ml.appendChild(add(el('div'), el('span', 'mono', m.sha.slice(0, 7)), text(' ' + m.subject))) })
|
|
934
|
+
add(railEl, ml)
|
|
935
|
+
}
|
|
936
|
+
return
|
|
937
|
+
}
|
|
938
|
+
var c = commits[0]
|
|
939
|
+
var ov = el('div', 'overviewrow')
|
|
940
|
+
var ovRow = extraRow(0, 'overview', 'Overview', 'task, summary, where to start')
|
|
941
|
+
var lvl = levelOf(c).level
|
|
942
|
+
if (lvl) {
|
|
943
|
+
var chip = add(el('span', 'lv'), gauge(lvl, true), el('span', null, lvl.toUpperCase()))
|
|
944
|
+
chip.style.setProperty('--lv', 'var(--attention-' + lvl + ')')
|
|
945
|
+
chip.title = 'Attention: ' + lvl + (c.attention ? ' (declared)' : ' (derived)')
|
|
946
|
+
ovRow.querySelector('.pick').appendChild(chip)
|
|
947
|
+
}
|
|
948
|
+
add(ov, ovRow)
|
|
949
|
+
add(railEl, ov)
|
|
950
|
+
add(railEl, add(el('div', 'railhead'), el('div', 'eyebrow', 'Review order · look first, then by impact')))
|
|
951
|
+
groupList(0, railEl)
|
|
952
|
+
var rr = risksRow(0)
|
|
953
|
+
if (rr) add(railEl, add(el('div', 'railsec'), el('div', 'eyebrow', 'Risks and alternatives'), rr))
|
|
954
|
+
add(railEl, add(el('div', 'railfoot'), el('div', 'eyebrow', 'Reviews'), reviewStatus(c)))
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Run a re-render and give keyboard focus back to the control that had it:
|
|
958
|
+
// the one with the same data-fid, or if that is gone or disabled (Next on the
|
|
959
|
+
// last group) the enabled button that is left in the same bar.
|
|
960
|
+
function keepFocus(render, pane) {
|
|
961
|
+
var active = document.activeElement
|
|
962
|
+
var fid = active && active.getAttribute ? active.getAttribute('data-fid') : null
|
|
963
|
+
var inPane = !!(pane && active && active !== document.body && mainEl.contains(active))
|
|
964
|
+
render()
|
|
965
|
+
var target = null
|
|
966
|
+
if (fid) {
|
|
967
|
+
target = app.querySelector('[data-fid="' + CSS.escape(fid) + '"]')
|
|
968
|
+
if (!target || target.disabled) {
|
|
969
|
+
var bar = fid.indexOf('nav:') === 0 ? '.nav' : fid.indexOf('code:') === 0 ? '.chead' : null
|
|
970
|
+
target = bar ? mainEl.querySelector(bar + ' button:not(:disabled)') : null
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// A link or card in the pane that opened another pane: focus moves to the new pane's heading.
|
|
974
|
+
if (!target && inPane) {
|
|
975
|
+
target = mainEl.querySelector('h1')
|
|
976
|
+
if (target) target.setAttribute('tabindex', '-1')
|
|
977
|
+
}
|
|
978
|
+
if (target) target.focus({ preventScroll: true })
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Re-render the rail and top bar (and what depends on the filters in the
|
|
982
|
+
// pane), keeping keyboard focus on the same control.
|
|
983
|
+
function renderChrome() {
|
|
984
|
+
keepFocus(function () {
|
|
985
|
+
renderTop()
|
|
986
|
+
renderRail()
|
|
987
|
+
if (multi && state.kind === 'overview') renderPane(false)
|
|
988
|
+
else refreshNav()
|
|
989
|
+
})
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ---- selection, kept in the hash so links and reloads land on it ----
|
|
993
|
+
function hashFor() {
|
|
994
|
+
var c = commits[state.ci]
|
|
995
|
+
if (state.kind === 'group') return '#g-' + key(c, state.gid)
|
|
996
|
+
if (state.kind === 'reviews') return '#reviews' + (multi ? '-' + sha7(c) : '') + (state.stage ? '-' + encodeURIComponent(state.stage) : '')
|
|
997
|
+
if (state.kind === 'risks') return '#risks' + (multi ? '-' + sha7(c) : '')
|
|
998
|
+
if (state.kind === 'unannotated') return '#unannotated' + (multi ? '-' + sha7(c) : '')
|
|
999
|
+
return multi ? '#c-' + sha7(c) : '#overview'
|
|
1000
|
+
}
|
|
1001
|
+
function remember() {
|
|
1002
|
+
try { history.replaceState(null, '', hashFor()) } catch (e) { /* not allowed in this context */ }
|
|
1003
|
+
}
|
|
1004
|
+
function show(scroll) {
|
|
1005
|
+
remember()
|
|
1006
|
+
renderChrome()
|
|
1007
|
+
renderPane(scroll)
|
|
1008
|
+
}
|
|
1009
|
+
function selectKind(ci, kind) {
|
|
1010
|
+
state.ci = ci
|
|
1011
|
+
state.kind = kind
|
|
1012
|
+
state.gid = null
|
|
1013
|
+
show(true)
|
|
1014
|
+
}
|
|
1015
|
+
function selectGroup(ci, gid) {
|
|
1016
|
+
var c = commits[ci]
|
|
1017
|
+
var g = groupOf(c, gid)
|
|
1018
|
+
if (!g) return
|
|
1019
|
+
// Following a link to a group the filters hide turns them off.
|
|
1020
|
+
if (visibleGroups(c).indexOf(g) < 0) resetFilters()
|
|
1021
|
+
state.ci = ci
|
|
1022
|
+
state.kind = 'group'
|
|
1023
|
+
state.gid = gid
|
|
1024
|
+
show(true)
|
|
1025
|
+
}
|
|
1026
|
+
function applyHash() {
|
|
1027
|
+
var h
|
|
1028
|
+
try { h = decodeURIComponent(location.hash.slice(1)) } catch (e) { return false }
|
|
1029
|
+
var i, j, c
|
|
1030
|
+
if (h.indexOf('g-') === 0) {
|
|
1031
|
+
for (i = 0; i < commits.length; i++) {
|
|
1032
|
+
for (j = 0; j < commits[i].groups.length; j++) {
|
|
1033
|
+
if (key(commits[i], commits[i].groups[j].id) === h.slice(2)) { selectGroup(i, commits[i].groups[j].id); return true }
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} else if (h === 'overview') { selectKind(0, 'overview'); return true }
|
|
1037
|
+
else if (h === 'risks' && !multi) { selectRisks(0); return true }
|
|
1038
|
+
else if (h.indexOf('risks-') === 0 && multi) {
|
|
1039
|
+
for (i = 0; i < commits.length; i++) if (sha7(commits[i]) === h.slice(6)) { selectRisks(i); return true }
|
|
1040
|
+
}
|
|
1041
|
+
else if (h.indexOf('c-') === 0 || h.indexOf('unannotated-') === 0 || h === 'unannotated') {
|
|
1042
|
+
var sha = h.slice(h.indexOf('-') + 1)
|
|
1043
|
+
for (i = 0; i < commits.length; i++) {
|
|
1044
|
+
c = commits[i]
|
|
1045
|
+
if ((h === 'unannotated' && i === 0) || (h !== 'unannotated' && sha7(c) === sha)) {
|
|
1046
|
+
selectKind(i, h.indexOf('c-') === 0 ? 'overview' : 'unannotated')
|
|
1047
|
+
return true
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
} else if (h === 'reviews' || h.indexOf('reviews-') === 0) {
|
|
1051
|
+
var rest = h.slice(8)
|
|
1052
|
+
if (!multi) { selectReviews(0, rest || null); return true }
|
|
1053
|
+
for (i = 0; i < commits.length; i++) {
|
|
1054
|
+
if (sha7(commits[i]) === rest.slice(0, 7)) { selectReviews(i, rest.slice(8) || null); return true }
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return false
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ---- actions ----
|
|
1061
|
+
function currentGroup() {
|
|
1062
|
+
return state.kind === 'group' ? groupOf(commits[state.ci], state.gid) : null
|
|
1063
|
+
}
|
|
1064
|
+
function markRead(on) {
|
|
1065
|
+
var g = currentGroup()
|
|
1066
|
+
if (!g) return
|
|
1067
|
+
setRead(commits[state.ci], g.id, on)
|
|
1068
|
+
renderChrome()
|
|
366
1069
|
}
|
|
367
1070
|
|
|
368
1071
|
document.addEventListener('keydown', function (ev) {
|
|
369
1072
|
if (ev.metaKey || ev.ctrlKey || ev.altKey) return
|
|
370
1073
|
var t = ev.target
|
|
371
1074
|
if (t && (/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName) || t.isContentEditable)) return
|
|
372
|
-
if (ev.key === 'j')
|
|
373
|
-
else if (ev.key === 'k')
|
|
374
|
-
else if (ev.key === 'o'
|
|
375
|
-
|
|
376
|
-
|
|
1075
|
+
if (ev.key === 'j') stepTo(1)
|
|
1076
|
+
else if (ev.key === 'k') stepTo(-1)
|
|
1077
|
+
else if (ev.key === 'o') {
|
|
1078
|
+
var f = document.activeElement
|
|
1079
|
+
var w = f && f.closest ? f.closest('.hunk') : null
|
|
1080
|
+
var tg = (w && w.querySelector('.htoggle')) || mainEl.querySelector('.htoggle')
|
|
1081
|
+
if (tg) { setHunkOpen(tg.closest('.hunk'), tg.getAttribute('aria-expanded') !== 'true'); syncFold() }
|
|
1082
|
+
}
|
|
1083
|
+
else if (ev.key === 'e') setCode(true)
|
|
1084
|
+
else if (ev.key === 'c') setCode(false)
|
|
1085
|
+
else if (ev.key === 'r') {
|
|
1086
|
+
var g = currentGroup()
|
|
1087
|
+
if (g) markRead(!readSet(commits[state.ci])[g.id])
|
|
1088
|
+
}
|
|
377
1089
|
})
|
|
1090
|
+
window.addEventListener('hashchange', function () { applyHash() })
|
|
378
1091
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
1092
|
+
// ---- start ----
|
|
1093
|
+
var desk = add(el('div', 'desk'), topEl, body)
|
|
1094
|
+
app.textContent = ''
|
|
1095
|
+
app.appendChild(desk)
|
|
1096
|
+
if (multi) {
|
|
1097
|
+
var first = commitOrder()[0]
|
|
1098
|
+
state.ci = first
|
|
1099
|
+
state.kind = 'overview'
|
|
382
1100
|
}
|
|
1101
|
+
if (!applyHash()) { renderChrome(); renderPane(false); remember() }
|
|
1102
|
+
booting = false
|
|
383
1103
|
})()
|