~bigbes/sr-ht-dolt

ref: e6125fce57bf084c875d39b68efa434647b9ce3c sr-ht-dolt/beads/events.go -rw-r--r-- 3.7 KiB
e6125fce — Eugene Blikh db: list every database a viewer may see 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package beads

import (
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"
)

// humanizeEvent turns one audit row into a readable summary line (and optional
// body text). status_changed / updated carry a JSON new_value fragment
// ({"status":"in_progress"}, {"priority":0}); created and closed are lifecycle
// markers, with closed's new_value holding the free-text close reason; label
// events keep their whole story in the comment note, so they collapse to a
// single summary line rather than a "label added" header + redundant body.
func humanizeEvent(eventType, oldVal, newVal, note string) (summary, text string) {
	note = strings.TrimSpace(note)
	switch strings.ToLower(strings.TrimSpace(eventType)) {
	case "created":
		return "created the issue", ""
	case "closed":
		// new_value is the close reason (plain text), not JSON; older rows put it
		// in the note instead.
		if r := strings.TrimSpace(newVal); r != "" {
			return "closed the issue", r
		}
		return "closed the issue", note
	case "status_changed":
		if s := jsonField(newVal, "status"); s != "" {
			return "changed status to " + s, ""
		}
		return "changed status", ""
	case "updated":
		if pairs := jsonPairs(newVal); pairs != "" {
			return "updated " + pairs, ""
		}
		return "updated the issue", ""
	case "label_added":
		return labelLine(note, "added"), ""
	case "label_removed":
		return labelLine(note, "removed"), ""
	default:
		et := strings.ReplaceAll(strings.TrimSpace(eventType), "_", " ")
		if et == "" {
			et = "changed"
		}
		return et, note
	}
}

// labelLine collapses a label event to one line. The note reads "Added label:
// <name>"; we drop everything up to the FIRST colon (the "Added label:" prefix)
// and keep the rest, so a namespaced label like "milestone:m3" survives intact
// and the summary becomes "added label milestone:m3".
func labelLine(note, verb string) string {
	name := note
	if i := strings.Index(name, ":"); i >= 0 {
		name = name[i+1:]
	}
	name = strings.TrimSpace(name)
	if name == "" {
		return verb + " a label"
	}
	return verb + " label " + name
}

// jsonField extracts one string-ish field from a JSON object fragment, or ""
// when the value is not a JSON object or the key is absent.
func jsonField(raw, key string) string {
	m := decodeJSONObject(raw)
	if m == nil {
		return ""
	}
	if v, ok := m[key]; ok {
		return scalarString(v)
	}
	return ""
}

// jsonPairs renders a JSON object fragment as "k to v, k2 to v2", used for the
// "updated …" summary. Keys are sorted for a deterministic line.
func jsonPairs(raw string) string {
	m := decodeJSONObject(raw)
	if len(m) == 0 {
		return ""
	}
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, k := range keys {
		parts = append(parts, k+" to "+scalarString(m[k]))
	}
	return strings.Join(parts, ", ")
}

// decodeJSONObject parses raw into a map, tolerating the browse NULL placeholder
// and non-object payloads (returns nil rather than erroring).
func decodeJSONObject(raw string) map[string]any {
	raw = strings.TrimSpace(raw)
	if raw == "" || raw == "NULL" {
		return nil
	}
	var m map[string]any
	if err := json.Unmarshal([]byte(raw), &m); err != nil {
		return nil
	}
	return m
}

// scalarString renders a decoded JSON scalar the way a person would read it:
// integers without a trailing ".0", everything else via fmt.
func scalarString(v any) string {
	switch t := v.(type) {
	case string:
		return t
	case float64:
		if t == float64(int64(t)) {
			return strconv.FormatInt(int64(t), 10)
		}
		return strconv.FormatFloat(t, 'g', -1, 64)
	case bool:
		if t {
			return "true"
		}
		return "false"
	case nil:
		return ""
	default:
		return fmt.Sprintf("%v", t)
	}
}