• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dcdpr / jp / 25983494976

17 May 2026 06:27AM UTC coverage: 65.015%. First build
25983494976

Pull #650

github

web-flow
Merge 4d64f9b5a into 79609abcf
Pull Request #650: chore(tools, github): Add `repository`, `page` params + comment fetching

80 of 227 new or added lines in 6 files covered. (35.24%)

26880 of 41344 relevant lines covered (65.02%)

236.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/.config/jp/tools/src/github/pulls.rs
1
use chrono::{DateTime, Utc};
2
use jp_github::{models::repos::DiffEntryStatus, params};
3
use url::Url;
4

5
use super::{auth_optional, parse_repo};
6
use crate::{Result, github::handle_404, to_xml, to_xml_with_root, util::OneOrMany};
7

8
/// Comments-per-page when fetching a specific pull request. Matches the
9
/// issues tool — long discussions are walked with the `page` parameter.
10
const COMMENTS_PER_PAGE: u8 = 10;
11

12
/// The status of a issue or pull request.
13
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
14
#[serde(rename_all = "lowercase")]
15
pub enum State {
16
    Open,
17
    Closed,
18
}
19

20
pub(crate) async fn github_pulls(
×
NEW
21
    repository: Option<String>,
×
22
    number: Option<u64>,
×
23
    state: Option<State>,
×
24
    file_diffs: Option<OneOrMany<String>>,
×
NEW
25
    page: Option<u64>,
×
26
) -> Result<String> {
×
NEW
27
    auth_optional().await?;
×
28

NEW
29
    let (owner, repo) = parse_repo(repository)?;
×
NEW
30
    let page = page.unwrap_or(1).max(1);
×
31
    let file_diffs = file_diffs.unwrap_or_default();
×
32

33
    match number {
×
NEW
34
        Some(number) if !file_diffs.is_empty() => {
×
NEW
35
            diff(&owner, &repo, number, file_diffs.into_vec()).await
×
36
        }
NEW
37
        Some(number) => get(&owner, &repo, number, page).await,
×
NEW
38
        None => list(&owner, &repo, state, page).await,
×
39
    }
40
}
×
41

NEW
42
async fn get(owner: &str, repo: &str, number: u64, page: u64) -> Result<String> {
×
43
    #[derive(serde::Serialize)]
44
    struct ChangedFile {
45
        filename: String,
46
        status: DiffEntryStatus,
47
        additions: u64,
48
        deletions: u64,
49
        changes: u64,
50
        previous_filename: Option<String>,
51
    }
52

53
    #[derive(serde::Serialize)]
54
    struct Comment {
55
        author: String,
56
        created_at: DateTime<Utc>,
57
        body: Option<String>,
58
    }
59

60
    #[derive(serde::Serialize)]
61
    struct Pull {
62
        number: u64,
63
        title: Option<String>,
64
        body: Option<String>,
65
        url: Option<Url>,
66
        labels: Vec<String>,
67
        author: Option<String>,
68
        created_at: Option<DateTime<Utc>>,
69
        closed_at: Option<DateTime<Utc>>,
70
        merged_at: Option<DateTime<Utc>>,
71
        merge_commit_sha: Option<String>,
72
        changed_files: Vec<ChangedFile>,
73
        comments_count: u64,
74
        comments_page: u64,
75
        comments_per_page: u8,
76
        comments: Vec<Comment>,
77
    }
78

NEW
79
    let client = jp_github::instance();
×
80

NEW
81
    let pull = client
×
NEW
82
        .pulls(owner, repo)
×
83
        .get(number)
×
84
        .await
×
NEW
85
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {owner}/{repo}")))?;
×
86

NEW
87
    let files_page = client
×
NEW
88
        .pulls(owner, repo)
×
89
        .list_files(number)
×
90
        .await
×
NEW
91
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {owner}/{repo}")))?;
×
92

NEW
93
    let changed_files = client
×
NEW
94
        .all_pages(files_page)
×
95
        .await?
×
96
        .into_iter()
×
97
        .map(|file| ChangedFile {
×
98
            filename: file.filename,
×
99
            status: file.status,
×
100
            additions: file.additions,
×
101
            deletions: file.deletions,
×
102
            changes: file.changes,
×
103
            previous_filename: file.previous_filename,
×
104
        })
×
105
        .collect();
×
106

107
    // PR conversation comments share the issues endpoint — `/issues/{N}/comments`
108
    // returns the same thread shown in the "Conversation" tab. Inline review
109
    // comments live on a different endpoint and aren't part of this scope.
NEW
110
    let comments = client
×
NEW
111
        .issues(owner, repo)
×
NEW
112
        .list_comments(number)
×
NEW
113
        .page(page)
×
NEW
114
        .per_page(COMMENTS_PER_PAGE)
×
NEW
115
        .send()
×
NEW
116
        .await
×
NEW
117
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {owner}/{repo}")))?
×
NEW
118
        .into_iter()
×
NEW
119
        .map(|c| Comment {
×
NEW
120
            author: c.user.login,
×
NEW
121
            created_at: c.created_at,
×
NEW
122
            body: c.body,
×
NEW
123
        })
×
NEW
124
        .collect();
×
125

126
    to_xml(Pull {
×
127
        number,
×
128
        title: pull.title,
×
129
        body: pull.body,
×
130
        url: pull.html_url,
×
131
        labels: pull
×
132
            .labels
×
133
            .into_iter()
×
134
            .flatten()
×
135
            .map(|label| label.name)
×
136
            .collect(),
×
137
        author: pull.user.map(|user| user.login),
×
138
        created_at: pull.created_at,
×
139
        closed_at: pull.closed_at,
×
140
        merged_at: pull.merged_at,
×
141
        merge_commit_sha: pull.merge_commit_sha,
×
142
        changed_files,
×
NEW
143
        comments_count: pull.comments,
×
NEW
144
        comments_page: page,
×
145
        comments_per_page: COMMENTS_PER_PAGE,
NEW
146
        comments,
×
147
    })
148
}
×
149

