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