detached cd4708e

refactor(app): switch to incremental snapshots

commit
cd4708ea0b795fe8d6fe26c4ad95465cf0f31f31
author
brice · · 2026-07-21 21:50 UTC
parent
792ed4c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +36 -7
M internal/app/run.go +10 -3
@@ -450,7 +450,8 @@ func runDaemon() (retErr error) {
450450 daemonOpts = append(daemonOpts, daemon.WithConfig(cfg))
451451 daemonOpts = append(daemonOpts, daemon.WithBarScriptCommandRunner(shellcmd.New()))
452452 daemonOpts = append(daemonOpts, daemon.WithProcessInspector(platform.NewProcessInspector()), daemon.WithDirOrHome(platform.DirOrHome))
453- daemonOpts = append(daemonOpts, daemon.WithSnapshotStore(snapshotadapter.NewStore(snapshotDir())))
453+ snapshotRepository := snapshotadapter.NewRepository(snapshotDir())
454+ daemonOpts = append(daemonOpts, daemon.WithSnapshotRepository(snapshotRepository, snapshotRepository))
454455 daemonOpts = append(daemonOpts, daemon.WithNoticeStore(noticefile.New(platform.StateDir())))
455456 storePath := persist.StorePath(platform.StateDir())
456457 var storeErr error
@@ -1221,10 +1222,16 @@ func runKill(ctx context.Context, name string, all, daemon bool) error {
12211222 if closeErr := p.Close(); closeErr != nil {
12221223 return fmt.Errorf("vev: closing stored sessions: %w", closeErr)
12231224 }
1224- snapshots := snapshotadapter.NewStore(snapshotDir())
1225- if deleteErr := snapshots.Delete(name); deleteErr != nil {
1225+ snapshots := snapshotadapter.NewRepository(snapshotDir())
1226+ if deleteErr := snapshots.Delete(ctx, name); deleteErr != nil {
12261227 return fmt.Errorf("vev: deleting session snapshot: %w", deleteErr)
12271228 }
1229+ // A daemon-less delete may be the first command after upgrade, before
1230+ // startup has had a chance to import v3. Remove that one-way source as
1231+ // well so a later daemon cannot resurrect an explicitly killed session.
1232+ if deleteErr := snapshots.DeleteLegacy(ctx, name); deleteErr != nil {
1233+ return fmt.Errorf("vev: deleting legacy session snapshot: %w", deleteErr)
1234+ }
12281235 printKillSuccess(name, all, daemon)
12291236 return nil
12301237 }
@@ -450,7 +450,8 @@ func runDaemon() (retErr error) {
450 daemonOpts = append(daemonOpts, daemon.WithConfig(cfg))450 daemonOpts = append(daemonOpts, daemon.WithConfig(cfg))
451 daemonOpts = append(daemonOpts, daemon.WithBarScriptCommandRunner(shellcmd.New()))451 daemonOpts = append(daemonOpts, daemon.WithBarScriptCommandRunner(shellcmd.New()))
452 daemonOpts = append(daemonOpts, daemon.WithProcessInspector(platform.NewProcessInspector()), daemon.WithDirOrHome(platform.DirOrHome))452 daemonOpts = append(daemonOpts, daemon.WithProcessInspector(platform.NewProcessInspector()), daemon.WithDirOrHome(platform.DirOrHome))
453- daemonOpts = append(daemonOpts, daemon.WithSnapshotStore(snapshotadapter.NewStore(snapshotDir())))453+ snapshotRepository := snapshotadapter.NewRepository(snapshotDir())
454+ daemonOpts = append(daemonOpts, daemon.WithSnapshotRepository(snapshotRepository, snapshotRepository))
454 daemonOpts = append(daemonOpts, daemon.WithNoticeStore(noticefile.New(platform.StateDir())))455 daemonOpts = append(daemonOpts, daemon.WithNoticeStore(noticefile.New(platform.StateDir())))
455 storePath := persist.StorePath(platform.StateDir())456 storePath := persist.StorePath(platform.StateDir())
456 var storeErr error457 var storeErr error
@@ -1221,10 +1222,16 @@ func runKill(ctx context.Context, name string, all, daemon bool) error {
1221 if closeErr := p.Close(); closeErr != nil {1222 if closeErr := p.Close(); closeErr != nil {
1222 return fmt.Errorf("vev: closing stored sessions: %w", closeErr)1223 return fmt.Errorf("vev: closing stored sessions: %w", closeErr)
1223 }1224 }
1224- snapshots := snapshotadapter.NewStore(snapshotDir())1225+ snapshots := snapshotadapter.NewRepository(snapshotDir())
1225- if deleteErr := snapshots.Delete(name); deleteErr != nil {1226+ if deleteErr := snapshots.Delete(ctx, name); deleteErr != nil {
1226 return fmt.Errorf("vev: deleting session snapshot: %w", deleteErr)1227 return fmt.Errorf("vev: deleting session snapshot: %w", deleteErr)
1227 }1228 }
1229+ // A daemon-less delete may be the first command after upgrade, before
1230+ // startup has had a chance to import v3. Remove that one-way source as
1231+ // well so a later daemon cannot resurrect an explicitly killed session.
1232+ if deleteErr := snapshots.DeleteLegacy(ctx, name); deleteErr != nil {
1233+ return fmt.Errorf("vev: deleting legacy session snapshot: %w", deleteErr)
1234+ }
1228 printKillSuccess(name, all, daemon)1235 printKillSuccess(name, all, daemon)
1229 return nil1236 return nil
1230 }1237 }
M internal/usecase/daemon/session.go +26 -4
@@ -553,7 +553,15 @@ func (d *Daemon) renameSession(sess *session, name string) error {
553553 }
554554 }
555555 if !wasEphemeral && oldName != name {
556- if d.snapsEnabled && d.snaps != nil {
556+ if d.snapsEnabled && d.snapshotRepository != nil {
557+ if err := d.snapshotRepository.Delete(context.Background(), oldName); err != nil {
558+ if cleanupErr := d.persist.Delete(name); cleanupErr != nil {
559+ d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name)
560+ }
561+ sess.mu.Unlock()
562+ return err
563+ }
564+ } else if d.snapsEnabled && d.snaps != nil {
557565 if err := d.snaps.Delete(oldName); err != nil {
558566 if cleanupErr := d.persist.Delete(name); cleanupErr != nil {
559567 d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name)
@@ -731,9 +739,15 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error {
731739 sess.mu.Lock()
732740 name := sess.name
733741 sess.mu.Unlock()
734- if err := d.snaps.Delete(name); err != nil {
735- snapshotDeleteErr = err
736- d.log.Warn("deleting session snapshot failed", "err", err, "session", name)
742+ if d.snapshotRepository != nil {
743+ // Repository deletion is intentionally deferred until the render
744+ // coordinator is cancelled and joined below. See client.go's lock
745+ // ordering notes: no coordinator callback may race a purge.
746+ } else if d.snaps != nil {
747+ if err := d.snaps.Delete(name); err != nil {
748+ snapshotDeleteErr = err
749+ d.log.Warn("deleting session snapshot failed", "err", err, "session", name)
750+ }
737751 }
738752 } else {
739753 markSnapshotDirty(sess)
@@ -840,6 +854,14 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error {
840854 d.log.Warn("removing clipboard temp file failed", "err", err, "path", path)
841855 }
842856 }
857+ // The coordinator and all panes are now stopped, so no producer can
858+ // publish another generation after this destructive repository delete.
859+ if !ephemeral && purge && d.snapshotRepository != nil {
860+ if err := d.snapshotRepository.Delete(context.Background(), stoppedName); err != nil {
861+ purgeErr = errors.Join(purgeErr, err)
862+ d.log.Warn("deleting session snapshot failed", "err", err, "session", stoppedName)
863+ }
864+ }
843865 if empty {
844866 d.doneOnce.Do(func() { close(d.done) })
845867 } else if ringing {
@@ -553,7 +553,15 @@ func (d *Daemon) renameSession(sess *session, name string) error {
553 }553 }
554 }554 }
555 if !wasEphemeral && oldName != name {555 if !wasEphemeral && oldName != name {
556- if d.snapsEnabled && d.snaps != nil {556+ if d.snapsEnabled && d.snapshotRepository != nil {
557+ if err := d.snapshotRepository.Delete(context.Background(), oldName); err != nil {
558+ if cleanupErr := d.persist.Delete(name); cleanupErr != nil {
559+ d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name)
560+ }
561+ sess.mu.Unlock()
562+ return err
563+ }
564+ } else if d.snapsEnabled && d.snaps != nil {
557 if err := d.snaps.Delete(oldName); err != nil {565 if err := d.snaps.Delete(oldName); err != nil {
558 if cleanupErr := d.persist.Delete(name); cleanupErr != nil {566 if cleanupErr := d.persist.Delete(name); cleanupErr != nil {
559 d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name)567 d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name)
@@ -731,9 +739,15 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error {
731 sess.mu.Lock()739 sess.mu.Lock()
732 name := sess.name740 name := sess.name
733 sess.mu.Unlock()741 sess.mu.Unlock()
734- if err := d.snaps.Delete(name); err != nil {742+ if d.snapshotRepository != nil {
735- snapshotDeleteErr = err743+ // Repository deletion is intentionally deferred until the render
736- d.log.Warn("deleting session snapshot failed", "err", err, "session", name)744+ // coordinator is cancelled and joined below. See client.go's lock
745+ // ordering notes: no coordinator callback may race a purge.
746+ } else if d.snaps != nil {
747+ if err := d.snaps.Delete(name); err != nil {
748+ snapshotDeleteErr = err
749+ d.log.Warn("deleting session snapshot failed", "err", err, "session", name)
750+ }
737 }751 }
738 } else {752 } else {
739 markSnapshotDirty(sess)753 markSnapshotDirty(sess)
@@ -840,6 +854,14 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error {
840 d.log.Warn("removing clipboard temp file failed", "err", err, "path", path)854 d.log.Warn("removing clipboard temp file failed", "err", err, "path", path)
841 }855 }
842 }856 }
857+ // The coordinator and all panes are now stopped, so no producer can
858+ // publish another generation after this destructive repository delete.
859+ if !ephemeral && purge && d.snapshotRepository != nil {
860+ if err := d.snapshotRepository.Delete(context.Background(), stoppedName); err != nil {
861+ purgeErr = errors.Join(purgeErr, err)
862+ d.log.Warn("deleting session snapshot failed", "err", err, "session", stoppedName)
863+ }
864+ }
843 if empty {865 if empty {
844 d.doneOnce.Do(func() { close(d.done) })866 d.doneOnce.Do(func() { close(d.done) })
845 } else if ringing {867 } else if ringing {

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