feat(workspace): fetch+checkout when switching default_branch

This commit is contained in:
2026-06-10 18:00:18 +08:00
parent 3a1909f8f4
commit a4a6e6a01a
2 changed files with 84 additions and 1 deletions
+18 -1
View File
@@ -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)
}