返回 DeepSeek-Reasonix
workspace_git_branches.go
根目录 / desktop / workspace_git_branches.go
1 package main
2
3 import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "time"
10 )
11
12 func (a *App) gitWorkspaceBaseForTab(tabID, expectedRoot string) (string, error) {
13 tabID = strings.TrimSpace(tabID)
14 if tabID == "" {
15 return "", fmt.Errorf("workspace tab is required")
16 }
17 root, _, ok := a.workspaceChangesTarget(tabID)
18 if !ok || !filepath.IsAbs(root) {
19 return "", fmt.Errorf("workspace tab %q is unavailable", tabID)
20 }
21 if !filepath.IsAbs(expectedRoot) || !sameProjectRoot(root, expectedRoot) {
22 return "", fmt.Errorf("workspace tab %q has changed project", tabID)
23 }
24 if info, err := os.Stat(root); err != nil || !info.IsDir() {
25 return "", fmt.Errorf("workspace tab %q directory is unavailable", tabID)
26 }
27 return workspaceBaseFromRoot(root)
28 }
29
30 func (a *App) GitBranchesForTab(tabID, workspaceRoot string) ([]string, error) {
31 base, err := a.gitWorkspaceBaseForTab(tabID, workspaceRoot)
32 if err != nil {
33 return nil, err
34 }
35 return workspaceLocalBranches(base)
36 }
37
38 func (a *App) GitCheckoutForTab(tabID, workspaceRoot, branch string) error {
39 base, err := a.gitWorkspaceBaseForTab(tabID, workspaceRoot)
40 if err != nil {
41 return err
42 }
43 return workspaceCheckoutBranch(base, branch, false)
44 }
45
46 func (a *App) GitCreateBranchForTab(tabID, workspaceRoot, name string) error {
47 base, err := a.gitWorkspaceBaseForTab(tabID, workspaceRoot)
48 if err != nil {
49 return err
50 }
51 return workspaceCheckoutBranch(base, name, true)
52 }
53
54 // The launcher needs only Git totals, not session checkpoints or per-file views.
55 func (a *App) WorkspaceGitStatsForTab(tabID, workspaceRoot string) (WorkspaceChangesView, error) {
56 base, err := a.gitWorkspaceBaseForTab(tabID, workspaceRoot)
57 if err != nil {
58 return WorkspaceChangesView{}, err
59 }
60 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
61 defer cancel()
62 out := WorkspaceChangesView{Files: []WorkspaceChangeView{}, GitAvailable: true}
63 out.GitBranch, _ = workspaceGitBranchContext(ctx, base)
64 entries, err := workspaceGitStatusContext(ctx, base)
65 if err != nil {
66 out.GitAvailable, out.Incomplete, out.GitErr = false, true, err.Error()
67 return out, nil
68 }
69 untracked := []string{}
70 for _, entry := range entries {
71 if entry.Status == "??" {
72 untracked = append(untracked, entry.Path)
73 }
74 }
75 out.Added, out.Removed, out.Incomplete = workspaceGitDiffTally(ctx, base, untracked)
76 return out, nil
77 }
78
79 func workspaceLocalBranches(base string) ([]string, error) {
80 raw, err := workspaceGitOutputWithTimeout(3*time.Second, "-C", base, "branch", "--format=%(refname:short)")
81 if err != nil {
82 return nil, err
83 }
84 return append([]string{}, strings.FieldsFunc(strings.TrimSpace(string(raw)), func(r rune) bool { return r == '\n' })...), nil
85 }
86
87 func workspaceCheckoutBranch(base, name string, create bool) error {
88 name = strings.TrimSpace(name)
89 if !validGitBranchName(name) {
90 return fmt.Errorf("invalid branch name %q", name)
91 }
92 args := []string{"-C", base, "checkout"}
93 if create {
94 args = append(args, "-b")
95 }
96 args = append(args, name)
97 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
98 defer cancel()
99 out, err := workspaceGitCommand(ctx, args...).CombinedOutput()
100 if err != nil {
101 return fmt.Errorf("git checkout: %w: %s", err, strings.TrimSpace(string(out)))
102 }
103 branch, _ := workspaceGitBranchContext(ctx, base)
104 workspaceGitBranchCache.Lock()
105 if branch == "" {
106 delete(workspaceGitBranchCache.entries, filepath.Clean(base))
107 } else {
108 workspaceGitBranchCache.entries[filepath.Clean(base)] = workspaceGitBranchCacheEntry{
109 branch: branch, expires: time.Now().Add(workspaceGitBranchCacheTTL),
110 }
111 }
112 workspaceGitBranchCache.Unlock()
113 return nil
114 }
115
115 lines GO