Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

99 lines
3.1 KiB
Go

package agent
import (
"context"
"os"
"os/exec"
"strings"
"time"
"github.com/tutti-os/tutti/packages/agent/daemon/runtimecmd"
)
// gitBranchListTimeout bounds the git subprocesses backing the review picker so
// a hung working directory (e.g. a stalled network mount) degrades to an empty
// result instead of blocking the request, mirroring the timeout the app
// factory applies to its own exec calls.
const gitBranchListTimeout = 5 * time.Second
// GitBranches describes the local branches of a working directory plus the
// currently checked-out branch (empty when detached or unknown).
type GitBranches struct {
Branches []string
CurrentBranch string
}
// listGitBranches lists local git branches for a working directory. A missing
// directory, a non-git directory, or an unavailable git binary degrades
// gracefully to an empty result rather than an error: the review picker simply
// offers no branches instead of surfacing a failure.
func listGitBranches(ctx context.Context, cwd string) (GitBranches, error) {
cwd = strings.TrimSpace(cwd)
if cwd == "" {
return GitBranches{}, nil
}
ctx, cancel := context.WithTimeout(ctx, gitBranchListTimeout)
defer cancel()
out, err := runGit(ctx, cwd, "for-each-ref", "--format=%(refname:short)", "refs/heads")
if err != nil {
return GitBranches{}, nil
}
var branches []string
for _, line := range strings.Split(out, "\n") {
if name := strings.TrimSpace(line); name != "" {
branches = append(branches, name)
}
}
currentBranch := gitCurrentBranch(ctx, cwd)
return GitBranches{
Branches: branches,
CurrentBranch: currentBranch,
}, nil
}
// gitCurrentBranch returns the checked-out branch name, or "" when the working
// directory is in a detached-HEAD state or the lookup fails.
func gitCurrentBranch(ctx context.Context, cwd string) string {
out, err := runGit(ctx, cwd, "rev-parse", "--abbrev-ref", "HEAD")
if err != nil {
return ""
}
branch := strings.TrimSpace(out)
if branch == "HEAD" {
return ""
}
return branch
}
func runGit(ctx context.Context, cwd string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = cwd
// Resolve the repository from cwd, never from an ambient GIT_DIR/GIT_WORK_TREE
// in the daemon's environment, so the picker always reflects the session's
// working directory.
// Inject the macOS system proxy for parity with the other agent subprocesses.
// Branch discovery is local, so this is a no-op today, but keeps the env
// consistent if git ever needs to reach a remote here.
cmd.Env = runtimecmd.InjectSystemProxyEnv(gitEnvScopedToDir())
out, err := cmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}
// gitEnvScopedToDir returns the process environment with the repository-pointing
// overrides stripped, so git discovers the repo from the command's working
// directory instead of an inherited GIT_DIR/GIT_WORK_TREE.
func gitEnvScopedToDir() []string {
env := os.Environ()
scoped := make([]string, 0, len(env))
for _, kv := range env {
if strings.HasPrefix(kv, "GIT_DIR=") || strings.HasPrefix(kv, "GIT_WORK_TREE=") {
continue
}
scoped = append(scoped, kv)
}
return scoped
}