返回 DeepSeek-Reasonix
workspace_git_tally.go
根目录 / desktop / workspace_git_tally.go
1 package main
2
3 import (
4 "bytes"
5 "context"
6 "io"
7 "os"
8 "path/filepath"
9 "strconv"
10 "strings"
11
12 "reasonix/internal/gitcmd"
13 )
14
15 const (
16 workspaceDiffTallyFileLimit = 1 << 20
17 workspaceDiffTallyReadLimit = 16 << 20
18 )
19
20 func workspaceGitDiffTally(ctx context.Context, repo gitcmd.Repo, untracked []string) (added, removed int, incomplete bool) {
21 base := repo.Dir
22 raw, err := workspaceGitCommand(ctx, repo, "-C", base, "diff", "--numstat", "--no-textconv", "HEAD", "--", ".").Output()
23 if err != nil && ctx.Err() == nil {
24 incomplete = true
25 raw, err = workspaceGitCommand(ctx, repo, "-C", base, "diff", "--numstat", "--no-textconv", "--", ".").Output()
26 }
27 if err != nil {
28 return 0, 0, true
29 }
30 for line := range strings.SplitSeq(strings.TrimSpace(string(raw)), "\n") {
31 fields := strings.Fields(line)
32 if len(fields) < 2 {
33 continue
34 }
35 n, _ := strconv.Atoi(fields[0])
36 added += n
37 n, _ = strconv.Atoi(fields[1])
38 removed += n
39 }
40 if len(untracked) == 0 {
41 return added, removed, incomplete || ctx.Err() != nil
42 }
43 root, err := os.OpenRoot(base)
44 if err != nil {
45 return added, removed, true
46 }
47 defer root.Close()
48 remaining := int64(workspaceDiffTallyReadLimit)
49 for _, rel := range untracked {
50 if ctx.Err() != nil || remaining <= 0 {
51 return added, removed, true
52 }
53 count, partial := workspaceCountFileLines(ctx, root, filepath.FromSlash(rel), &remaining)
54 added += count
55 incomplete = incomplete || partial
56 }
57 return added, removed, incomplete
58 }
59
60 func workspaceCountFileLines(ctx context.Context, root *os.Root, path string, remaining *int64) (int, bool) {
61 if !filepath.IsLocal(path) || ctx.Err() != nil {
62 return 0, true
63 }
64 // Check every component; Root also prevents a concurrent replacement escaping
65 // the workspace. The platform open rejects final symlinks and blocking FIFOs.
66 for parent := path; parent != "."; parent = filepath.Dir(parent) {
67 info, err := root.Lstat(parent)
68 if err != nil || info.Mode()&os.ModeSymlink != 0 {
69 return 0, true
70 }
71 }
72 before, err := root.Lstat(path)
73 if err != nil || !before.Mode().IsRegular() {
74 return 0, true
75 }
76 f, err := openWorkspaceTallyFile(root, path)
77 if err != nil {
78 return 0, true
79 }
80 defer f.Close()
81 info, err := f.Stat()
82 if err != nil || !info.Mode().IsRegular() || !os.SameFile(before, info) {
83 return 0, true
84 }
85 limit := min(int64(workspaceDiffTallyFileLimit), *remaining)
86 partial := info.Size() > limit
87 reader := io.LimitReader(f, limit)
88 buf := make([]byte, 64*1024)
89 count, total := 0, int64(0)
90 var last byte
91 for {
92 if ctx.Err() != nil {
93 return count, true
94 }
95 n, readErr := reader.Read(buf)
96 *remaining -= int64(n)
97 total += int64(n)
98 if n > 0 {
99 chunk := buf[:n]
100 if bytes.IndexByte(chunk, 0) >= 0 {
101 return 0, false
102 }
103 count += bytes.Count(chunk, []byte{'\n'})
104 last = chunk[n-1]
105 }
106 if readErr != nil {
107 if readErr != io.EOF {
108 return count, true
109 }
110 break
111 }
112 }
113 if total > 0 && last != '\n' && !partial {
114 count++
115 }
116 return count, partial
117 }
118
118 lines GO