From a4a6e6a01ad314d31312d5430c23f1de0360844f Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 10 Jun 2026 18:00:18 +0800 Subject: [PATCH] feat(workspace): fetch+checkout when switching default_branch --- internal/workspace/workspace_service.go | 19 +++++- internal/workspace/workspace_service_test.go | 66 ++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/internal/workspace/workspace_service.go b/internal/workspace/workspace_service.go index fea2aa3..7d10bfe 100644 --- a/internal/workspace/workspace_service.go +++ b/internal/workspace/workspace_service.go @@ -167,7 +167,24 @@ func (s *workspaceService) Update(ctx context.Context, c Caller, wsID uuid.UUID, if in.Description != nil { 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 { return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid default branch") } diff --git a/internal/workspace/workspace_service_test.go b/internal/workspace/workspace_service_test.go index e2c1c41..372de38 100644 --- a/internal/workspace/workspace_service_test.go +++ b/internal/workspace/workspace_service_test.go @@ -232,6 +232,24 @@ func (f *fakeGit) ListRemoteBranches(_ context.Context, _ string, _ *git.Credent 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{} 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.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) +}