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
99 lines
3.1 KiB
Go
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
|
|
}
|