From a49d45a846aaea2cccc2540ef9ec3a6dfa642894 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 10 Jun 2026 17:56:47 +0800 Subject: [PATCH] feat(git): add FetchRemoteBranch, Checkout, ListRemoteBranches to Runner --- internal/infra/git/branch.go | 64 ++++++++++++++++++++ internal/infra/git/runner.go | 3 + internal/workspace/workspace_service_test.go | 5 ++ 3 files changed, 72 insertions(+) create mode 100644 internal/infra/git/branch.go diff --git a/internal/infra/git/branch.go b/internal/infra/git/branch.go new file mode 100644 index 0000000..1e132a7 --- /dev/null +++ b/internal/infra/git/branch.go @@ -0,0 +1,64 @@ +package git + +import ( + "bufio" + "bytes" + "context" + "strings" +) + +// FetchRemoteBranch fetches a single remote branch (origin/). +// Lighter than Fetch --all when we only need one branch. +func (r *DefaultRunner) FetchRemoteBranch(ctx context.Context, dir, branch string, cred *Credential) error { + ctx, cancel := withCtx(ctx, r.cfg.FetchTimeout) + defer cancel() + env, cleanup, err := credentialEnv(r.cfg.TmpDir, cred) + if err != nil { + return err + } + defer cleanup() + _, _, err = r.exec(ctx, dir, env, "fetch", "origin", branch) + return err +} + +// Checkout switches the working tree to the given branch. +func (r *DefaultRunner) Checkout(ctx context.Context, dir, branch string) error { + ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout) + defer cancel() + _, _, err := r.exec(ctx, dir, nil, "checkout", branch) + return err +} + +// ListRemoteBranches queries the remote for all branch names via git ls-remote. +// Returns branch names with refs/heads/ prefix stripped. +func (r *DefaultRunner) ListRemoteBranches(ctx context.Context, dir string, cred *Credential) ([]string, error) { + ctx, cancel := withCtx(ctx, r.cfg.FetchTimeout) + defer cancel() + env, cleanup, err := credentialEnv(r.cfg.TmpDir, cred) + if err != nil { + return nil, err + } + defer cleanup() + out, _, err := r.exec(ctx, dir, env, "ls-remote", "--heads") + if err != nil { + return nil, err + } + return parseRemoteBranches(out), nil +} + +// parseRemoteBranches parses git ls-remote --heads output. +// Each line: "\trefs/heads/" +func parseRemoteBranches(buf []byte) []string { + var branches []string + sc := bufio.NewScanner(bytes.NewReader(buf)) + for sc.Scan() { + line := sc.Text() + if idx := strings.Index(line, "\trefs/heads/"); idx >= 0 { + name := line[idx+len("\trefs/heads/"):] + if name != "" { + branches = append(branches, name) + } + } + } + return branches +} diff --git a/internal/infra/git/runner.go b/internal/infra/git/runner.go index 4bd6f27..a4f44b2 100644 --- a/internal/infra/git/runner.go +++ b/internal/infra/git/runner.go @@ -23,6 +23,7 @@ const stderrTruncateLimit = 2000 type Runner interface { Clone(ctx context.Context, dst, remoteURL, branch string, cred *Credential) error Fetch(ctx context.Context, dir string, cred *Credential) error + FetchRemoteBranch(ctx context.Context, dir, branch string, cred *Credential) error Push(ctx context.Context, dir, branch string, cred *Credential) error Commit(ctx context.Context, dir, message string, addAll bool) (sha string, err error) Status(ctx context.Context, dir string) ([]FileStatus, error) @@ -30,6 +31,8 @@ type Runner interface { WorktreeAdd(ctx context.Context, dir, branch, path, startPoint string) error WorktreeRemove(ctx context.Context, dir, path string) error WorktreeList(ctx context.Context, dir string) ([]Worktree, error) + ListRemoteBranches(ctx context.Context, dir string, cred *Credential) ([]string, error) + Checkout(ctx context.Context, dir, branch string) error } // FileStatus 表示 `git status --porcelain=v1` 的一行。 diff --git a/internal/workspace/workspace_service_test.go b/internal/workspace/workspace_service_test.go index 6e51547..e2c1c41 100644 --- a/internal/workspace/workspace_service_test.go +++ b/internal/workspace/workspace_service_test.go @@ -226,6 +226,11 @@ func (f *fakeGit) WorktreeRemove(_ context.Context, _, _ string) error func (f *fakeGit) WorktreeList(_ context.Context, _ string) ([]git.Worktree, error) { return nil, nil } +func (f *fakeGit) FetchRemoteBranch(_ context.Context, _, _ string, _ *git.Credential) error { return nil } +func (f *fakeGit) Checkout(_ context.Context, _, _ string) error { return nil } +func (f *fakeGit) ListRemoteBranches(_ context.Context, _ string, _ *git.Credential) ([]string, error) { + return nil, nil +} type noopAudit struct{}