返回 DeepSeek-Reasonix
project_state_dir.go
根目录 / internal / config / project_state_dir.go
1 package config
2
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10
11 "reasonix/internal/fileutil"
12 )
13
14 const projectCollisionMarker = ".workspace-root"
15
16 func projectStateRoot(workspaceRoot string) string {
17 root := strings.TrimSpace(workspaceRoot)
18 if root == "" {
19 return ""
20 }
21 if abs, err := filepath.Abs(root); err == nil {
22 root = abs
23 }
24 if runtimeGOOS == "windows" {
25 root = strings.ToLower(root)
26 }
27 return root
28 }
29
30 func collisionProjectDir(userDir, root string) string {
31 sum := sha256.Sum256([]byte(root))
32 return filepath.Join(userDir, "projects", "@"+hex.EncodeToString(sum[:]))
33 }
34
35 // ProjectStateDir preserves the historical directory unless an explicit
36 // collision assignment was recorded when a project was added. Reads never
37 // claim a legacy directory or move existing state.
38 func ProjectStateDir(userDir, workspaceRoot string) string {
39 root := projectStateRoot(workspaceRoot)
40 if userDir == "" || root == "" {
41 return ""
42 }
43 legacy := filepath.Join(userDir, "projects", WorkspaceSlug(root))
44 assigned := collisionProjectDir(userDir, root)
45 owner, err := os.ReadFile(filepath.Join(assigned, projectCollisionMarker))
46 if err == nil && string(owner) == root+"\n" {
47 return assigned
48 }
49 return legacy
50 }
51
52 // AssignProjectStateCollision records a new project's alternate directory.
53 // Callers must first establish a slug collision from their saved project list;
54 // this write is never made by ProjectStateDir or a read-only listing.
55 func AssignProjectStateCollision(userDir, workspaceRoot string) error {
56 root := projectStateRoot(workspaceRoot)
57 if userDir == "" || root == "" {
58 return fmt.Errorf("project state collision: missing state or workspace root")
59 }
60 dir := collisionProjectDir(userDir, root)
61 marker := filepath.Join(dir, projectCollisionMarker)
62 owner, err := os.ReadFile(marker)
63 if err == nil {
64 if string(owner) == root+"\n" {
65 return nil
66 }
67 return fmt.Errorf("project state collision: %s belongs to another workspace", dir)
68 }
69 if !os.IsNotExist(err) {
70 return fmt.Errorf("project state collision: read assignment: %w", err)
71 }
72 if err := os.MkdirAll(dir, 0o700); err != nil {
73 return fmt.Errorf("project state collision: create directory: %w", err)
74 }
75 entries, err := os.ReadDir(dir)
76 if err != nil {
77 return fmt.Errorf("project state collision: inspect directory: %w", err)
78 }
79 if len(entries) != 0 {
80 return fmt.Errorf("project state collision: unowned directory %s is not empty", dir)
81 }
82 if err := fileutil.AtomicWriteFileStrict(marker, []byte(root+"\n"), 0o600); err != nil {
83 return fmt.Errorf("project state collision: record assignment: %w", err)
84 }
85 return nil
86 }
87
87 lines GO