1use serde::Deserialize;
2
3#[derive(Debug, Clone, Deserialize)]
4pub struct PullRequestEvent {
5 pub action: String,
6 pub number: Option<u64>,
7 pub pull_request: Option<PullRequest>,
8 pub repository: Repository,
9}
10
11#[derive(Debug, Clone, Deserialize)]
12pub struct PullRequest {
13 pub number: u64,
14 pub head: GitRef,
15 pub base: GitRef,
16 pub html_url: Option<String>,
17 pub title: Option<String>,
18}
19
20#[derive(Debug, Clone, Deserialize)]
21pub struct GitRef {
22 pub sha: String,
23 pub r#ref: String,
24 #[serde(default)]
25 pub repo: Option<HeadRepo>,
26}
27
28#[derive(Debug, Clone, Deserialize)]
29pub struct HeadRepo {
30 pub clone_url: String,
31}
32
33#[derive(Debug, Clone, Deserialize)]
34pub struct Repository {
35 pub full_name: String,
36 pub clone_url: String,
37 pub html_url: Option<String>,
38 pub owner: Owner,
39 pub name: String,
40}
41
42#[derive(Debug, Clone, Deserialize)]
43pub struct Owner {
44 pub login: String,
45}
46
47#[derive(Debug, Clone)]
48pub struct PrContext {
49 pub provider: ProviderKind,
50 pub action: String,
51 pub owner: String,
52 pub repo: String,
53 pub pr_number: u64,
54 pub head_sha: String,
55 pub head_ref: String,
56 pub base_ref: String,
57 pub clone_url: String,
58 pub head_clone_url: String,
59 pub pr_url: Option<String>,
60 pub title: Option<String>,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum ProviderKind {
65 GitHub,
66 Gitea,
67}
68
69impl PullRequestEvent {
70 pub fn into_context(self, provider: ProviderKind) -> Option<PrContext> {
71 if !is_action_supported(&self.action) {
72 return None;
73 }
74 let pr = self.pull_request?;
75 let clone_url = self.repository.clone_url;
76 Some(PrContext {
77 provider,
78 action: self.action,
79 owner: self.repository.owner.login,
80 repo: self.repository.name,
81 pr_number: pr.number,
82 head_sha: pr.head.sha,
83 head_ref: pr.head.r#ref,
84 base_ref: pr.base.r#ref,
85 head_clone_url: pr
86 .head
87 .repo
88 .as_ref()
89 .map(|r| r.clone_url.clone())
90 .unwrap_or_else(|| clone_url.clone()),
91 clone_url,
92 pr_url: pr.html_url,
93 title: pr.title,
94 })
95 }
96}
97
98fn is_action_supported(action: &str) -> bool {
99 matches!(action, "opened" | "synchronize" | "reopened" | "edited")
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn parses_opened_pull_request() {
108 let body = r#"{
109 "action": "opened",
110 "pull_request": {
111 "number": 42,
112 "head": { "sha": "abc123", "ref": "feature" },
113 "base": { "sha": "def456", "ref": "main" },
114 "html_url": "https://github.com/org/repo/pull/42"
115 },
116 "repository": {
117 "full_name": "org/repo",
118 "name": "repo",
119 "clone_url": "https://github.com/org/repo.git",
120 "owner": { "login": "org" }
121 }
122 }"#;
123 let event: PullRequestEvent = serde_json::from_str(body).unwrap();
124 let ctx = event.into_context(ProviderKind::GitHub).unwrap();
125 assert_eq!(ctx.pr_number, 42);
126 assert_eq!(ctx.head_sha, "abc123");
127 assert_eq!(ctx.base_ref, "main");
128 }
129
130 #[test]
131 fn ignores_closed_action() {
132 let body = r#"{
133 "action": "closed",
134 "pull_request": {
135 "number": 1,
136 "head": { "sha": "a", "ref": "b" },
137 "base": { "sha": "c", "ref": "main" }
138 },
139 "repository": {
140 "full_name": "o/r",
141 "name": "r",
142 "clone_url": "https://example.com/o/r.git",
143 "owner": { "login": "o" }
144 }
145 }"#;
146 let event: PullRequestEvent = serde_json::from_str(body).unwrap();
147 assert!(event.into_context(ProviderKind::GitHub).is_none());
148 }
149}