package provider import ( "context" "strings" "time" "git.toki-labs.com/toki/gito/services/core/internal/core" ) type ProviderID string const ( ProviderGitHub ProviderID = "github" ProviderGitLab ProviderID = "gitlab" ProviderGitea ProviderID = "gitea" ProviderForgejo ProviderID = "forgejo" ProviderPlane ProviderID = "plane" ProviderJira ProviderID = "jira" ) type Capability uint64 const ( CapabilityChangeRequest Capability = 1 << iota CapabilityWebhook CapabilityChecks CapabilityComment ) func (c Capability) Has(flag Capability) bool { return flag != 0 && c&flag == flag } type Config struct { ID ProviderID Endpoint string CredentialRef string Capabilities Capability CreatedAt time.Time UpdatedAt time.Time } func (c Config) Supports(flag Capability) bool { return c.Capabilities.Has(flag) } func (c Config) Normalized() Config { c.ID = ProviderID(strings.ToLower(strings.TrimSpace(string(c.ID)))) c.Endpoint = strings.TrimSpace(c.Endpoint) c.CredentialRef = strings.TrimSpace(c.CredentialRef) return c } type ChangeRequestAdapter interface { CreateChangeRequest(ctx context.Context, input core.ChangeRequestCreate) (core.ChangeRequest, error) UpdateChangeRequest(ctx context.Context, id string, input core.ChangeRequestUpdate) (core.ChangeRequest, error) CommentChangeRequest(ctx context.Context, id string, input core.ChangeRequestComment) error RequestChangeRequestReview(ctx context.Context, id string, input core.ChangeRequestReviewRequest) error MergeChangeRequest(ctx context.Context, id string, input core.ChangeRequestMerge) (core.ChangeRequest, error) CloseChangeRequest(ctx context.Context, id string, input core.ChangeRequestClose) (core.ChangeRequest, error) } type ChangeRequestMappingCandidate struct { Provider ProviderID ProviderObject string Fields []ChangeRequestFieldMapping States []ChangeRequestStateMapping Actions []core.ChangeRequestAction ExtensionFields []ChangeRequestExtensionField } type ChangeRequestFieldMapping struct { Target string Source string Notes string } type ChangeRequestStateMapping struct { State core.ChangeRequestState Source string Notes string } // ChangeRequestExtensionField describes provider-owned metadata that adapters // keep outside core.ChangeRequest until a later milestone promotes it. type ChangeRequestExtensionField struct { Name string Source string Notes string } func ChangeRequestMappingCandidates() []ChangeRequestMappingCandidate { return []ChangeRequestMappingCandidate{ GitHubChangeRequestMapping(), GitLabChangeRequestMapping(), GiteaChangeRequestMapping(), } } func GitHubChangeRequestMapping() ChangeRequestMappingCandidate { return ChangeRequestMappingCandidate{ Provider: ProviderGitHub, ProviderObject: "pull_request", Fields: []ChangeRequestFieldMapping{ {Target: "provider", Source: string(ProviderGitHub)}, {Target: "external_id", Source: "number/pull_number"}, {Target: "repo_id", Source: "Gito repo binding"}, {Target: "source_branch", Source: "head"}, {Target: "target_branch", Source: "base"}, {Target: "title", Source: "title"}, {Target: "body", Source: "body"}, {Target: "draft", Source: "draft"}, {Target: "state", Source: "state + merged check"}, }, States: []ChangeRequestStateMapping{ {State: core.ChangeRequestOpen, Source: "state=open"}, {State: core.ChangeRequestMerged, Source: "merged=true or merge endpoint success"}, {State: core.ChangeRequestClosed, Source: "state=closed and merged=false"}, }, Actions: changeRequestActions(), ExtensionFields: []ChangeRequestExtensionField{ {Name: "head_repo", Source: "head_repo", Notes: "cross-repository create metadata"}, {Name: "maintainer_can_modify", Source: "maintainer_can_modify"}, {Name: "merge_method", Source: "merge_method"}, {Name: "issue", Source: "issue", Notes: "issue conversion and issue-only fields stay adapter-owned"}, }, } } func GitLabChangeRequestMapping() ChangeRequestMappingCandidate { return ChangeRequestMappingCandidate{ Provider: ProviderGitLab, ProviderObject: "merge_request", Fields: []ChangeRequestFieldMapping{ {Target: "provider", Source: string(ProviderGitLab)}, {Target: "external_id", Source: "iid"}, {Target: "repo_id", Source: "Gito repo binding"}, {Target: "source_branch", Source: "source_branch"}, {Target: "target_branch", Source: "target_branch"}, {Target: "title", Source: "title"}, {Target: "body", Source: "description"}, {Target: "draft", Source: "draft"}, {Target: "state", Source: "state"}, }, States: []ChangeRequestStateMapping{ {State: core.ChangeRequestOpen, Source: "state=opened"}, {State: core.ChangeRequestMerged, Source: "state=merged"}, {State: core.ChangeRequestClosed, Source: "state=closed"}, }, Actions: changeRequestActions(), ExtensionFields: []ChangeRequestExtensionField{ {Name: "project_id", Source: "project_id/source_project_id/target_project_id"}, {Name: "detailed_merge_status", Source: "detailed_merge_status"}, {Name: "squash_on_merge", Source: "squash_on_merge"}, {Name: "merge_after", Source: "merge_after"}, {Name: "blocking_discussions_resolved", Source: "blocking_discussions_resolved"}, }, } } func GiteaChangeRequestMapping() ChangeRequestMappingCandidate { return ChangeRequestMappingCandidate{ Provider: ProviderGitea, ProviderObject: "pull_request", Fields: []ChangeRequestFieldMapping{ {Target: "provider", Source: string(ProviderGitea)}, {Target: "external_id", Source: "number"}, {Target: "repo_id", Source: "Gito repo binding"}, {Target: "source_branch", Source: "head"}, {Target: "target_branch", Source: "base"}, {Target: "title", Source: "title"}, {Target: "body", Source: "body"}, {Target: "draft", Source: "draft"}, {Target: "state", Source: "state + merged"}, }, States: []ChangeRequestStateMapping{ {State: core.ChangeRequestOpen, Source: "state=open"}, {State: core.ChangeRequestMerged, Source: "merged=true"}, {State: core.ChangeRequestClosed, Source: "state=closed and merged=false"}, }, Actions: changeRequestActions(), ExtensionFields: []ChangeRequestExtensionField{ {Name: "mergeable", Source: "mergeable"}, {Name: "allow_maintainer_edit", Source: "allow_maintainer_edit"}, {Name: "merge_style", Source: "merge option style"}, {Name: "deadline", Source: "due_date"}, {Name: "requested_reviewers", Source: "requested_reviewers/requested_reviewers_teams"}, }, } } func changeRequestActions() []core.ChangeRequestAction { return []core.ChangeRequestAction{ core.ChangeRequestActionCreate, core.ChangeRequestActionUpdate, core.ChangeRequestActionComment, core.ChangeRequestActionRequestReview, core.ChangeRequestActionMerge, core.ChangeRequestActionClose, } } type WebhookEvent struct { Provider string EventType string ExternalID string Payload []byte } // WebhookRequest represents a provider webhook with all metadata needed // for signature verification and normalization. type WebhookRequest struct { Provider ProviderID EventType string ExternalID string Payload []byte Headers map[string][]string Query map[string][]string ReceivedAt time.Time } // WebhookCandidate represents a provider-neutral normalization candidate // that contains minimal metadata without requiring provider secrets. type WebhookCandidate struct { Type string Provider ProviderID ExternalID string Revision *core.RevisionEvent } // WebhookVerifier defines the interface for verifying webhook signatures. type WebhookVerifier interface { VerifyWebhook(ctx context.Context, req WebhookRequest) error } // WebhookNormalizer defines the interface for normalizing webhook events // into provider-neutral candidates. type WebhookNormalizer interface { NormalizeWebhook(ctx context.Context, req WebhookRequest) ([]WebhookCandidate, error) } // WebhookAdapter combines verification and normalization capabilities. type WebhookAdapter interface { WebhookVerifier WebhookNormalizer }