返回 DeepSeek-Reasonix
historical_version_recovery.go
根目录 / desktop / historical_version_recovery.go
1 package main
2
3 import (
4 "context"
5 "path/filepath"
6
7 "reasonix/desktop/internal/workspacestate"
8 "reasonix/internal/identitylock"
9 "reasonix/internal/session"
10 )
11
12 // Called only for an explicit version import, while the source's import and
13 // directory ownership guards are held. Startup and ordinary opens never rekey
14 // an old adoption or turn a failed replacement into a new visible branch.
15 func (a *App) resumeConflictingHistoricalVersion(ctx context.Context, state workspacestate.State, source historicalSource, workspaceID string) (SessionRestoreResult, bool, error) {
16 if source.version == "" || source.format != "canonical" {
17 return SessionRestoreResult{}, false, nil
18 }
19 baseKey := desktopSourceKey(source.path, source.head)
20 var candidate *workspacestate.Operation
21 for _, op := range state.PendingOperations {
22 if op.Kind != "import" || op.Phase != "content_ready" || op.Mapping == nil ||
23 op.Mapping.SourceKey != baseKey || op.Mapping.Fingerprint != source.version || op.WorkspaceID != workspaceID ||
24 op.Mapping.Format != source.format || op.Mapping.HeadID != source.head || !sameDesktopPath(op.Mapping.Path, source.path) {
25 continue
26 }
27 if candidate != nil {
28 return SessionRestoreResult{}, true, workspacestate.ErrMutationConflict
29 }
30 copy := op
31 candidate = &copy
32 }
33 if candidate == nil {
34 return SessionRestoreResult{}, false, nil
35 }
36 if len(candidate.SessionIDs) != 1 {
37 return SessionRestoreResult{}, true, workspacestate.ErrMutationConflict
38 }
39 releaseMutation, ok := a.tryLockRuntimeMutation("recover historical version")
40 if !ok {
41 return SessionRestoreResult{}, true, errTopicArchiveBusy
42 }
43 defer releaseMutation()
44 // As with cold export, the source directory guard plus a shared writer
45 // lock freezes the source without opening or modifying its event log.
46 releaseSource, err := identitylock.TryAcquireMode(filepath.Join(source.path, "writer.lock"), identitylock.ModeShared)
47 if err != nil {
48 return SessionRestoreResult{}, true, err
49 }
50 defer releaseSource()
51 fingerprint, err := desktopSourceFingerprint(source.path)
52 if err != nil {
53 return SessionRestoreResult{}, true, err
54 }
55 if fingerprint != source.version {
56 return SessionRestoreResult{}, true, workspacestate.ErrMutationConflict
57 }
58 id := candidate.SessionIDs[0]
59 releaseTarget, err := session.NewFilesystemPersistence(a.desktopSessions.root).AcquireMaintenance(id)
60 if err != nil {
61 return SessionRestoreResult{}, true, err
62 }
63 defer releaseTarget()
64 target := session.SessionRef{HostID: localDesktopHostID, SessionID: id}
65 if err := a.validateDesktopWorkspaceMembership(ctx, workspaceID, target); err != nil {
66 return SessionRestoreResult{}, true, err
67 }
68 if _, err := a.desktopSessionService("").Query().Snapshot(ctx, target); err != nil {
69 return SessionRestoreResult{}, true, err
70 }
71 old, err := session.NewService("migration-source", session.NewFilesystemPersistence(filepath.Dir(source.path)))
72 if err != nil {
73 return SessionRestoreResult{}, true, err
74 }
75 defer func() { _ = old.Shutdown(context.Background()) }()
76 want, err := canonicalMigrationDigest(ctx, old.Query(), session.SessionRef{HostID: "migration-source", SessionID: filepath.Base(source.path)})
77 if err != nil {
78 return SessionRestoreResult{}, true, err
79 }
80 got, err := canonicalMigrationDigest(ctx, a.desktopSessionService("").Query(), target)
81 if err != nil {
82 return SessionRestoreResult{}, true, err
83 }
84 if got != want {
85 return SessionRestoreResult{}, true, workspacestate.ErrMutationConflict
86 }
87 if err := a.workspaceRegistry().CommitHistoricalVersion(ctx, *candidate, baseKey+":review:"+source.version); err != nil {
88 return SessionRestoreResult{}, true, err
89 }
90 updated, err := a.workspaceRegistry().Load(ctx)
91 if err != nil {
92 return SessionRestoreResult{}, true, err
93 }
94 return SessionRestoreResult{Session: target, WorkspaceID: workspaceID, Generation: updated.Generation}, true, nil
95 }
96
96 lines GO