Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ repositories:

`source` 使用绝对路径,`base` 必须在本地可解析,`worktree` 必须位于任务的 `worktrees/` 目录内。Taskflow 不隐式 fetch;请在 source 仓库准备好 base 后再重试 create。

首次通过 `--repo` 声明仓库时,Taskflow 默认读取该 source 的 `origin/HEAD`,并将其解析到本地可用的远程默认分支作为 base;同时生成 `feature/<task-id>` 分支。`origin/HEAD` 缺失或对应引用不可用时,create 会在写入初始配置或创建 worktree 前失败。已存在配置中的显式 `base` 和 `branch` 保持不变;已有配置的后续修改由用户或 AI 直接编辑 YAML。
首次通过 `--repo` 声明仓库时,Taskflow 默认读取该 source 的 `origin/HEAD`,并将其解析到本地可用的远程默认分支作为 base;同时生成 `feature/<task-id>` 分支,但只使用该远程分支的提交作为起点,不建立 upstream 关联。例如 `origin/HEAD` 指向 `origin/main` 时,配置中的 base 是 `origin/main`,但生成的 worktree 分支不会默认关联 `origin/main`;`origin/master` 等其他远程默认分支同理。`origin/HEAD` 缺失或对应引用不可用时,create 会在写入初始配置或创建 worktree 前失败。已存在配置中的显式 `base` 和 `branch` 保持不变;已有配置的后续修改由用户或 AI 直接编辑 YAML。

## 安全边界

