package webhook import "encoding/json" // GiteaPushPayload represents a Gitea push webhook payload. // //nolint:tagliatelle // Field names match Gitea API (snake_case) type GiteaPushPayload struct { Ref string `json:"ref"` Before string `json:"before"` After string `json:"after"` CompareURL UnparsedURL `json:"compare_url"` Repository struct { FullName string `json:"full_name"` CloneURL UnparsedURL `json:"clone_url"` SSHURL string `json:"ssh_url"` HTMLURL UnparsedURL `json:"html_url"` } `json:"repository"` Pusher struct { Username string `json:"username"` Email string `json:"email"` } `json:"pusher"` Commits []struct { ID string `json:"id"` URL UnparsedURL `json:"url"` Message string `json:"message"` Author struct { Name string `json:"name"` Email string `json:"email"` } `json:"author"` } `json:"commits"` } // GitHubPushPayload represents a GitHub push webhook payload. // //nolint:tagliatelle // Field names match GitHub API (snake_case) type GitHubPushPayload struct { Ref string `json:"ref"` Before string `json:"before"` After string `json:"after"` CompareURL string `json:"compare"` Repository struct { FullName string `json:"full_name"` CloneURL UnparsedURL `json:"clone_url"` SSHURL string `json:"ssh_url"` HTMLURL UnparsedURL `json:"html_url"` } `json:"repository"` Pusher struct { Name string `json:"name"` Email string `json:"email"` } `json:"pusher"` HeadCommit *struct { ID string `json:"id"` URL UnparsedURL `json:"url"` Message string `json:"message"` } `json:"head_commit"` Commits []struct { ID string `json:"id"` URL UnparsedURL `json:"url"` Message string `json:"message"` Author struct { Name string `json:"name"` Email string `json:"email"` } `json:"author"` } `json:"commits"` } // GitLabPushPayload represents a GitLab push webhook payload. // //nolint:tagliatelle // Field names match GitLab API (snake_case) type GitLabPushPayload struct { Ref string `json:"ref"` Before string `json:"before"` After string `json:"after"` UserName string `json:"user_name"` UserEmail string `json:"user_email"` Project struct { PathWithNamespace string `json:"path_with_namespace"` GitHTTPURL UnparsedURL `json:"git_http_url"` GitSSHURL string `json:"git_ssh_url"` WebURL UnparsedURL `json:"web_url"` } `json:"project"` Commits []struct { ID string `json:"id"` URL UnparsedURL `json:"url"` Message string `json:"message"` Author struct { Name string `json:"name"` Email string `json:"email"` } `json:"author"` } `json:"commits"` } // ParsePushPayload parses a raw webhook payload into a normalized PushEvent // based on the detected webhook source. Returns an error if JSON unmarshaling // fails. For SourceUnknown, falls back to Gitea format for backward // compatibility. func ParsePushPayload(source Source, payload []byte) (*PushEvent, error) { switch source { case SourceGitHub: return parsePush(payload, githubPushEvent) case SourceGitLab: return parsePush(payload, gitlabPushEvent) case SourceGitea, SourceUnknown: // Gitea and unknown both use Gitea format for backward compatibility. return parsePush(payload, giteaPushEvent) } // Unreachable for known source values, but satisfies exhaustive checker. return parsePush(payload, giteaPushEvent) } // parsePush unmarshals payload into P and converts it into a normalized // PushEvent via build. func parsePush[P any](payload []byte, build func(P) *PushEvent) (*PushEvent, error) { var p P unmarshalErr := json.Unmarshal(payload, &p) if unmarshalErr != nil { return nil, unmarshalErr } return build(p), nil } // basePushEvent builds a PushEvent populated with the fields shared by all // webhook sources. func basePushEvent(source Source, ref, before, after string) *PushEvent { return &PushEvent{ Source: source, Ref: ref, Before: before, After: after, Branch: extractBranch(ref), } } // giteaPushEvent converts a Gitea push payload to a normalized PushEvent. func giteaPushEvent(p GiteaPushPayload) *PushEvent { event := basePushEvent(SourceGitea, p.Ref, p.Before, p.After) event.RepoName = p.Repository.FullName event.CloneURL = p.Repository.CloneURL event.HTMLURL = p.Repository.HTMLURL event.CommitURL = extractGiteaCommitURL(p) event.Pusher = p.Pusher.Username return event } // gitlabPushEvent converts a GitLab push payload to a normalized PushEvent. func gitlabPushEvent(p GitLabPushPayload) *PushEvent { event := basePushEvent(SourceGitLab, p.Ref, p.Before, p.After) event.RepoName = p.Project.PathWithNamespace event.CloneURL = p.Project.GitHTTPURL event.HTMLURL = p.Project.WebURL event.CommitURL = extractGitLabCommitURL(p) event.Pusher = p.UserName return event } // githubPushEvent converts a GitHub push payload to a normalized PushEvent. func githubPushEvent(p GitHubPushPayload) *PushEvent { event := basePushEvent(SourceGitHub, p.Ref, p.Before, p.After) event.RepoName = p.Repository.FullName event.CloneURL = p.Repository.CloneURL event.HTMLURL = p.Repository.HTMLURL event.CommitURL = extractGitHubCommitURL(p) event.Pusher = p.Pusher.Name return event } // extractBranch extracts the branch name from a git ref. func extractBranch(ref string) string { // refs/heads/main -> main const prefix = "refs/heads/" if len(ref) >= len(prefix) && ref[:len(prefix)] == prefix { return ref[len(prefix):] } return ref } // extractGiteaCommitURL extracts the commit URL from a Gitea push payload. // Prefers the URL from the head commit, falls back to constructing from repo URL. func extractGiteaCommitURL(payload GiteaPushPayload) UnparsedURL { for _, commit := range payload.Commits { if commit.ID == payload.After && commit.URL != "" { return commit.URL } } if payload.Repository.HTMLURL != "" && payload.After != "" { return UnparsedURL(payload.Repository.HTMLURL.String() + "/commit/" + payload.After) } return "" } // extractGitHubCommitURL extracts the commit URL from a GitHub push payload. // Prefers head_commit.url, then searches commits, then constructs from repo URL. func extractGitHubCommitURL(payload GitHubPushPayload) UnparsedURL { if payload.HeadCommit != nil && payload.HeadCommit.URL != "" { return payload.HeadCommit.URL } for _, commit := range payload.Commits { if commit.ID == payload.After && commit.URL != "" { return commit.URL } } if payload.Repository.HTMLURL != "" && payload.After != "" { return UnparsedURL(payload.Repository.HTMLURL.String() + "/commit/" + payload.After) } return "" } // extractGitLabCommitURL extracts the commit URL from a GitLab push payload. // Prefers commit URL from the commits list, falls back to constructing from // project web URL. func extractGitLabCommitURL(payload GitLabPushPayload) UnparsedURL { for _, commit := range payload.Commits { if commit.ID == payload.After && commit.URL != "" { return commit.URL } } if payload.Project.WebURL != "" && payload.After != "" { return UnparsedURL(payload.Project.WebURL.String() + "/-/commit/" + payload.After) } return "" }