1package diff23import (4 "encoding/json"5 "maps"6 "slices"7 "strconv"89 "src.rest/cat/012/internal/sheet"10)1112// Conflict is a part of a workbook both sides changed differently. The13// merge keeps ours; the report says what theirs had.14type Conflict struct {15 Where string // as a Change's: Q3!B7, sheet Q3, Q3 widths, name Sales16 Field string // what about it: input, format, note, a field's key, or ""17 // Base, Ours and Theirs are the three versions as text, "" where18 // the thing isn't there; What says what happened instead, when the19 // conflict is about a whole sheet.20 Base, Ours, Theirs string21 What string22}2324// String is the conflict as the merge report lists it.25func (c Conflict) String() string {26 s := c.Where27 if c.Field != "" {28 s += " " + c.Field29 }30 if c.What != "" {31 return s + ": " + c.What32 }33 return s + ": ours " + side3(c.Ours) + ", theirs " + side3(c.Theirs) + ", base " + side3(c.Base)34}3536func side3(s string) string {37 if s == "" {38 return "(none)"39 }40 return quoted(s)41}4243// Merge combines ours and theirs, two workbook files changed from base,44// and returns the merged file as 012 saves it. Each cell's input, format45// and note, each field of a sheet or the workbook (a column's width, a46// region, a named range, a macro) and each sheet's name and presence47// merge on their own: a part changed on one side only takes that side's48// version, and a part both changed alike takes it too. A part both49// changed differently is a conflict: the merge keeps ours, and a cell's50// note says what theirs had (on top of its own note), so the conflicts51// can be found in the grid. Empty contents stand for a missing file.52//53// Sheets are matched by name or, when renamed, by their cells (see54// matchSheets). Rows or columns inserted on one side shift the cells55// below or right of them, which a cell-by-cell merge can't tell from56// edits: merge such changes in 012 rather than here.57func Merge(base, ours, theirs []byte) ([]byte, []Conflict, error) {58 var books [3]*rawBook59 for i, data := range [][]byte{base, ours, theirs} {60 b, err := parseRaw(data)61 if err != nil {62 return nil, nil, err63 }64 books[i] = b65 }66 m := &merger{}67 merged := m.books(books[0], books[1], books[2])68 if len(merged.sheets) == 0 {69 return nil, m.conflicts, nil70 }71 out, err := normalize(merged)72 return out, m.conflicts, err73}7475// merger collects the conflicts of a merge.76type merger struct {77 conflicts []Conflict78}7980func (m *merger) conflict(where, field string, b, o, t json.RawMessage) {81 m.conflicts = append(m.conflicts, Conflict{Where: where, Field: field, Base: rawText(b), Ours: rawText(o), Theirs: rawText(t)})82}8384// said records a conflict that isn't between three values, in words.85func (m *merger) said(where, field, what string) {86 m.conflicts = append(m.conflicts, Conflict{Where: where, Field: field, What: what})87}8889// merge3 merges one value: what changed on one side, or both alike.90func merge3(b, o, t json.RawMessage) (json.RawMessage, bool) {91 switch {92 case equal(o, t), equal(b, t):93 return o, false94 case equal(b, o):95 return t, false96 }97 return o, true98}99100// value merges one value, reporting a conflict at where.101func (m *merger) value(where, field string, b, o, t json.RawMessage) json.RawMessage {102 v, clash := merge3(b, o, t)103 if clash {104 m.conflict(where, field, b, o, t)105 }106 return v107}108109// books merges the workbooks' fields, then their sheets.110func (m *merger) books(b, o, t *rawBook) *rawBook {111 out := &rawBook{top: map[string]json.RawMessage{}}112 for _, k := range unionKeys3(b.top, o.top, t.top) {113 var v json.RawMessage114 switch k {115 case "version":116 v = maxVersion(b.top[k], o.top[k], t.top[k])117 case "names":118 v = m.object(b.top[k], o.top[k], t.top[k], func(key string) (string, string) { return "name " + key, "" })119 case "macros":120 v = m.list(b.top[k], o.top[k], t.top[k], func(key string) string { return "macro " + key })121 case "active", "shellHistory", "macroOrigin":122 v, _ = merge3(b.top[k], o.top[k], t.top[k]) // what's shown or was typed: ours if both differ123 default:124 v = m.value("workbook "+k, "", b.top[k], o.top[k], t.top[k])125 }126 if v != nil {127 out.top[k] = v128 }129 }130 out.sheets = m.sheets(b, o, t)131 if n, err := strconv.Atoi(string(out.top["active"])); err == nil && n >= len(out.sheets) {132 delete(out.top, "active") // the sheet shown went: show the first133 }134 trust(out, o, t)135 return out136}137138// trust keeps the computer ours trusted its commands on only when the139// merge's commands (macros, notebook cells and linked files) are ours',140// or theirs were trusted on the same computer, so a merge never makes141// someone else's commands run without asking.142func trust(out, o, t *rawBook) {143 if equal(o.top["macroOrigin"], t.top["macroOrigin"]) || code(out) == code(o) {144 if origin := o.top["macroOrigin"]; origin != nil {145 out.top["macroOrigin"] = origin146 }147 return148 }149 delete(out.top, "macroOrigin")150}151152// code is what a workbook can make 012 run or read: its macros, its153// notebooks' cells and the files it links, in one string to compare.154func code(b *rawBook) string {155 var parts []string156 parts = append(parts, string(b.top["macros"]))157 for _, s := range b.sheets {158 parts = append(parts, string(sources(s.fields["notebookCells"])))159 for _, r := range named(s.fields["regions"]) {160 f, _ := object(r)161 parts = append(parts, string(f["command"])+string(f["path"])+string(f["table"])+string(f["query"]))162 }163 }164 slices.Sort(parts)165 raw, _ := json.Marshal(parts)166 return string(raw)167}168169func maxVersion(vs ...json.RawMessage) json.RawMessage {170 best, out := 0, json.RawMessage(nil)171 for _, v := range vs {172 if n, err := strconv.Atoi(string(v)); err == nil && n > best {173 best, out = n, v174 }175 }176 return out177}178179// object merges an object key by key (named ranges, column widths);180// where names each key's place and field in conflicts.181func (m *merger) object(b, o, t json.RawMessage, where func(key string) (string, string)) json.RawMessage {182 ob, okB := object(b)183 oo, okO := object(o)184 ot, okT := object(t)185 if !okB || !okO || !okT {186 at, _ := where("")187 return m.value(at, "", b, o, t)188 }189 out := map[string]json.RawMessage{}190 for _, k := range unionKeys3(ob, oo, ot) {191 at, field := where(k)192 if v := m.value(at, field, ob[k], oo[k], ot[k]); v != nil {193 out[k] = v194 }195 }196 if len(out) == 0 && o == nil && t == nil {197 return nil198 }199 raw, _ := json.Marshal(out)200 return raw201}202203// list merges a list of things with names (regions, macros) thing by204// thing, in ours' order with theirs' additions after.205func (m *merger) list(b, o, t json.RawMessage, where func(string) string) json.RawMessage {206 lb, lo, lt := named(b), named(o), named(t)207 var order []string208 for _, raw := range []json.RawMessage{o, t} {209 var items []struct{ Name string }210 json.Unmarshal(raw, &items)211 for _, it := range items {212 if !slices.Contains(order, it.Name) {213 order = append(order, it.Name)214 }215 }216 }217 var out []json.RawMessage218 for _, k := range order {219 if v := m.value(where(k), "", lb[k], lo[k], lt[k]); v != nil {220 out = append(out, v)221 }222 }223 if len(out) == 0 {224 return nil225 }226 raw, _ := json.Marshal(out)227 return raw228}229230// cell merges a cell's input, format and note apart. Where they231// conflict, ours stays and the note says what theirs had.232func (m *merger) cell(where string, b, o, t json.RawMessage) json.RawMessage {233 pb, po, pt := splitCell(b), splitCell(o), splitCell(t)234 str := func(s string) json.RawMessage {235 if s == "" {236 return nil237 }238 raw, _ := json.Marshal(s)239 return raw240 }241 out := cellParts{}242 var said []string243 part := func(field string, b, o, t json.RawMessage) json.RawMessage {244 v, clash := merge3(b, o, t)245 if clash {246 text := rawText247 if field == "format" {248 text = formatText249 }250 m.conflicts = append(m.conflicts, Conflict{Where: where, Field: field, Base: text(b), Ours: text(o), Theirs: text(t)})251 said = append(said, field+" "+side3(text(t)))252 }253 return v254 }255 json.Unmarshal(part("input", str(pb.input), str(po.input), str(pt.input)), &out.input)256 out.format = part("format", pb.format, po.format, pt.format)257 json.Unmarshal(part("note", str(pb.note), str(po.note), str(pt.note)), &out.note)258 if len(said) > 0 {259 out.note = conflictNote(out.note, said)260 }261 return joinCell(out)262}263264// conflictNote is a cell's note with what theirs had added below it.265func conflictNote(note string, said []string) string {266 text := "Merge conflict, kept ours; theirs had "267 for i, s := range said {268 if i > 0 {269 text += "; "270 }271 text += s272 }273 if note != "" {274 text = note + "\n\n" + text275 }276 return sheet.CleanNote(text)277}278279func unionKeys3(a, b, c map[string]json.RawMessage) []string {280 keys := map[string]bool{}281 for _, m := range []map[string]json.RawMessage{a, b, c} {282 for k := range m {283 keys[k] = true284 }285 }286 return slices.Sorted(maps.Keys(keys))287}