Expand Down
7 changes: 4 additions & 3 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type DeleteOptions struct {
type createResolution struct {
task domain.Task
configurationChanged bool
trackBase bool
}

type deleteAction struct {
Expand Down Expand Up @@ -199,7 +200,7 @@ func (s Service) Create(ctx context.Context, o CreateOptions) (report.Result, re
res.Fail(report.Diagnostic{Code: "CREATE_WORKTREE_FAILED", Repo: repository.Name, Message: err.Error()})
return res, report.ExitPartial
}
if err = s.Git.AddWorktree(ctx, repository.Source, repository.Branch, target, repository.Base); err != nil {
if err = s.Git.AddWorktree(ctx, repository.Source, repository.Branch, target, repository.Base, resolved.trackBase); err != nil {
items[index].Status = "failed"
res.Data = createData(resolved.task, items, false)
res.Fail(report.Diagnostic{Code: "CREATE_WORKTREE_FAILED", Repo: repository.Name, Message: err.Error()})
Expand Down Expand Up @@ -623,7 +624,7 @@ func (s Service) resolveCreate(ctx context.Context, tasksRoot string, o CreateOp
if !configurationExists {
return createResolution{}, &report.Diagnostic{Code: "INVALID_ARGUMENT", Message: "at least one --repo is required for a new task"}, report.ExitConfig
}
return createResolution{task: task}, nil, report.ExitOK
return createResolution{task: task, trackBase: true}, nil, report.ExitOK
}
if configurationExists {
return createResolution{}, &report.Diagnostic{
Expand All @@ -647,7 +648,7 @@ func (s Service) resolveCreate(ctx context.Context, tasksRoot string, o CreateOp
if err := config.Validate(&task); err != nil {
return createResolution{}, &report.Diagnostic{Code: "INVALID_CONFIGURATION", Message: err.Error()}, report.ExitConfig
}
return createResolution{task: task, configurationChanged: true}, nil, report.ExitOK
return createResolution{task: task, configurationChanged: true, trackBase: false}, nil, report.ExitOK
}

func resolveRepository(taskID, raw string) (domain.Repository, error) {
Expand Down
24 changes: 22 additions & 2 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,37 @@ func (c Client) Worktrees(ctx context.Context, path string) ([]Worktree, error)
flush()
return result, nil
}
func (c Client) AddWorktree(ctx context.Context, source, branch, target, base string) error {
func (c Client) AddWorktree(ctx context.Context, source, branch, target, base string, trackBase bool) error {
args := []string{"-C", source, "worktree", "add"}
if c.HasRef(ctx, source, "refs/heads/"+branch) {
args = append(args, target, branch)
} else {
args = append(args, "-b", branch, target, base)
startingPoint := base
if !trackBase {
commit, err := c.ResolveCommit(ctx, source, base)
if err != nil {
return err
}
startingPoint = commit
}
args = append(args, "-b", branch, target, startingPoint)
}
_, err := c.Runner.Run(ctx, execx.CommandSpec{Executable: "git", Args: args})
return err
}

func (c Client) ResolveCommit(ctx context.Context, path, ref string) (string, error) {
r, err := c.Runner.Run(ctx, execx.CommandSpec{Executable: "git", Args: []string{"-C", path, "rev-parse", "--verify", ref + "^{commit}"}})
if err != nil {
return "", fmt.Errorf("resolve base %s: %s", ref, strings.TrimSpace(r.Stderr))
}
commit := strings.TrimSpace(r.Stdout)
if commit == "" {
return "", fmt.Errorf("resolve base %s: empty commit result", ref)
}
return commit, nil
}

func (c Client) RemoveWorktree(ctx context.Context, source, target string, force bool) error {
args := []string{"-C", source, "worktree", "remove"}
if force {
Expand Down
48 changes: 47 additions & 1 deletion internal/git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestClientInspectsAndCreatesWorktrees(t *testing.T) {
}
}
target := filepath.Join(t.TempDir(), "feature worktree")
if err := client.AddWorktree(context.Background(), root, "feature/test", target, "main"); err != nil {
if err := client.AddWorktree(context.Background(), root, "feature/test", target, "main", true); err != nil {
t.Fatal(err)
}
worktrees, err := client.Worktrees(context.Background(), root)
Expand Down Expand Up @@ -94,3 +94,49 @@ func TestClientDefaultBaseResolvesOriginHead(t *testing.T) {
t.Fatal("expected missing origin/HEAD error")
}
}

func TestClientAddWorktreeDefaultDoesNotTrackRemoteBase(t *testing.T) {
root := filepath.Join(t.TempDir(), "repo")
if out, err := exec.Command("git", "init", "-b", "master", root).CombinedOutput(); err != nil {
t.Fatalf("git init: %v: %s", err, out)
}
for _, args := range [][]string{
{"-C", root, "config", "user.email", "test@example.com"},
{"-C", root, "config", "user.name", "Test"},
{"-C", root, "commit", "--allow-empty", "-m", "init"},
{"-C", root, "remote", "add", "origin", "https://example.test/repo.git"},
{"-C", root, "update-ref", "refs/remotes/origin/master", "HEAD"},
} {
if out, err := exec.Command("git", args...).CombinedOutput(); err != nil {
t.Fatalf("git %v: %v: %s", args, err, out)
}
}

target := filepath.Join(t.TempDir(), "worktree")
client := Client{Runner: execx.OSRunner{}}
if err := client.AddWorktree(context.Background(), root, "feature/master-base", target, "origin/master", false); err != nil {
t.Fatal(err)
}
info, err := client.Inspect(context.Background(), target)
if err != nil {
t.Fatal(err)
}
if info.Upstream != "" {
t.Fatalf("upstream=%q, want no upstream", info.Upstream)
}
if info.Head == "" || !client.HasRef(context.Background(), root, "origin/master") {
t.Fatalf("worktree did not start from origin/master: info=%#v", info)
}

trackedTarget := filepath.Join(t.TempDir(), "tracked-worktree")
if err := client.AddWorktree(context.Background(), root, "feature/explicit-master", trackedTarget, "origin/master", true); err != nil {
t.Fatal(err)
}
trackedInfo, err := client.Inspect(context.Background(), trackedTarget)
if err != nil {
t.Fatal(err)
}
if trackedInfo.Upstream != "origin/master" {
t.Fatalf("explicit upstream=%q, want origin/master", trackedInfo.Upstream)
}
}
2 changes: 1 addition & 1 deletion skills/taskflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ taskflow --json --tasks-root <tasks-root> create <task-id> \

执行会先检查所有 source、base ref、branch 占用、target path 和 worktree identity,再写 taskflow.yaml、ownership.json 或运行 `git worktree add`。它不会删除、移动、reset 或覆盖现有路径;复用的手工 worktree不会获得 ownership。

新声明的仓库默认从 source 的 `origin/HEAD` 解析本地远程默认分支作为 base,并使用 `feature/<task-id>` 作为分支;Taskflow 不会隐式 fetch。若 `origin/HEAD` 缺失或目标引用不可用,先在 source 仓库修复远程引用后再重试。已有 taskflow.yaml 中明确配置的 `base` 和 `branch` 不会被覆盖。
新声明的仓库默认从 source 的 `origin/HEAD` 解析本地远程默认分支作为 base,并使用 `feature/<task-id>` 作为分支;创建新分支时只使用远程基线的提交作为起点,不建立 upstream 关联。例如 `origin/HEAD` 指向 `origin/main` 时,配置中的 base 是 `origin/main`,但 worktree 分支不会默认关联 `origin/main`;`origin/master` 等其他远程默认分支同理。Taskflow 不会隐式 fetch。若 `origin/HEAD` 缺失或目标引用不可用,先在 source 仓库修复远程引用后再重试。已有 taskflow.yaml 中明确配置的 `base` 和 `branch` 不会被覆盖。

已有任务可以直接重试:

Expand Down
Loading