返回 DeepSeek-Reasonix
merge_state.go
根目录 / internal / worktree / merge_state.go
1 package worktree
2
3 import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "errors"
8 "fmt"
9 "hash"
10 "io"
11 "os"
12 "path/filepath"
13 "sort"
14 "strings"
15 )
16
17 // worktreeStateToken fingerprints the real index and dirty filesystem state
18 // without modifying either. Porcelain and ls-files -z keep unusual paths
19 // unambiguous, while the index entries bind staged-only content and modes.
20 func worktreeStateToken(ctx context.Context, root string) (string, error) {
21 status, stderr, err := runGitEnv(ctx, root, gitNoOptionalLocks, "status", "--porcelain=v1", "-z", "--untracked-files=all")
22 if err != nil {
23 return "", fmt.Errorf("list changed paths: %w%s", err, stderrSuffix(stderr))
24 }
25 index, stderr, err := runGitEnv(ctx, root, gitNoOptionalLocks, "ls-files", "--stage", "-z")
26 if err != nil {
27 return "", fmt.Errorf("snapshot index entries: %w%s", err, stderrSuffix(stderr))
28 }
29 paths, err := nulStatusPaths(status)
30 if err != nil {
31 return "", err
32 }
33 hash := sha256.New()
34 _, _ = io.WriteString(hash, "reasonix-worktree-state-v3\x00status\x00")
35 _, _ = io.WriteString(hash, status)
36 _, _ = io.WriteString(hash, "\x00index\x00")
37 _, _ = io.WriteString(hash, index)
38 _, _ = io.WriteString(hash, "\x00filesystem\x00")
39 for _, relative := range paths {
40 if err := hashWorktreePath(ctx, hash, root, relative); err != nil {
41 return "", err
42 }
43 }
44 return hex.EncodeToString(hash.Sum(nil)), nil
45 }
46
47 func nulStatusPaths(status string) ([]string, error) {
48 records := strings.Split(status, "\x00")
49 seen := map[string]struct{}{}
50 paths := []string{}
51 for index := 0; index < len(records); index++ {
52 record := records[index]
53 if record == "" {
54 continue
55 }
56 if len(record) < 4 || record[2] != ' ' {
57 return nil, fmt.Errorf("unexpected Git status record %q", record)
58 }
59 path := record[3:]
60 if err := validateStatePath(path); err != nil {
61 return nil, err
62 }
63 if _, ok := seen[path]; !ok {
64 seen[path] = struct{}{}
65 paths = append(paths, path)
66 }
67 if record[0] == 'R' || record[0] == 'C' || record[1] == 'R' || record[1] == 'C' {
68 index++
69 if index >= len(records) || records[index] == "" {
70 return nil, errors.New("Git status rename record is incomplete")
71 }
72 oldPath := records[index]
73 if err := validateStatePath(oldPath); err != nil {
74 return nil, err
75 }
76 if _, ok := seen[oldPath]; !ok {
77 seen[oldPath] = struct{}{}
78 paths = append(paths, oldPath)
79 }
80 }
81 }
82 sort.Strings(paths)
83 return paths, nil
84 }
85
86 func validateStatePath(path string) error {
87 if path == "" || filepath.IsAbs(filepath.FromSlash(path)) {
88 return fmt.Errorf("unsafe changed path %q", path)
89 }
90 clean := filepath.Clean(filepath.FromSlash(path))
91 if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
92 return fmt.Errorf("changed path escapes worktree: %q", path)
93 }
94 return nil
95 }
96
97 func hashWorktreePath(ctx context.Context, stateHash hash.Hash, root, relative string) error {
98 _, _ = io.WriteString(stateHash, "path\x00"+relative+"\x00")
99 path := filepath.Join(root, filepath.FromSlash(relative))
100 info, err := os.Lstat(path)
101 if errors.Is(err, os.ErrNotExist) {
102 _, _ = io.WriteString(stateHash, "deleted\x00")
103 return nil
104 }
105 if err != nil {
106 return fmt.Errorf("inspect changed path %q: %w", relative, err)
107 }
108 _, _ = io.WriteString(stateHash, info.Mode().String()+"\x00")
109 switch {
110 case info.Mode().IsRegular():
111 digest, err := digestWorktreeStateFile(ctx, path)
112 if err != nil {
113 return fmt.Errorf("digest changed path %q: %w", relative, err)
114 }
115 _, _ = io.WriteString(stateHash, digest)
116 case info.Mode()&os.ModeSymlink != 0:
117 target, err := os.Readlink(path)
118 if err != nil {
119 return fmt.Errorf("read changed symlink %q: %w", relative, err)
120 }
121 _, _ = io.WriteString(stateHash, target)
122 case info.IsDir():
123 head, stderr, err := gitValue(ctx, path, "rev-parse", "--verify", "HEAD")
124 if err != nil {
125 return fmt.Errorf("inspect changed Git directory %q: %w%s", relative, err, stderrSuffix(stderr))
126 }
127 status, stderr, err := runGitEnv(ctx, path, gitNoOptionalLocks, "status", "--porcelain=v1", "-z", "--untracked-files=all")
128 if err != nil {
129 return fmt.Errorf("inspect changed Git directory status %q: %w%s", relative, err, stderrSuffix(stderr))
130 }
131 _, _ = io.WriteString(stateHash, head+"\x00"+status)
132 default:
133 return fmt.Errorf("changed path %q has unsupported file type %s", relative, info.Mode().Type())
134 }
135 _, _ = io.WriteString(stateHash, "\x00")
136 return nil
137 }
138
139 func digestWorktreeStateFile(ctx context.Context, path string) (string, error) {
140 file, err := os.Open(path)
141 if err != nil {
142 return "", err
143 }
144 defer file.Close()
145
146 digest := sha256.New()
147 buffer := make([]byte, 128*1024)
148 for {
149 if err := ctx.Err(); err != nil {
150 return "", err
151 }
152 count, readErr := file.Read(buffer)
153 if count > 0 {
154 if _, err := digest.Write(buffer[:count]); err != nil {
155 return "", err
156 }
157 }
158 if errors.Is(readErr, io.EOF) {
159 return hex.EncodeToString(digest.Sum(nil)), nil
160 }
161 if readErr != nil {
162 return "", readErr
163 }
164 }
165 }
166
167 func gitOperation(ctx context.Context, root string) (string, error) {
168 operations := []struct{ name, marker string }{
169 {"merge", "MERGE_HEAD"}, {"rebase", "rebase-merge"}, {"rebase", "rebase-apply"},
170 {"cherry-pick", "CHERRY_PICK_HEAD"}, {"revert", "REVERT_HEAD"}, {"bisect", "BISECT_LOG"},
171 }
172 for _, operation := range operations {
173 path, stderr, err := gitValue(ctx, root, "rev-parse", "--git-path", operation.marker)
174 if err != nil {
175 return "", fmt.Errorf("inspect Git operation %s: %w%s", operation.name, err, stderrSuffix(stderr))
176 }
177 if !filepath.IsAbs(path) {
178 path = filepath.Join(root, path)
179 }
180 if _, err := os.Stat(path); err == nil {
181 return operation.name, nil
182 } else if !errors.Is(err, os.ErrNotExist) {
183 return "", fmt.Errorf("inspect Git operation %s: %w", operation.name, err)
184 }
185 }
186 return "", nil
187 }
188
188 lines GO