Files
agentic-coding-workflow/internal/infra/git/worktree.go
T

86 lines
2.8 KiB
Go

package git
import (
"bufio"
"bytes"
"context"
"strings"
)
// WorktreeAdd 在 dir(main worktree)创建一个支线 worktree。
//
// path: 绝对路径,由 workspace 包计算
// branch: 用 -B 强制创建/重置 — 已存在的分支会被 reset 到 origin/HEAD
// (本地未推送 commit 会丢失,可从 reflog 找回);--track 让分支
// 默认 track origin/HEAD(通常是 main),首次 push 时由 service
// 层 set-upstream 到对应远端分支。
//
// 失败时如果 stderr 含 "already exists" / "is already used by worktree",
// errors.IsConflict 返回 true,调用方应映射为 WORKTREE_BRANCH_CONFLICT。
func (r *DefaultRunner) WorktreeAdd(ctx context.Context, dir, branch, path string) error {
ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout)
defer cancel()
args := []string{"worktree", "add", "--track", "-B", branch, path}
_, _, err := r.exec(ctx, dir, nil, args...)
return err
}
// WorktreeRemove 删除 path 这个 worktree(包含磁盘目录)。
// 即使 worktree 当前不存在 / 已被外部 rm 掉,调用 git worktree remove --force 也会成功
// 或返回特定 stderr,调用方据此决定是否吞错。
func (r *DefaultRunner) WorktreeRemove(ctx context.Context, dir, path string) error {
ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout)
defer cancel()
_, _, err := r.exec(ctx, dir, nil, "worktree", "remove", "--force", path)
return err
}
// WorktreeList 返回 dir 下所有 worktree 的元数据(含 main)。
func (r *DefaultRunner) WorktreeList(ctx context.Context, dir string) ([]Worktree, error) {
ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout)
defer cancel()
out, _, err := r.exec(ctx, dir, nil, "worktree", "list", "--porcelain")
if err != nil {
return nil, err
}
return parseWorktreeList(out), nil
}
// parseWorktreeList 解析 git worktree list --porcelain 输出。
// 每个 worktree 由若干 KV 行组成,记录间空行分隔:
//
// worktree /abs/path
// HEAD <sha>
// branch refs/heads/<name> (or "detached")
// bare (only for bare main)
func parseWorktreeList(buf []byte) []Worktree {
var out []Worktree
var cur Worktree
flush := func() {
if cur.Path != "" {
out = append(out, cur)
}
cur = Worktree{}
}
sc := bufio.NewScanner(bytes.NewReader(buf))
for sc.Scan() {
line := sc.Text()
if line == "" {
flush()
continue
}
switch {
case strings.HasPrefix(line, "worktree "):
cur.Path = strings.TrimPrefix(line, "worktree ")
case strings.HasPrefix(line, "HEAD "):
cur.HEAD = strings.TrimPrefix(line, "HEAD ")
case strings.HasPrefix(line, "branch "):
cur.Branch = strings.TrimPrefix(strings.TrimPrefix(line, "branch "), "refs/heads/")
case line == "bare":
cur.Bare = true
}
}
flush()
return out
}