move_lifecycle.go RawHistoryFolder

1package daemon
2
3import (
4	"cmp"
5	"errors"
6	"slices"
7	"sync"
8)
9
10var errMoveLifecycleUnavailable = errors.New("move lifecycle is no longer available")
11
12// moveLifecycleReservation pins a unique, canonically ordered set of sessions
13// plus the daemon move gate. Release is idempotent so every abort path can use
14// defer without coordinating ownership with its caller.
15type moveLifecycleReservation struct {
16	daemon   *Daemon
17	sessions []*session
18	once     sync.Once
19}
20
21// reserveMoveLifecycles admits a move at the daemon gate before touching any
22// per-session lifecycle state. All session counts publish atomically while the
23// complete ordered teardown lock set is held.
24func (d *Daemon) reserveMoveLifecycles(source, destination *session) (*moveLifecycleReservation, error) {
25	if d == nil || source == nil || destination == nil {
26		return nil, errMoveLifecycleUnavailable
27	}
28
29	d.moveLifecycleMu.Lock()
30	if d.moveLifecycleClosing {
31		d.moveLifecycleMu.Unlock()
32		return nil, errMoveLifecycleUnavailable
33	}
34	d.mu.Lock()
35	closing := d.closing
36	purgeClosing := d.purgeAdmissionClosing
37	if !closing && !purgeClosing {
38		// A move transfers live state across the exact lifecycle set a KillAll
39		// purge captured, so it reserves the purge admission gate too: the purge
40		// drains this reservation before it snapshots, and a move arriving during
41		// the purge is rejected instead of racing the set.
42		d.purgeAdmissionActive++
43	}
44	d.mu.Unlock()
45	if closing {
46		d.moveLifecycleMu.Unlock()
47		return nil, errMoveLifecycleUnavailable
48	}
49	if purgeClosing {
50		// A transient KillAll purge owns the exact lifecycle set, so no move can
51		// reserve it. Unlike a shutdown this is retry state, so report the same
52		// typed admission outcome as the other command paths instead of a
53		// permanent "target gone" verdict.
54		d.moveLifecycleMu.Unlock()
55		return nil, errPurgeAdmissionClosed
56	}
57	d.moveLifecycleActive++
58	d.signalMoveLifecycleChangedLocked()
59	d.moveLifecycleMu.Unlock()
60	if d.afterMoveLifecycleGateBeforeTeardownLocks != nil {
61		d.afterMoveLifecycleGateBeforeTeardownLocks()
62	}
63
64	sessions := uniqueMoveSessions(source, destination)
65	for _, sess := range sessions {
66		sess.teardownMu.Lock()
67	}
68	for _, sess := range sessions {
69		if sess.teardownActive {
70			unlockMoveSessions(sessions)
71			d.moveLifecycleMu.Lock()
72			d.releaseMoveAdmissionLocked()
73			d.moveLifecycleMu.Unlock()
74			return nil, errMoveLifecycleUnavailable
75		}
76	}
77	for _, sess := range sessions {
78		sess.moveReservations++
79		sess.signalTeardownChangedLocked()
80	}
81	unlockMoveSessions(sessions)
82
83	return &moveLifecycleReservation{daemon: d, sessions: sessions}, nil
84}
85
86func uniqueMoveSessions(source, destination *session) []*session {
87	sessions := []*session{source}
88	if destination != source {
89		sessions = append(sessions, destination)
90	}
91	slices.SortFunc(sessions, func(left, right *session) int {
92		return cmp.Compare(left.id, right.id)
93	})
94	return sessions
95}
96
97func unlockMoveSessions(sessions []*session) {
98	for i := len(sessions) - 1; i >= 0; i-- {
99		sessions[i].teardownMu.Unlock()
100	}
101}
102
103func (r *moveLifecycleReservation) Release() {
104	if r == nil {
105		return
106	}
107	r.once.Do(func() {
108		r.daemon.moveLifecycleMu.Lock()
109		for _, sess := range r.sessions {
110			sess.teardownMu.Lock()
111		}
112		for _, sess := range r.sessions {
113			sess.moveReservations--
114			sess.signalTeardownChangedLocked()
115		}
116		unlockMoveSessions(r.sessions)
117		r.daemon.releaseMoveAdmissionLocked()
118		r.daemon.moveLifecycleMu.Unlock()
119	})
120}
121
122// closeMoveLifecycles rejects new moves, publishes daemon shutdown to ordinary
123// routing, then drains reservations without holding either daemon mutex. Pane
124// process lifetime is cancelled only after the final reservation releases.
125func (d *Daemon) closeMoveLifecycles() {
126	if d == nil {
127		return
128	}
129	d.moveLifecycleMu.Lock()
130	if !d.moveLifecycleClosing {
131		d.moveLifecycleClosing = true
132		d.signalMoveLifecycleChangedLocked()
133	}
134	d.moveLifecycleMu.Unlock()
135
136	// Publish ordinary shutdown admission separately: no move gate mutex is held
137	// while taking the architecture registry lock.
138	d.mu.Lock()
139	d.closing = true
140	d.mu.Unlock()
141
142	d.moveLifecycleMu.Lock()
143	for d.moveLifecycleActive != 0 {
144		changed := d.moveLifecycleChangeLocked()
145		d.moveLifecycleMu.Unlock()
146		<-changed
147		d.moveLifecycleMu.Lock()
148	}
149	cancel := d.paneProcessCancel
150	d.moveLifecycleMu.Unlock()
151	if cancel != nil {
152		cancel()
153	}
154}
155
156// releaseMoveAdmissionLocked drops one move's move-gate and KillAll purge-gate
157// reservations. Caller holds moveLifecycleMu; the purge gate is guarded by mu.
158func (d *Daemon) releaseMoveAdmissionLocked() {
159	d.moveLifecycleActive--
160	d.signalMoveLifecycleChangedLocked()
161	d.mu.Lock()
162	d.purgeAdmissionActive--
163	d.signalPurgeAdmissionChangedLocked()
164	d.mu.Unlock()
165}
166
167func (d *Daemon) moveLifecycleChangeLocked() chan struct{} {
168	if d.moveLifecycleChanged == nil {
169		d.moveLifecycleChanged = make(chan struct{})
170	}
171	return d.moveLifecycleChanged
172}
173
174func (d *Daemon) signalMoveLifecycleChangedLocked() {
175	close(d.moveLifecycleChangeLocked())
176	d.moveLifecycleChanged = make(chan struct{})
177}

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