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 := ¬es{}
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}