1
use chrono::{DateTime, Utc};
2
use serde::{Deserialize, Serialize};
3

            
4
#[cfg(test)]
5
mod test;
6

            
7
/// Name of check-runs created by the bot
8
pub const CHECK_RUN_NAME: &str = "cerberus-mergeguard";
9
/// Status for unfinished check-runs from the bot
10
/// Using 'queued', because while 'pending' is valid according to docs, the actual API does not allow it.
11
pub const CHECK_RUN_INITIAL_STATUS: &str = "queued";
12
/// Status for completed check-runs from the bot
13
pub const CHECK_RUN_COMPLETED_STATUS: &str = "completed";
14
/// Conclusion for completed check-runs from the bot
15
pub const CHECK_RUN_CONCLUSION: &str = "success";
16
/// Title for unfinished check-runs from the bot
17
pub const CHECK_RUN_INITIAL_TITLE: &str = "Waiting for other checks to complete";
18
/// Title for completed check-runs from the bot
19
pub const CHECK_RUN_COMPLETED_TITLE: &str = "All status checks have passed";
20
/// Summary for check-runs from the bot
21
pub const CHECK_RUN_SUMMARY: &str = "Will block merging until all other checks have completed";
22

            
23
/// Partial fields of a pull_request event webhook payload.
24
#[derive(Debug, Serialize, Deserialize)]
25
pub struct PullRequestEvent {
26
    pub action: String,
27
    pub installation: Option<Installation>,
28
    pub number: u64,
29
    pub pull_request: PullRequest,
30
    pub repository: Repo,
31
}
32

            
33
/// Partial fields of a check_run event webhook payload.
34
#[derive(Debug, Serialize, Deserialize)]
35
pub struct CheckRunEvent {
36
    pub action: String,
37
    pub check_run: CheckRun,
38
    pub installation: Option<Installation>,
39
    pub repository: Repo,
40
}
41

            
42
/// Partial fields of an issue_comment event webhook payload.
43
#[derive(Debug, Serialize, Deserialize)]
44
pub struct IssueCommentEvent {
45
    pub action: String,
46
    pub issue: Issue,
47
    pub comment: Comment,
48
    pub installation: Option<Installation>,
49
    pub repository: Repo,
50
}
51

            
52
/// Partial fields of a pull_request object.
53
#[derive(Debug, Serialize, Deserialize)]
54
pub struct PullRequest {
55
    pub number: u64,
56
    pub title: String,
57
    pub head: BranchRef,
58
}
59

            
60
/// Partial fields of a branch reference object.
61
#[derive(Debug, Serialize, Deserialize)]
62
pub struct BranchRef {
63
    pub label: String,
64
    #[serde(rename = "ref")]
65
    pub ref_field: String,
66
    pub sha: String,
67
    pub repo: Repo,
68
}
69

            
70
/// Partial fields of a repository object.
71
#[derive(Debug, Serialize, Deserialize)]
72
pub struct Repo {
73
    pub id: u64,
74
    pub name: String,
75
    pub full_name: String,
76
}
77

            
78
/// Partial fields of a check_run object.
79
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
80
pub struct CheckRun {
81
    #[serde(skip_serializing_if = "is_zero")]
82
    pub id: u64,
83
    pub name: String,
84
    pub head_sha: String,
85
    pub status: String,
86
    #[serde(skip_serializing_if = "Option::is_none")]
87
    pub conclusion: Option<String>,
88
    #[serde(skip_serializing_if = "Option::is_none")]
89
    pub started_at: Option<String>,
90
    #[serde(skip_serializing_if = "Option::is_none")]
91
    pub completed_at: Option<String>,
92
    #[serde(skip_serializing_if = "Option::is_none")]
93
    pub output: Option<CheckRunOutput>,
94
    #[serde(skip_serializing_if = "Option::is_none")]
95
    pub app: Option<App>,
96
}
97

            
98
22
fn is_zero(value: &u64) -> bool {
99
22
    *value == 0
100
22
}
101

            
102
impl CheckRun {
103
    /// Create a new check-run for the given commit.
104
12
    pub fn new(commit: &str) -> Self {
105
12
        CheckRun {
106
12
            name: CHECK_RUN_NAME.to_string(),
107
12
            head_sha: commit.to_string(),
108
12
            status: CHECK_RUN_INITIAL_STATUS.to_string(),
109
12
            output: Some(CheckRunOutput {
110
12
                title: Some(CHECK_RUN_INITIAL_TITLE.to_string()),
111
12
                summary: Some(CHECK_RUN_SUMMARY.to_string()),
112
12
            }),
113
12
            ..Default::default()
114
12
        }
115
12
    }
116
    /// Update the status based on the count of uncompleted check-runs.
117
    /// Returns if the content of the check-run has changed.
118
7
    pub fn update_status(&mut self, count: u32) -> bool {
119
        let status: String;
120
        let conclusion: Option<String>;
121
        let output_title: Option<String>;
122

            
123
7
        if count == 0 {
124
3
            status = CHECK_RUN_COMPLETED_STATUS.to_string();
125
3
            conclusion = Some(CHECK_RUN_CONCLUSION.to_string());
126
3
            output_title = Some(CHECK_RUN_COMPLETED_TITLE.to_string());
127
4
        } else {
128
4
            status = CHECK_RUN_INITIAL_STATUS.to_string();
129
4
            conclusion = None;
130
4
            output_title = Some(format!("Waiting for {count} other checks to complete"));
131
4
        }
132

            
133
7
        let mut changed = false;
134

            
135
7
        if self.status != status {
136
3
            changed = true;
137
3
            self.status = status;
138
4
        }
139
7
        if self.conclusion != conclusion {
140
3
            changed = true;
141
3
            self.conclusion = conclusion;
142
4
        }
143
7
        match &mut self.output {
144
7
            Some(output) => {
145
7
                if output.title != output_title {
146
5
                    changed = true;
147
5
                    output.title = output_title;
148
5
                }
149
            }
150
            None => {
151
                changed = true;
152
                self.output = Some(CheckRunOutput {
153
                    title: output_title,
154
                    summary: Some(CHECK_RUN_SUMMARY.to_string()),
155
                });
156
            }
157
        }
158

            
159
7
        changed
160
7
    }
161
}
162

            
163
/// Partial fields of a check_run output object.
164
#[derive(Debug, Serialize, Deserialize, Clone)]
165
pub struct CheckRunOutput {
166
    #[serde(skip_serializing_if = "Option::is_none")]
167
    pub title: Option<String>,
168
    #[serde(skip_serializing_if = "Option::is_none")]
169
    pub summary: Option<String>,
170
}
171

            
172
/// Partial fields of a GitHub App object.
173
#[derive(Debug, Serialize, Deserialize, Clone)]
174
pub struct App {
175
    pub id: u64,
176
    pub client_id: String,
177
    pub slug: String,
178
    pub name: String,
179
}
180

            
181
/// Partial fields of an installation object.
182
#[derive(Debug, Serialize, Deserialize)]
183
pub struct Installation {
184
    pub id: u64,
185
}
186

            
187
/// Partial fields of a comment object.
188
#[derive(Debug, Serialize, Deserialize)]
189
pub struct Comment {
190
    pub id: u64,
191
    pub body: String,
192
}
193

            
194
/// Partial fields of an issue object.
195
#[derive(Debug, Serialize, Deserialize)]
196
pub struct Issue {
197
    pub id: u64,
198
    pub number: u64,
199
}
200

            
201
/// Response to check-run requests from the GitHub API.
202
#[derive(Debug, Serialize, Deserialize, Clone)]
203
pub struct CheckRunsResponse {
204
    pub total_count: u64,
205
    pub check_runs: Vec<CheckRun>,
206
}
207

            
208
/// Response to installation token requests from the GitHub API.
209
#[derive(Debug, Serialize, Deserialize, Clone)]
210
pub struct TokenResponse {
211
    pub token: String,
212
    pub expires_at: DateTime<Utc>,
213
}
214

            
215
/// Response to get pull request from the GitHub API.
216
#[derive(Debug, Serialize, Deserialize)]
217
pub struct PullRequestResponse {
218
    pub id: u64,
219
    pub number: u64,
220
    pub head: BranchRef,
221
}