v0.2.0 80c61e3

move.go RawHistoryFolder

1package daemon
2
3import (
4	"reflect"
5
6	"github.com/bnema/vev/internal/domain"
7)
8
9// movePaneRequest identifies both lifecycles and both topology objects by
10// immutable identity. Session names are advisory; the ID and incarnation are
11// the commit-time authority.
12type movePaneRequest struct {
13	Attachment       *attachedClient
14	AttachmentToken  attachmentConnectionToken
15	Source           moveSessionLocator
16	SourceTabID      domain.TabStableID
17	SourcePaneID     domain.PaneStableID
18	Destination      moveSessionLocator
19	DestinationTabID domain.TabStableID
20}
21
22type moveTabRequest struct {
23	Attachment      *attachedClient
24	AttachmentToken attachmentConnectionToken
25	Source          moveSessionLocator
26	SourceTabID     domain.TabStableID
27	Destination     moveSessionLocator
28}
29
30// movePane resolves, reserves, validates, and commits one pane relocation. All
31// fallible work happens before the membership publication section. Once the
32// section starts, the caller holds the ordered architecture locks and only
33// non-failing in-memory writes are permitted.
34func (d *Daemon) movePane(req movePaneRequest) (result error) {
35	defer func() { result = normalizeMoveRejection(result) }()
36	if d == nil || req.Source.ID == "" || req.Destination.ID == "" ||
37		req.SourceTabID == "" || req.SourcePaneID == "" || req.DestinationTabID == "" {
38		return errMovePaneInvalid
39	}
40
41	d.mu.Lock()
42	source := moveSessionForLocatorLocked(d, req.Source)
43	destination := moveSessionForLocatorLocked(d, req.Destination)
44	d.mu.Unlock()
45	if source == nil || destination == nil {
46		return errMoveStaleTarget
47	}
48
49	// Dispatch admission must precede lifecycle reservation. A final close holds
50	// dispatchMu while it waits for teardown ownership; reserving first would let
51	// that close wait on a move which is itself blocked on dispatchMu.
52	if d.beforeMoveDispatch != nil {
53		d.beforeMoveDispatch()
54	}
55	unlockDispatch := lockMoveDispatch(source, destination)
56	dispatchHeld := true
57	defer func() {
58		if dispatchHeld {
59			unlockDispatch()
60		}
61	}()
62
63	reservation, err := d.reserveMoveLifecycles(source, destination)
64	if err != nil {
65		return errMoveStaleTarget
66	}
67	reservationHeld := true
68	defer func() {
69		if reservationHeld {
70			reservation.Release()
71		}
72	}()
73	if d.afterMoveLifecycleReserved != nil {
74		d.afterMoveLifecycleReserved()
75	}
76
77	// Snapshot the exact live objects and generations before waiting on resize
78	// fences. The locked commit repeats every authority check before publication.
79	admission, err := d.snapshotMovePaneAdmission(req, source, destination)
80	if err != nil {
81		return err
82	}
83	sourceTab := admission.sourceTab
84	destinationTab := admission.destinationTab
85	movedPane := admission.movedPane
86	sourceTabInitialGeneration := admission.sourceGeneration
87	destinationTabInitialGeneration := admission.destinationGeneration
88	finalSourceTab := admission.finalSourceTab
89	if d.afterMovePaneSourceSnapshot != nil {
90		d.afterMovePaneSourceSnapshot()
91	}
92
93	// A final source tab is retired without transferring any attachment to the
94	// destination. Freeze every affected source connection before publication so
95	// teardown invalidates exact generations rather than racing input or render.
96	var frozen frozenAttachmentEffectGates
97	var sourceEffectsFrozen bool
98	if finalSourceTab && source != destination && len(admission.sourceAttachments) != 0 {
99		interrupts := make([]attachmentTransportInterrupt, 0, len(admission.sourceAttachments))
100		for _, ac := range admission.sourceAttachments {
101			if transport := admission.sourceTransports[ac]; transport.transport != nil {
102				interrupts = append(interrupts, attachmentTransportInterrupt{ac: ac, transport: transport})
103			}
104		}
105		frozen = freezeAttachmentEffectGatesWith(attachmentEffectFreezeOptions{interrupts: interrupts, nonblocking: true, afterFrozen: func(ac *attachedClient) {
106			if d.afterAttachmentEffectGateFrozen != nil {
107				d.afterAttachmentEffectGateFrozen("move-pane", ac)
108			}
109		}}, admission.sourceAttachments...)
110		if !frozen.acquired || !frozen.drained {
111			frozen.unfreeze()
112			return errMovePaneInvalid
113		}
114		sourceEffectsFrozen = true
115	}
116	if sourceEffectsFrozen {
117		defer func() {
118			if sourceEffectsFrozen {
119				frozen.unfreeze()
120			}
121		}()
122	}
123
124	commit := movePaneCommit{
125		req:                   req,
126		source:                source,
127		destination:           destination,
128		sourceTab:             sourceTab,
129		destinationTab:        destinationTab,
130		movedPane:             movedPane,
131		sourceAttachments:     admission.sourceAttachments,
132		sourceTransports:      admission.sourceTransports,
133		sourceGeneration:      sourceTabInitialGeneration,
134		destinationGeneration: destinationTabInitialGeneration,
135		frozenEffects:         frozen,
136	}
137	fences := newMovePaneResizeFences(source, destination, sourceTab, destinationTab, movedPane)
138	if !fences.acquire(func() bool { return commit.publishLocked(d) }) {
139		if commit.err != nil {
140			return commit.err
141		}
142		return errMoveStaleTarget
143	}
144	// Owner/layout fences end at the atomic publication boundary. The immutable
145	// postcommit plan owns every later cleanup and effect.
146	fences.Release()
147	postcommit := movePanePostcommitPlan{
148		source:                   source,
149		destination:              destination,
150		sourceTab:                sourceTab,
151		destinationTab:           destinationTab,
152		movedPane:                movedPane,
153		sourceAttachments:        admission.sourceAttachments,
154		syncCleanup:              commit.syncCleanup,
155		frozenEffects:            frozen,
156		effectsFrozen:            sourceEffectsFrozen,
157		unlockDispatch:           unlockDispatch,
158		reservation:              reservation,
159		sourceTabRemoved:         commit.sourceTabRemoved,
160		sourceEmpty:              commit.sourceEmpty,
161		retiredParked:            commit.retiredParked,
162		retiredAttachments:       commit.retiredAttachments,
163		sourceMetadata:           commit.sourceMetadata,
164		sourceMetadataValid:      commit.sourceMetadataValid,
165		destinationMetadata:      commit.destinationMetadata,
166		destinationMetadataValid: commit.destinationMetadataValid,
167	}
168	// Transfer cleanup ownership away from the admission defers only after the
169	// complete postcommit plan exists.
170	sourceEffectsFrozen = false
171	dispatchHeld = false
172	reservationHeld = false
173	postcommit.execute(d)
174	return nil
175}
176
177// moveSessionForLocatorLocked resolves only live sessions. The name is
178// intentionally ignored after the stable ID/incarnation pair is checked.
179func moveSessionForLocatorLocked(d *Daemon, locator moveSessionLocator) *session {
180	if d == nil {
181		return nil
182	}
183	sess := d.sessions[locator.ID]
184	if sess == nil || sess.incarnation != locator.Incarnation {
185		return nil
186	}
187	return sess
188}
189
190func moveSessionLocatorCurrentLocked(sess *session, locator moveSessionLocator) bool {
191	return sess != nil && sess.id == locator.ID && sess.incarnation == locator.Incarnation
192}
193
194func findMoveTabLocked(sess *session, stableID domain.TabStableID) *tab {
195	if sess == nil {
196		return nil
197	}
198	for _, tb := range sess.tabs {
199		if tb != nil && tb.stableID == string(stableID) {
200			return tb
201		}
202	}
203	return nil
204}
205
206func indexMoveTabLocked(sess *session, target *tab) int {
207	for i, tb := range sess.tabs {
208		if tb == target {
209			return i
210		}
211	}
212	return -1
213}
214
215func moveTabMemberLocked(sess *session, target *tab) bool {
216	return indexMoveTabLocked(sess, target) >= 0
217}
218
219func lockMoveTabs(a, b *tab) func() {
220	if a == nil && b == nil {
221		return func() {}
222	}
223	if a == nil {
224		b.mu.Lock()
225		return b.mu.Unlock
226	}
227	if b == nil {
228		a.mu.Lock()
229		return a.mu.Unlock
230	}
231	if a == b {
232		a.mu.Lock()
233		return a.mu.Unlock
234	}
235	first, second := a, b
236	if first.stableID > second.stableID ||
237		(first.stableID == second.stableID && reflect.ValueOf(first).Pointer() > reflect.ValueOf(second).Pointer()) {
238		first, second = second, first
239	}
240	first.mu.Lock()
241	second.mu.Lock()
242	return func() {
243		second.mu.Unlock()
244		first.mu.Unlock()
245	}
246}
247
248func lockMoveDispatch(a, b *session) func() {
249	if a == b {
250		a.dispatchMu.Lock()
251		return a.dispatchMu.Unlock
252	}
253	first, second := a, b
254	if first.id > second.id {
255		first, second = second, first
256	}
257	first.dispatchMu.Lock()
258	second.dispatchMu.Lock()
259	return func() {
260		second.dispatchMu.Unlock()
261		first.dispatchMu.Unlock()
262	}
263}

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