~bigbes/sr-ht-dolt

ref: 8dfe078bcda61dc7ec82d607468747771d9dfa41 sr-ht-dolt/beads/events.go -rw-r--r-- 3.9 KiB
8dfe078b — Eugene Blikh beads: report a clipped read from the remaining projections 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
136
137
138
139
140
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 an empty value and
// non-object payloads (returns nil rather than erroring).
//
// It used to special-case the string "NULL" as well, back when that string was
// how an absent value reached it. cell answers an absent value as "" now, so a
// "NULL" arriving here is four characters a row actually stores — which is not a
// JSON object, and takes the same nil the parse error gives it.
func decodeJSONObject(raw string) map[string]any {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		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)
	}
}