navigation_inventory.go RawHistoryFolder

1package daemon
2
3import (
4	"encoding/hex"
5
6	"github.com/bnema/vev/internal/domain"
7	"github.com/bnema/vev/internal/ports"
8	"github.com/bnema/vev/internal/protocol"
9)
10
11// navigationInventoryEntryKey derives a stable opaque key for an unchanged
12// private exact target (authority, lifecycle, name). Presentation-only
13// updates keep the key; a changed or removed target retires it and a
14// reappearing target gets a new key through its new lifecycle.
15func navigationInventoryEntryKey(lifecycle domain.SessionLifecycleID, name string) string {
16	return hex.EncodeToString(lifecycle[:]) + "/" + name
17}
18
19// handleNavigationInventory answers a one-shot source-control query without a
20// Hello session, attachment, startup, PTY, or geometry effect. It closes the
21// exact connection on every exit so blocked operations unwind.
22func (d *Daemon) handleNavigationInventory(tr ports.ServerConnection, request protocol.NavigationInventoryRequest) {
23	defer func() { _ = tr.Close() }()
24	response := d.answerNavigationInventory(request)
25	_ = d.boundedControlSend(tr, response)
26}
27
28// answerNavigationInventory builds the typed response for a validated or
29// rejected inventory request.
30func (d *Daemon) answerNavigationInventory(request protocol.NavigationInventoryRequest) protocol.NavigationInventoryResponse {
31	if err := protocol.ValidateNavigationInventoryRequest(request); err != nil {
32		return protocol.NavigationInventoryResponse{RequestID: request.RequestID, Operation: request.Operation, Status: protocol.NavigationInventoryInvalid}
33	}
34	if request.Version != protocol.Version {
35		return protocol.NavigationInventoryResponse{RequestID: request.RequestID, Operation: request.Operation, Status: protocol.NavigationInventoryVersionMismatch}
36	}
37	if request.Operation == protocol.NavigationInventoryResolve {
38		return d.resolveNavigationInventory(request)
39	}
40	return d.snapshotNavigationInventory(request.RequestID)
41}
42
43// snapshotNavigationInventory answers with this daemon's own sessions only:
44// other daemons are observed by the client's broker, never relayed here.
45func (d *Daemon) snapshotNavigationInventory(requestID uint64) protocol.NavigationInventoryResponse {
46	return d.localNavigationInventorySnapshot(requestID)
47}
48
49// localNavigationInventorySnapshot is the prepared local-only navigation
50// projection: exactly one local source group, built from the
51// daemon's own capture.
52func (d *Daemon) localNavigationInventorySnapshot(requestID uint64) protocol.NavigationInventoryResponse {
53	inv := d.captureLocalSessionInventory(viewOptions{}, false)
54	response := protocol.NavigationInventoryResponse{
55		RequestID: requestID, Operation: protocol.NavigationInventorySnapshot, Status: protocol.NavigationInventoryOK,
56		Groups: []protocol.NavigationInventorySourceGroup{d.localInventoryGroup(inv)},
57	}
58	return fitNavigationInventoryResponse(response)
59}
60
61// localInventoryGroup projects live named sessions and resumable stopped
62// sessions. Observation failures do not exist for local state; broken and
63// purging records are excluded here because they are never resumable. It reads
64// only the remote-free local capture.
65func (d *Daemon) localInventoryGroup(inv localSessionInventory) protocol.NavigationInventorySourceGroup {
66	group := protocol.NavigationInventorySourceGroup{SourceKey: protocol.NavigationInventoryLocalSourceKey, Status: protocol.NavigationInventorySourceOK}
67	for _, item := range inv.live {
68		view := item.view
69		if view.name == "" || view.ephemeral {
70			continue
71		}
72		group.Entries = append(group.Entries, protocol.NavigationInventoryEntry{
73			SourceKey: group.SourceKey,
74			EntryKey:  navigationInventoryEntryKey(view.incarnation, view.name),
75			Name:      view.name,
76			State:     "up",
77		})
78	}
79	for _, entry := range inv.resumableStopped() {
80		group.Entries = append(group.Entries, protocol.NavigationInventoryEntry{
81			SourceKey: group.SourceKey,
82			EntryKey:  navigationInventoryEntryKey(entry.incarnation, entry.name),
83			Name:      entry.name,
84			State:     "stopped",
85		})
86	}
87	if len(group.Entries) > protocol.NavigationInventoryMaxLocalEntries {
88		return protocol.NavigationInventorySourceGroup{SourceKey: group.SourceKey, Status: protocol.NavigationInventorySourceTooLarge}
89	}
90	return group
91}
92
93// navigationInventoryEncodedBudget mirrors the wire export budget without
94// importing the codec layer. The daemon never encodes frames itself; the
95// estimate below upper-bounds the real payload so fit degrades early rather
96// than emitting an oversized frame.
97const navigationInventoryEncodedBudget = 4 << 20
98
99func navigationInventorySnapshotSize(response protocol.NavigationInventoryResponse) int {
100	size := 10 + 2
101	for _, group := range response.Groups {
102		size += 2 + len(group.SourceKey) + 1 + 4
103		for _, entry := range group.Entries {
104			size += 2 + len(entry.EntryKey) + 2 + len(entry.Name) + 2 + len(entry.DisplayOrigin) + 2 + len(entry.State) + 2 + len(entry.Reason)
105		}
106	}
107	return size
108}
109
110// fitNavigationInventoryResponse degrades the largest complete group to
111// status-only until the response fits the encoded budget. It never emits a
112// silently truncated group or a claim of complete inventory.
113func fitNavigationInventoryResponse(response protocol.NavigationInventoryResponse) protocol.NavigationInventoryResponse {
114	if protocol.ValidateNavigationInventoryResponse(response) != nil {
115		response.Groups = nil
116		response.Status = protocol.NavigationInventoryInvalid
117		return response
118	}
119	for range len(response.Groups) + 1 {
120		if navigationInventorySnapshotSize(response) <= navigationInventoryEncodedBudget {
121			return response
122		}
123		largest := -1
124		for i, group := range response.Groups {
125			if group.Status != protocol.NavigationInventorySourceOK || len(group.Entries) == 0 {
126				continue
127			}
128			if largest == -1 || len(group.Entries) > len(response.Groups[largest].Entries) {
129				largest = i
130			}
131		}
132		if largest == -1 {
133			response.Groups = nil
134			response.Status = protocol.NavigationInventoryUnavailable
135			return response
136		}
137		source := response.Groups[largest].SourceKey
138		response.Groups[largest] = protocol.NavigationInventorySourceGroup{SourceKey: source, Status: protocol.NavigationInventorySourceTooLarge}
139	}
140	response.Groups = nil
141	response.Status = protocol.NavigationInventoryUnavailable
142	return response
143}
144
145// resolveNavigationInventory revalidates the selected key against a fresh
146// capture and returns a non-mutating AttachTarget. Registration is checked at
147// resolve: a removed or re-registered source rejects without attaching a
148// same-name replacement.
149func (d *Daemon) resolveNavigationInventory(request protocol.NavigationInventoryRequest) protocol.NavigationInventoryResponse {
150	response := protocol.NavigationInventoryResponse{RequestID: request.RequestID, Operation: protocol.NavigationInventoryResolve}
151	if request.SourceKey == protocol.NavigationInventoryLocalSourceKey {
152		target, ok := d.localNavigationResolve(request)
153		if !ok {
154			response.Status = protocol.NavigationInventoryUnavailable
155			return response
156		}
157		response.Status = protocol.NavigationInventoryOK
158		resolved := protocol.AttachTarget{Session: target.SessionName, Intent: protocol.IntentAttach, ExactTarget: &target}
159		response.Resolved = &resolved
160		return response
161	}
162	// Remote sources are the client broker's; this daemon resolves only its
163	// own sessions.
164	response.Status = protocol.NavigationInventoryUnavailable
165	return response
166}
167
168// localNavigationResolve is the prepared local-only navigation resolver
169// It rejects a structured foreign source key or registration
170// before any lookup, so a local resolve can never be satisfied by a remote
171// endpoint or a foreign registration even when a local session shares the
172// row's name. Matching is by the opaque lifecycle-qualified key only: a stale
173// or replaced lifecycle never falls back to a same-name session.
174func (d *Daemon) localNavigationResolve(request protocol.NavigationInventoryRequest) (protocol.ExactSessionTarget, bool) {
175	if request.SourceKey != protocol.NavigationInventoryLocalSourceKey || !request.Registration.IsZero() {
176		return protocol.ExactSessionTarget{}, false
177	}
178	return localNavigationResolveEntry(d.captureLocalSessionInventory(viewOptions{}, false), request.EntryKey)
179}
180
181// localNavigationResolveEntry matches the opaque key against fresh live and
182// resumable stopped state. A stopped target that is now live with the same
183// lifecycle and selector stays attachable; a broken or purging record is not
184// resumable and rejects; a changed lifecycle rejects.
185func localNavigationResolveEntry(inv localSessionInventory, entryKey string) (protocol.ExactSessionTarget, bool) {
186	for _, item := range inv.live {
187		view := item.view
188		if view.name == "" || view.ephemeral {
189			continue
190		}
191		if navigationInventoryEntryKey(view.incarnation, view.name) != entryKey {
192			continue
193		}
194		return protocol.ExactSessionTarget{LifecycleID: view.incarnation, SessionName: view.name}, true
195	}
196	for _, entry := range inv.resumableStopped() {
197		if navigationInventoryEntryKey(entry.incarnation, entry.name) != entryKey {
198			continue
199		}
200		return protocol.ExactSessionTarget{LifecycleID: entry.incarnation, SessionName: entry.name}, true
201	}
202	return protocol.ExactSessionTarget{}, false
203}

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