recovery.go RawHistoryFolder

1package domain
2
3import (
4	"encoding/hex"
5	"errors"
6	"fmt"
7	"io"
8	"slices"
9	"strings"
10	"unicode/utf8"
11)
12
13// IncarnationID permanently distinguishes uses of the same session name.
14type IncarnationID [16]byte
15
16// CheckpointRef identifies one immutable snapshot manifest.
17type CheckpointRef struct {
18	Generation     uint64
19	ManifestDigest [32]byte
20}
21
22// CatalogueTabRecord is the durable ordered metadata for one stopped tab.
23// StableID is empty for records migrated from the legacy TabNames-only format.
24type CatalogueTabRecord struct {
25	StableID TabStableID
26	Name     string
27}
28
29// CatalogueRecord is the durable source of truth for one named session.
30type CatalogueRecord struct {
31	Name          string
32	IncarnationID IncarnationID
33	Cwd           string
34	CreatedAt     int64
35	UpdatedAt     int64
36	LastUsedSeq   uint64
37	// TabNames remains the compatibility metadata consumed by older recovery
38	// paths. TabRecords is authoritative when present and retains trailing
39	// unnamed tabs instead of silently trimming them.
40	TabNames       []string
41	TabRecords     []CatalogueTabRecord
42	Committed      *CheckpointRef
43	DegradedReason string
44}
45
46// Equal reports canonical value equality between checkpoint references.
47func (r *CheckpointRef) Equal(other *CheckpointRef) bool {
48	if r == nil || other == nil {
49		return r == nil && other == nil
50	}
51	return *r == *other
52}
53
54// Equal reports canonical value equality, including ordered tab names and
55// pointed-to checkpoint values rather than pointer identity.
56func (r CatalogueRecord) Equal(other CatalogueRecord) bool {
57	return r.Name == other.Name && r.IncarnationID == other.IncarnationID && r.Cwd == other.Cwd &&
58		r.CreatedAt == other.CreatedAt && r.UpdatedAt == other.UpdatedAt && r.LastUsedSeq == other.LastUsedSeq &&
59		slices.Equal(r.TabNames, other.TabNames) && slices.Equal(r.TabRecords, other.TabRecords) && r.Committed.Equal(other.Committed) &&
60		r.DegradedReason == other.DegradedReason
61}
62
63// CatalogueMetadataUpdate changes mutable runtime metadata for one catalogue
64// incarnation without carrying authority-owned recovery or checkpoint fields.
65type CatalogueMetadataUpdate struct {
66	Name          string
67	IncarnationID IncarnationID
68	Cwd           *string
69	UpdatedAt     *int64
70	LastUsedSeq   *uint64
71	TabNames      *[]string
72	TabRecords    *[]CatalogueTabRecord
73}
74
75// MetadataUpdate returns a complete mutable-metadata update for r. Catalogue
76// implementations retain all recovery and checkpoint fields they own.
77func (r CatalogueRecord) MetadataUpdate() CatalogueMetadataUpdate {
78	cwd, updatedAt, lastUsedSeq := r.Cwd, r.UpdatedAt, r.LastUsedSeq
79	tabNames := append([]string(nil), r.TabNames...)
80	tabRecords := append([]CatalogueTabRecord(nil), r.TabRecords...)
81	return CatalogueMetadataUpdate{
82		Name: r.Name, IncarnationID: r.IncarnationID,
83		Cwd: &cwd, UpdatedAt: &updatedAt, LastUsedSeq: &lastUsedSeq, TabNames: &tabNames, TabRecords: &tabRecords,
84	}
85}
86
87// NewIncarnationID reads a new non-zero identity from r.
88func NewIncarnationID(r io.Reader) (IncarnationID, error) {
89	var id IncarnationID
90	if _, err := io.ReadFull(r, id[:]); err != nil {
91		return id, err
92	}
93	if id == (IncarnationID{}) {
94		return id, errors.New("zero incarnation ID")
95	}
96	return id, nil
97}
98
99func (id IncarnationID) String() string {
100	return hex.EncodeToString(id[:])
101}
102
103// MarshalText emits the canonical lowercase hexadecimal identity.
104func (id IncarnationID) MarshalText() ([]byte, error) {
105	if id == (IncarnationID{}) {
106		return nil, errors.New("zero incarnation ID")
107	}
108	return []byte(id.String()), nil
109}
110
111// UnmarshalText accepts only a canonical lowercase non-zero identity.
112func (id *IncarnationID) UnmarshalText(text []byte) error {
113	if len(text) != 32 || strings.ToLower(string(text)) != string(text) {
114		return errors.New("invalid incarnation ID")
115	}
116	var decoded IncarnationID
117	if _, err := hex.Decode(decoded[:], text); err != nil {
118		return fmt.Errorf("invalid incarnation ID: %w", err)
119	}
120	if decoded == (IncarnationID{}) {
121		return errors.New("zero incarnation ID")
122	}
123	*id = decoded
124	return nil
125}
126
127// Validate rejects catalogue states that cannot be restored deterministically.
128func (r CatalogueRecord) Validate() error {
129	if err := ValidateSessionName(r.Name); err != nil {
130		return err
131	}
132	if r.IncarnationID == (IncarnationID{}) {
133		return errors.New("zero catalogue incarnation ID")
134	}
135
136	for _, tab := range r.TabRecords {
137		if tab.StableID != "" {
138			if err := ValidateTabStableID(tab.StableID); err != nil {
139				return errors.New("invalid catalogue tab metadata")
140			}
141		}
142		if !utf8.ValidString(tab.Name) {
143			return errors.New("invalid catalogue tab metadata")
144		}
145	}
146	if r.DegradedReason != "" && r.Committed == nil {
147		return errors.New("broken session has no committed checkpoint")
148	}
149	if r.Committed != nil {
150		if r.Committed.Generation == 0 {
151			return errors.New("zero checkpoint generation")
152		}
153		if r.Committed.ManifestDigest == ([32]byte{}) {
154			return errors.New("zero checkpoint manifest digest")
155		}
156	}
157	return nil
158}

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 12

Tags 11

↑↓ moveenter openesc close