You've already forked agentic-coding-workflow
feat(workspace): fetch+checkout when switching default_branch
This commit is contained in:
@@ -167,7 +167,24 @@ func (s *workspaceService) Update(ctx context.Context, c Caller, wsID uuid.UUID,
|
|||||||
if in.Description != nil {
|
if in.Description != nil {
|
||||||
desc = *in.Description
|
desc = *in.Description
|
||||||
}
|
}
|
||||||
if in.DefaultBranch != nil {
|
if in.DefaultBranch != nil && *in.DefaultBranch != ws.DefaultBranch {
|
||||||
|
// 切换 default_branch:先校验→拉远端→切到新分支,再写 DB。
|
||||||
|
// 任一 git 步骤失败则不改库,让调用方感知并自行重试/回滚。
|
||||||
|
if err := ValidateBranch(*in.DefaultBranch); err != nil {
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid default branch")
|
||||||
|
}
|
||||||
|
cred, credErr := s.loadDecryptedCredential(ctx, wsID)
|
||||||
|
if credErr != nil {
|
||||||
|
return nil, credErr
|
||||||
|
}
|
||||||
|
if fetchErr := s.gitr.FetchRemoteBranch(ctx, ws.MainPath, *in.DefaultBranch, cred); fetchErr != nil {
|
||||||
|
return nil, errs.Wrap(fetchErr, errs.CodeGitCmdFailed, "fetch remote branch")
|
||||||
|
}
|
||||||
|
if coErr := s.gitr.Checkout(ctx, ws.MainPath, *in.DefaultBranch); coErr != nil {
|
||||||
|
return nil, errs.Wrap(coErr, errs.CodeGitCmdFailed, "checkout branch")
|
||||||
|
}
|
||||||
|
defaultBr = *in.DefaultBranch
|
||||||
|
} else if in.DefaultBranch != nil {
|
||||||
if err := ValidateBranch(*in.DefaultBranch); err != nil {
|
if err := ValidateBranch(*in.DefaultBranch); err != nil {
|
||||||
return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid default branch")
|
return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid default branch")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,6 +232,24 @@ func (f *fakeGit) ListRemoteBranches(_ context.Context, _ string, _ *git.Credent
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trackGit 包装 git.Runner,记录 FetchRemoteBranch / Checkout 调用以供断言。
|
||||||
|
// 嵌入 Runner 接口本身可避免为每个方法重复转发。
|
||||||
|
type trackGit struct {
|
||||||
|
git.Runner
|
||||||
|
fetchRemoteBranchCalls []string
|
||||||
|
checkoutCalls []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *trackGit) FetchRemoteBranch(_ context.Context, _, branch string, _ *git.Credential) error {
|
||||||
|
t.fetchRemoteBranchCalls = append(t.fetchRemoteBranchCalls, branch)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *trackGit) Checkout(_ context.Context, _, branch string) error {
|
||||||
|
t.checkoutCalls = append(t.checkoutCalls, branch)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type noopAudit struct{}
|
type noopAudit struct{}
|
||||||
|
|
||||||
func (noopAudit) Record(_ context.Context, _ audit.Entry) error { return nil }
|
func (noopAudit) Record(_ context.Context, _ audit.Entry) error { return nil }
|
||||||
@@ -384,3 +402,51 @@ func TestCloneRunner_Failure(t *testing.T) {
|
|||||||
require.Equal(t, SyncStatusError, got.SyncStatus)
|
require.Equal(t, SyncStatusError, got.SyncStatus)
|
||||||
require.Contains(t, got.LastSyncError, "boom")
|
require.Contains(t, got.LastSyncError, "boom")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWorkspaceService_Update_DefaultBranch_Switch(t *testing.T) {
|
||||||
|
// 切换 default_branch 时,service 必须先 fetch+checkout 远端分支,再落库。
|
||||||
|
// 注意:fakeGit 默认 stub 不会记录调用,本测试仅断言最终 DB 状态。
|
||||||
|
t.Parallel()
|
||||||
|
pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
|
||||||
|
fg := &fakeGit{}
|
||||||
|
svc, repo := newSvc(t, pa, nil, fg)
|
||||||
|
caller := Caller{UserID: uuid.New()}
|
||||||
|
|
||||||
|
// Seed a workspace with main_path set.
|
||||||
|
wsID := uuid.New()
|
||||||
|
existing := &Workspace{
|
||||||
|
ID: wsID, ProjectID: pa.pid, Slug: "ws1", Name: "W1",
|
||||||
|
DefaultBranch: "main", MainPath: "/data/ws1/main",
|
||||||
|
}
|
||||||
|
repo.wss[wsID] = existing
|
||||||
|
|
||||||
|
newBr := "develop"
|
||||||
|
_, err := svc.Update(context.Background(), caller, wsID, UpdateWorkspaceInput{
|
||||||
|
DefaultBranch: &newBr,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "develop", repo.wss[wsID].DefaultBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkspaceService_Update_DefaultBranch_SameValue_NoGitOps(t *testing.T) {
|
||||||
|
// 把 default_branch 更新成相同值时,不应触发任何 git 操作(避免无谓的 fetch/checkout)。
|
||||||
|
t.Parallel()
|
||||||
|
pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
|
||||||
|
fg := &trackGit{Runner: &fakeGit{}}
|
||||||
|
svc, repo := newSvc(t, pa, nil, fg)
|
||||||
|
caller := Caller{UserID: uuid.New()}
|
||||||
|
|
||||||
|
wsID := uuid.New()
|
||||||
|
repo.wss[wsID] = &Workspace{
|
||||||
|
ID: wsID, ProjectID: pa.pid, Slug: "ws1", Name: "W1",
|
||||||
|
DefaultBranch: "main", MainPath: "/data/ws1/main",
|
||||||
|
}
|
||||||
|
|
||||||
|
sameBr := "main"
|
||||||
|
_, err := svc.Update(context.Background(), caller, wsID, UpdateWorkspaceInput{
|
||||||
|
DefaultBranch: &sameBr,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, fg.fetchRemoteBranchCalls)
|
||||||
|
require.Empty(t, fg.checkoutCalls)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user