v0.1.0 1a6c6c9

notes.go RawHistoryFolder

1package main
2
3import (
4	"fmt"
5	"sort"
6	"strings"
7
8	"github.com/bnema/go-wayland-bindings/spec"
9)
10
11// notes is the difference between the vendored spec and a new tree.
12type notes struct {
13	sources   []string // "id: old → new"
14	added     []string // package names
15	removed   []string
16	bumps     []string // "pkg: iface vA → vB"
17	newIfaces []string // "pkg: iface (vN)"
18	updated   []string // packages whose XML bytes changed
19	breaking  []string
20}
21
22func (n *notes) empty() bool {
23	return len(n.sources)+len(n.added)+len(n.removed)+len(n.bumps)+
24		len(n.newIfaces)+len(n.updated)+len(n.breaking) == 0
25}
26
27// parseOld parses the currently vendored XML of every manifest entry.
28func parseOld(s *state) (map[string]*spec.Protocol, error) {
29	out := map[string]*spec.Protocol{}
30	for _, e := range s.manifest.Protocols {
31		data, ok := s.xml[e.XML]
32		if !ok {
33			return nil, fmt.Errorf("current manifest lists %s but spec/xml/%s is missing", e.Package, e.XML)
34		}
35		p, err := spec.Parse(data)
36		if err != nil {
37			return nil, fmt.Errorf("current %s: %w", e.XML, err)
38		}
39		out[e.Package] = p
40	}
41	return out, nil
42}
43
44// diff compares the on-disk state with the desired tree.
45func diff(old *state, oldParsed map[string]*spec.Protocol, t *tree) (*notes, error) {
46	n := &notes{}
47
48	oldSrc := map[string]spec.Source{}
49	for _, s := range old.manifest.Sources {
50		oldSrc[s.ID] = s
51	}
52	newSrc := map[string]spec.Source{}
53	for _, s := range t.manifest.Sources {
54		newSrc[s.ID] = s
55		switch o, ok := oldSrc[s.ID]; {
56		case !ok:
57			n.sources = append(n.sources, fmt.Sprintf("%s: added at %s", s.ID, s.Ref))
58		case o.Ref != s.Ref:
59			n.sources = append(n.sources, fmt.Sprintf("%s: %s → %s", s.ID, o.Ref, s.Ref))
60		case o.Commit != s.Commit:
61			n.sources = append(n.sources, fmt.Sprintf("%s: %s %s → %s", s.ID, s.Ref, short(o.Commit), short(s.Commit)))
62		}
63	}
64	for _, s := range old.manifest.Sources {
65		if _, ok := newSrc[s.ID]; !ok {
66			n.sources = append(n.sources, fmt.Sprintf("%s: removed", s.ID))
67		}
68	}
69
70	oldEntries := map[string]spec.ProtocolEntry{}
71	for _, e := range old.manifest.Protocols {
72		oldEntries[e.Package] = e
73	}
74	newEntries := map[string]spec.ProtocolEntry{}
75	for _, e := range t.manifest.Protocols {
76		newEntries[e.Package] = e
77	}
78
79	for _, e := range t.manifest.Protocols {
80		o, existed := oldEntries[e.Package]
81		if !existed {
82			n.added = append(n.added, e.Package)
83			continue
84		}
85		if o.SHA256 != e.SHA256 {
86			n.updated = append(n.updated, e.Package)
87		}
88		if o.Source != e.Source || o.Path != e.Path {
89			n.breaking = append(n.breaking, fmt.Sprintf("%s: major replacement, %s:%s → %s:%s",
90				e.Package, o.Source, o.Path, e.Source, e.Path))
91		}
92		newP, err := spec.Parse(t.xml[e.XML])
93		if err != nil {
94			return nil, err
95		}
96		diffProtocol(n, e.Package, oldParsed[e.Package], newP)
97	}
98	for _, e := range old.manifest.Protocols {
99		if _, ok := newEntries[e.Package]; !ok {
100			n.removed = append(n.removed, e.Package)
101			n.breaking = append(n.breaking, fmt.Sprintf("%s: package removed", e.Package))
102		}
103	}
104
105	for _, l := range []*[]string{&n.sources, &n.added, &n.removed, &n.bumps, &n.newIfaces, &n.updated, &n.breaking} {
106		sort.Strings(*l)
107	}
108	return n, nil
109}
110
111func short(commit string) string {
112	if len(commit) > 12 {
113		return commit[:12]
114	}
115	return commit
116}
117
118func diffProtocol(n *notes, pkg string, old, cur *spec.Protocol) {
119	oldIf := map[string]spec.Interface{}
120	for _, it := range old.Interfaces {
121		oldIf[it.Name] = it
122	}
123	seen := map[string]bool{}
124	for _, it := range cur.Interfaces {
125		seen[it.Name] = true
126		o, ok := oldIf[it.Name]
127		if !ok {
128			n.newIfaces = append(n.newIfaces, fmt.Sprintf("%s: %s (v%d)", pkg, it.Name, it.Version))
129			continue
130		}
131		if o.Version != it.Version {
132			n.bumps = append(n.bumps, fmt.Sprintf("%s: %s v%d → v%d", pkg, it.Name, o.Version, it.Version))
133			if it.Version < o.Version {
134				n.breaking = append(n.breaking, fmt.Sprintf("%s: interface %s version decreased v%d → v%d", pkg, it.Name, o.Version, it.Version))
135			}
136		}
137		diffEnums(n, pkg, it.Name, o.Enums, it.Enums)
138		diffMessages(n, pkg, it.Name, "request", o.Requests, it.Requests)
139		diffMessages(n, pkg, it.Name, "event", o.Events, it.Events)
140	}
141	for _, it := range old.Interfaces {
142		if !seen[it.Name] {
143			n.breaking = append(n.breaking, fmt.Sprintf("%s: interface %s removed", pkg, it.Name))
144		}
145	}
146}
147
148// diffEnums reports removed enums and removed or renamed enum entries.
149// Entries are matched by name, so a rename shows up as a removal.
150func diffEnums(n *notes, pkg, iface string, old, cur []spec.Enum) {
151	curByName := map[string]spec.Enum{}
152	for _, e := range cur {
153		curByName[e.Name] = e
154	}
155	for _, o := range old {
156		e, ok := curByName[o.Name]
157		if !ok {
158			n.breaking = append(n.breaking, fmt.Sprintf("%s: %s enum %s removed", pkg, iface, o.Name))
159			continue
160		}
161		entries := map[string]bool{}
162		for _, en := range e.Entries {
163			entries[en.Name] = true
164		}
165		for _, en := range o.Entries {
166			if !entries[en.Name] {
167				n.breaking = append(n.breaking, fmt.Sprintf("%s: %s enum %s entry %s removed or renamed", pkg, iface, o.Name, en.Name))
168			}
169		}
170	}
171}
172
173func diffMessages(n *notes, pkg, iface, kind string, old, cur []spec.Message) {
174	curByName := map[string]spec.Message{}
175	for _, m := range cur {
176		curByName[m.Name] = m
177	}
178	for _, o := range old {
179		m, ok := curByName[o.Name]
180		if !ok {
181			n.breaking = append(n.breaking, fmt.Sprintf("%s: %s %s %s removed", pkg, iface, kind, o.Name))
182			continue
183		}
184		if (o.Type == "destructor") != (m.Type == "destructor") {
185			n.breaking = append(n.breaking, fmt.Sprintf("%s: %s %s %s destructor flag changed: %t → %t",
186				pkg, iface, kind, o.Name, o.Type == "destructor", m.Type == "destructor"))
187		}
188		if change := argsChange(o.Args, m.Args); change != "" {
189			n.breaking = append(n.breaking, fmt.Sprintf("%s: %s %s %s arguments changed: %s", pkg, iface, kind, o.Name, change))
190		}
191	}
192}
193
194// argsChange describes the differences between two argument lists, comparing
195// name, type, interface, enum and allow-null by position. It returns "" when equal.
196func argsChange(old, cur []spec.Arg) string {
197	if len(old) != len(cur) {
198		return fmt.Sprintf("%d → %d arguments", len(old), len(cur))
199	}
200	var diffs []string
201	for i := range old {
202		o, c := old[i], cur[i]
203		if o.Name != c.Name || o.Type != c.Type || o.Interface != c.Interface ||
204			o.Enum != c.Enum || o.AllowNull != c.AllowNull {
205			diffs = append(diffs, fmt.Sprintf("#%d %s → %s", i+1, argString(o), argString(c)))
206		}
207	}
208	return strings.Join(diffs, "; ")
209}
210
211func argString(a spec.Arg) string {
212	s := a.Name + ":" + a.Type
213	if a.Interface != "" {
214		s += "<" + a.Interface + ">"
215	}
216	if a.Enum != "" {
217		s += " enum=" + a.Enum
218	}
219	if a.AllowNull {
220		s += " allow-null"
221	}
222	return s
223}
224
225// markdown renders the release notes.
226func (n *notes) markdown() string {
227	var b strings.Builder
228	b.WriteString("# Upstream sync\n\n")
229	if n.empty() {
230		b.WriteString("No changes.\n")
231		return b.String()
232	}
233	section := func(title string, items []string, code bool) {
234		if len(items) == 0 {
235			return
236		}
237		fmt.Fprintf(&b, "## %s\n\n", title)
238		for _, it := range items {
239			if code {
240				fmt.Fprintf(&b, "- `%s`\n", it)
241			} else {
242				fmt.Fprintf(&b, "- %s\n", it)
243			}
244		}
245		b.WriteString("\n")
246	}
247	section("BREAKING", n.breaking, false)
248	section("Sources", n.sources, false)
249	section("Added packages", n.added, true)
250	section("Removed packages", n.removed, true)
251	section("New interfaces", n.newIfaces, false)
252	section("Interface version bumps", n.bumps, false)
253	section("Updated XML", n.updated, true)
254	return strings.TrimRight(b.String(), "\n") + "\n"
255}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close