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 parseGitHubPush(payload) case SourceGitLab: return parseGitLabPush(payload) case SourceGitea, SourceUnknown: // Gitea and unknown both use Gitea format for backward compatibility. return parseGiteaPush(payload) } // Unreachable for known source values, but satisfies exhaustive checker. return parseGiteaPush(payload) } func parseGiteaPush(payload []byte) (*PushEvent, error) { var p GiteaPushPayload unmarshalErr := json.Unmarshal(payload, &p) if unmarshalErr != nil { return nil, unmarshalErr } commitURL := extractGiteaCommitURL(p) return &PushEvent{ Source: SourceGitea, Ref: p.Ref, Before: p.Before, After: p.After, Branch: extractBranch(p.Ref), RepoName: p.Repository.FullName, CloneURL: p.Repository.CloneURL, HTMLURL: p.Repository.HTMLURL, CommitURL: commitURL, Pusher: p.Pusher.Username, }, nil } func parseGitHubPush(payload []byte) (*PushEvent, error) { var p GitHubPushPayload unmarshalErr := json.Unmarshal(payload, &p) if unmarshalErr != nil { return nil, unmarshalErr } commitURL := extractGitHubCommitURL(p) return &PushEvent{ Source: SourceGitHub, Ref: p.Ref, Before: p.Before, After: p.After, Branch: extractBranch(p.Ref), RepoName: p.Repository.FullName, CloneURL: p.Repository.CloneURL, HTMLURL: p.Repository.HTMLURL, CommitURL: commitURL, Pusher: p.Pusher.Name, }, nil } func parseGitLabPush(payload []byte) (*PushEvent, error) { var p GitLabPushPayload unmarshalErr := json.Unmarshal(payload, &p) if unmarshalErr != nil { return nil, unmarshalErr } commitURL := extractGitLabCommitURL(p) return &PushEvent{ Source: SourceGitLab, Ref: p.Ref, Before: p.Before, After: p.After, Branch: extractBranch(p.Ref), RepoName: p.Project.PathWithNamespace, CloneURL: p.Project.GitHTTPURL, HTMLURL: p.Project.WebURL, CommitURL: commitURL, Pusher: p.UserName, }, nil } // 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 "" }