NEW
150
async fn diff(owner: &str, repo: &str, number: u64, file_diffs: Vec<String>) -> Result<String> {
×
151
    #[derive(serde::Serialize)]
152
    struct ChangedFile {
153
        filename: String,
154
        status: DiffEntryStatus,
155
        additions: u64,
156
        deletions: u64,
157
        changes: u64,
158
        previous_filename: Option<String>,
159
        patch: Option<String>,
160
    }
161

162
    let page = jp_github::instance()
×
NEW
163
        .pulls(owner, repo)
×
164
        .list_files(number)
×
165
        .await
×
NEW
166
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {owner}/{repo}")))?;
×
167

168
    let changed_files: Vec<_> = jp_github::instance()
×
169
        .all_pages(page)
×
170
        .await?
×
171
        .into_iter()
×
172
        .filter(|file| file_diffs.contains(&file.filename))
×
173
        .map(|file| ChangedFile {
×
174
            patch: file.patch,
×
175
            filename: file.filename,
×
176
            status: file.status,
×
177
            additions: file.additions,
×
178
            deletions: file.deletions,
×
179
            changes: file.changes,
×
180
            previous_filename: file.previous_filename,
×
181
        })
×
182
        .collect();
×
183

184
    to_xml_with_root(&changed_files, "files")
×
185
}
×
186

187
/// Items per page when listing pull requests. Fixed at 100 (the
188
/// GitHub API max for this endpoint).
189
const LIST_PER_PAGE: u8 = 100;
190

NEW
191
async fn list(owner: &str, repo: &str, state: Option<State>, page: u64) -> Result<String> {
×
192
    #[derive(serde::Serialize)]
193
    struct Pulls {
194
        page: u64,
195
        per_page: u8,
196
        pull: Vec<Pull>,
197
    }
198

199
    #[derive(serde::Serialize)]
200
    struct Pull {
201
        number: u64,
202
        title: Option<String>,
203
        url: Option<Url>,
204
        labels: Vec<String>,
205
        author: Option<String>,
206
        created_at: Option<DateTime<Utc>>,
207
        closed_at: Option<DateTime<Utc>>,
208
        merged_at: Option<DateTime<Utc>>,
209
        merge_commit_sha: Option<String>,
210
    }
211

212
    let state = match state {
×
213
        Some(State::Open) => params::State::Open,
×
214
        Some(State::Closed) => params::State::Closed,
×
215
        None => params::State::All,
×
216
    };
217

NEW
218
    let pulls = jp_github::instance()
×
NEW
219
        .pulls(owner, repo)
×
220
        .list()
×
221
        .state(state)
×
NEW
222
        .page(page)
×
NEW
223
        .per_page(LIST_PER_PAGE)
×
224
        .send()
×
225
        .await?;
×
226

NEW
227
    let pull = pulls
×
228
        .into_iter()
×
229
        .map(|pull| Pull {
×
230
            number: pull.number,
×
231
            title: pull.title,
×
232
            url: pull.html_url,
×
233
            labels: pull
×
234
                .labels
×
235
                .into_iter()
×
236
                .flatten()
×
237
                .map(|label| label.name)
×
238
                .collect(),
×
239
            author: pull.user.map(|user| user.login),
×
240
            created_at: pull.created_at,
×
241
            closed_at: pull.closed_at,
×
242
            merged_at: pull.merged_at,
×
243
            merge_commit_sha: pull.merge_commit_sha,
×
244
        })
×
245
        .collect();
×
246

NEW
247
    to_xml(Pulls {
×
NEW
248
        page,
×
NEW
249
        per_page: LIST_PER_PAGE,
×
NEW
250
        pull,
×
NEW
251
    })
×
252
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc