Files
kandev/apps/backend/internal/github/copy.go
T
2026-07-27 18:10:12 +01:00

91 lines
3.8 KiB
Go

package github
import (
"context"
"errors"
"fmt"
"strings"
)
// ErrSameWorkspace is returned when a copy targets the same workspace it reads
// from.
var ErrSameWorkspace = errors.New("github: source and target workspaces are the same")
// CopyWorkspaceSettingsToWorkspace copies the per-workspace GitHub operational
// settings (repo scope + saved/default query presets) and the workspace's
// quick-action presets from sourceWorkspaceID to targetWorkspaceID. Workspace
// automation and personal authentication are intentionally excluded. Watchers
// are also out of scope.
func (s *Service) CopyWorkspaceSettingsToWorkspace(ctx context.Context, sourceWorkspaceID, targetWorkspaceID string) (*WorkspaceSettings, error) {
sourceWorkspaceID = strings.TrimSpace(sourceWorkspaceID)
targetWorkspaceID = strings.TrimSpace(targetWorkspaceID)
if sourceWorkspaceID == "" || targetWorkspaceID == "" {
return nil, fmt.Errorf("%w: source and target workspace ids are required", ErrWorkspaceSettingsValidation)
}
if sourceWorkspaceID == targetWorkspaceID {
return nil, ErrSameWorkspace
}
if err := s.authorizeWorkspaceAccess(ctx, sourceWorkspaceID); err != nil {
return nil, err
}
if err := s.authorizeWorkspaceAccess(ctx, targetWorkspaceID); err != nil {
return nil, err
}
if s.store == nil {
return nil, fmt.Errorf("github store not configured")
}
// GetWorkspaceSettings returns defaults (never nil) for a missing row.
source, err := s.store.GetWorkspaceSettings(ctx, sourceWorkspaceID)
if err != nil {
return nil, fmt.Errorf("read source github settings: %w", err)
}
target := &WorkspaceSettings{
WorkspaceID: targetWorkspaceID,
TaskGitCredentialsMode: source.TaskGitCredentialsMode,
RepoScopeMode: source.RepoScopeMode,
RepoScopeOrgs: append([]string(nil), source.RepoScopeOrgs...),
RepoScopeRepos: append([]RepoFilter(nil), source.RepoScopeRepos...),
SavedPresets: cloneRawMessage(source.SavedPresets),
DefaultQueryPresets: cloneRawMessage(source.DefaultQueryPresets),
}
// Copy the action presets before the workspace-settings write. These are two
// separate store writes without a shared transaction, so ordering the
// preset copy first means a preset failure leaves the target completely
// untouched; only a failure of the final settings write can leave a partial
// copy, and a full retry overwrites both.
if err := s.copyActionPresets(ctx, sourceWorkspaceID, targetWorkspaceID); err != nil {
return nil, err
}
if err := s.store.UpsertWorkspaceSettings(ctx, target); err != nil {
return nil, fmt.Errorf("write target github settings: %w", err)
}
return s.store.GetWorkspaceSettings(ctx, targetWorkspaceID)
}
// copyActionPresets duplicates the source workspace's stored quick-action
// presets onto the target. When the source has no stored row it relies on
// built-in defaults, so the target's row is cleared as well — otherwise a copy
// would leave the target on its own customised presets while reporting success,
// diverging from the source. Both then fall back to the same defaults on read.
func (s *Service) copyActionPresets(ctx context.Context, sourceWorkspaceID, targetWorkspaceID string) error {
stored, err := s.store.GetActionPresets(ctx, sourceWorkspaceID)
if err != nil {
return fmt.Errorf("read source github action presets: %w", err)
}
if stored == nil {
if err := s.store.DeleteActionPresets(ctx, targetWorkspaceID); err != nil {
return fmt.Errorf("clear target github action presets: %w", err)
}
return nil
}
target := &ActionPresets{
WorkspaceID: targetWorkspaceID,
PR: append([]ActionPreset(nil), stored.PR...),
Issue: append([]ActionPreset(nil), stored.Issue...),
}
if err := s.store.UpsertActionPresets(ctx, target); err != nil {
return fmt.Errorf("write target github action presets: %w", err)
}
return nil
}