package projectsync import ( "errors" "fmt" "path/filepath" "strings" "github.com/nomadcode/nomadcode-core/internal/db" "github.com/nomadcode/nomadcode-core/internal/workitem" ) var ErrInvalidConfig = errors.New("invalid project sync config") const ( DefaultSlotIndex SlotIndex = 0 MaxSlotIndex SlotIndex = 999 ) type ProviderProjectTarget struct { Provider workitem.ProviderID Tenant string Project string } type Config struct { Target ProviderProjectTarget GitRemoteURL string SourceBranch string WorkspaceID string WorkspaceBasePath string RepoDirName string } type SlotIndex int type SlotState string const ( SlotStateAvailable SlotState = "available" SlotStateInUse SlotState = "in_use" SlotStateDirty SlotState = "dirty" SlotStateError SlotState = "error" ) func (t ProviderProjectTarget) Normalize() (ProviderProjectTarget, error) { target := ProviderProjectTarget{ Provider: workitem.ProviderID(strings.TrimSpace(string(t.Provider))), Tenant: strings.TrimSpace(t.Tenant), Project: strings.TrimSpace(t.Project), } if target.Provider == "" || target.Tenant == "" || target.Project == "" { return ProviderProjectTarget{}, ErrInvalidConfig } return target, nil } func (c Config) Normalize() (Config, error) { target, err := c.Target.Normalize() if err != nil { return Config{}, err } basePath, err := normalizePath(c.WorkspaceBasePath) if err != nil { return Config{}, err } repoDir, err := normalizeRepoDirName(c.RepoDirName) if err != nil { return Config{}, err } config := Config{ Target: target, GitRemoteURL: strings.TrimSpace(c.GitRemoteURL), SourceBranch: strings.TrimSpace(c.SourceBranch), WorkspaceID: strings.TrimSpace(c.WorkspaceID), WorkspaceBasePath: basePath, RepoDirName: repoDir, } if config.GitRemoteURL == "" || config.SourceBranch == "" || config.WorkspaceID == "" { return Config{}, ErrInvalidConfig } return config, nil } func (c Config) ProjectWorkspaceRoot() (string, error) { config, err := c.Normalize() if err != nil { return "", err } return ProjectWorkspaceRoot(config.WorkspaceBasePath, config.RepoDirName) } func (c Config) SlotWorkspacePath(slot SlotIndex) (string, error) { config, err := c.Normalize() if err != nil { return "", err } return SlotWorkspacePath(config.WorkspaceBasePath, config.RepoDirName, slot) } func NewSlotIndex(index int) (SlotIndex, error) { slot := SlotIndex(index) if !slot.Valid() { return 0, ErrInvalidConfig } return slot, nil } func (s SlotIndex) Valid() bool { return s >= DefaultSlotIndex && s <= MaxSlotIndex } func (s SlotIndex) String() string { return fmt.Sprintf("%03d", int(s)) } func (s SlotIndex) PathName() (string, error) { if !s.Valid() { return "", ErrInvalidConfig } return s.String(), nil } func (s SlotState) Valid() bool { switch s { case SlotStateAvailable, SlotStateInUse, SlotStateDirty, SlotStateError: return true default: return false } } func (s SlotState) Allocatable() bool { return s == SlotStateAvailable } func ProjectWorkspaceRoot(workspaceBasePath, repoDirName string) (string, error) { basePath, err := normalizePath(workspaceBasePath) if err != nil { return "", err } repoDir, err := normalizeRepoDirName(repoDirName) if err != nil { return "", err } return filepath.Join(basePath, repoDir), nil } func SlotWorkspacePath(workspaceBasePath, repoDirName string, slot SlotIndex) (string, error) { slotName, err := slot.PathName() if err != nil { return "", err } root, err := ProjectWorkspaceRoot(workspaceBasePath, repoDirName) if err != nil { return "", err } return filepath.Join(root, slotName), nil } func normalizePath(value string) (string, error) { trimmed := strings.TrimSpace(value) if trimmed == "" { return "", ErrInvalidConfig } cleaned := filepath.Clean(trimmed) if cleaned == "." { return "", ErrInvalidConfig } return cleaned, nil } func normalizeRepoDirName(value string) (string, error) { trimmed := strings.TrimSpace(value) if trimmed == "" { return "", ErrInvalidConfig } cleaned := filepath.Clean(trimmed) if cleaned == "." || cleaned == ".." || filepath.IsAbs(cleaned) || strings.ContainsAny(cleaned, `/\`) { return "", ErrInvalidConfig } return cleaned, nil } func (c Config) ToCreateProjectSyncParams() db.CreateProjectSyncSettingParams { return db.CreateProjectSyncSettingParams{ Provider: string(c.Target.Provider), Tenant: c.Target.Tenant, Project: c.Target.Project, GitRemoteUrl: c.GitRemoteURL, SourceBranch: c.SourceBranch, WorkspaceID: c.WorkspaceID, WorkspaceBasePath: c.WorkspaceBasePath, RepoDirName: c.RepoDirName, } } func ConfigFromDBRecord(r db.ProjectSyncSetting) Config { return Config{ Target: ProviderProjectTarget{ Provider: workitem.ProviderID(r.Provider), Tenant: r.Tenant, Project: r.Project, }, GitRemoteURL: r.GitRemoteUrl, SourceBranch: r.SourceBranch, WorkspaceID: r.WorkspaceID, WorkspaceBasePath: r.WorkspaceBasePath, RepoDirName: r.RepoDirName, } }