git workspace逻辑

This commit is contained in:
2026-06-10 16:06:55 +08:00
parent 023ab2f30e
commit c6f239f424
4 changed files with 70 additions and 2 deletions
+15
View File
@@ -228,6 +228,21 @@ func (s *workspaceService) Sync(ctx context.Context, c Caller, wsID uuid.UUID) (
}
// 状态回写与请求生命周期解耦:客户端断开/请求超时也要能写回状态,避免永久卡 syncing。
writeCtx := context.WithoutCancel(ctx)
// main 仓库目录缺失(历史 clone 从未成功 / 数据卷丢失)时 fetch 注定失败,
// 转为重新 clone 自愈:状态切回 cloning 并调度 CloneRunner,由其落 idle/error。
if _, statErr := os.Stat(ws.MainPath); errors.Is(statErr, os.ErrNotExist) {
if s.clones == nil {
_, _ = s.repo.UpdateWorkspaceSyncStatus(writeCtx, wsID, SyncStatusError, ws.LastSyncedAt, "main repo missing on disk; clone scheduler unavailable")
return nil, errs.New(errs.CodeGitCmdFailed, "main repo missing on disk")
}
updated, err := s.repo.UpdateWorkspaceSyncStatus(writeCtx, wsID, SyncStatusCloning, ws.LastSyncedAt, "")
if err != nil {
return nil, err
}
s.audit(writeCtx, &c.UserID, "workspace.reclone", "workspace", wsID.String(), map[string]any{"reason": "main path missing"})
s.clones.Schedule(wsID)
return updated, nil
}
fetchCtx, cancel := context.WithTimeout(ctx, s.fetchTimout)
defer cancel()
if fetchErr := s.gitr.Fetch(fetchCtx, ws.MainPath, cred); fetchErr != nil {
+34 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"log/slog"
"path/filepath"
"sync"
"testing"
"time"
@@ -79,7 +80,21 @@ func (f *fakeRepo) ResetStuckSyncStatuses(_ context.Context) ([]uuid.UUID, error
return nil, nil
}
func (f *fakeRepo) ListFetchTargets(_ context.Context) ([]FetchTarget, error) { return nil, nil }
func (f *fakeRepo) MarkSyncing(_ context.Context, _ uuid.UUID) (bool, error) { return false, nil }
// MarkSyncing 模拟真实实现的 CAS 语义:仅 idle/error 可抢到锁并置为 syncing。
func (f *fakeRepo) MarkSyncing(_ context.Context, id uuid.UUID) (bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
w, ok := f.wss[id]
if !ok {
return false, errors.New("not found")
}
if w.SyncStatus != SyncStatusIdle && w.SyncStatus != SyncStatusError {
return false, nil
}
w.SyncStatus = SyncStatusSyncing
return true, nil
}
func (f *fakeRepo) MarkFetchResult(_ context.Context, _ uuid.UUID, _ bool, _ string) error {
return nil
}
@@ -312,6 +327,24 @@ func TestWorkspaceService_Sync_BlockedWhenCloning(t *testing.T) {
require.Error(t, err)
}
func TestWorkspaceService_Sync_MissingMainPath_SchedulesReclone(t *testing.T) {
// main 仓库目录不存在(历史 clone 失败 / 数据卷丢失)时,Sync 不做 fetch,
// 而是状态切回 cloning 并调度 CloneRunner 重新 clone 自愈。
t.Parallel()
pa := &fakePA{pid: uuid.New(), canWrite: true, canRead: true}
sched := &fakeSched{}
svc, repo := newSvc(t, pa, sched, &fakeGit{})
id := uuid.New()
repo.wss[id] = &Workspace{
ID: id, ProjectID: pa.pid, SyncStatus: SyncStatusError,
MainPath: filepath.Join(t.TempDir(), "gone", "main"),
}
ws, err := svc.Sync(context.Background(), Caller{UserID: uuid.New()}, id)
require.NoError(t, err)
require.Equal(t, SyncStatusCloning, ws.SyncStatus)
require.Equal(t, []uuid.UUID{id}, sched.called)
}
func TestCloneRunner_HappyPath(t *testing.T) {
t.Parallel()
pa := &fakePA{pid: uuid.New(), canWrite: true, canRead: true}