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

dcdpr / jp / 26000587207

17 May 2026 07:32PM UTC coverage: 64.958%. First build
26000587207

Pull #650

github

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

126 of 394 new or added lines in 8 files covered. (31.98%)

26936 of 41467 relevant lines covered (64.96%)

181.16 hits per line

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

0.0
/.config/jp/tools/src/github/pr_diff.rs
1
use jp_github::models::repos::DiffEntryStatus;
2

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

6
/// Files per page when enumerating changed files. Fixed at 100 (the
7
/// GitHub API max for `/pulls/{N}/files`).
8
const FILES_PER_PAGE: u8 = 100;
9

NEW
10
pub(crate) async fn github_pr_diff(
×
NEW
11
    repository: Option<String>,
×
NEW
12
    number: u64,
×
NEW
13
    files: Option<OneOrMany<String>>,
×
NEW
14
    page: Option<u64>,
×
NEW
15
) -> Result<String> {
×
NEW
16
    auth_optional().await?;
×
17

NEW
18
    let (owner, repo) = parse_repo(repository)?;
×
NEW
19
    let page = page.unwrap_or(1).max(1);
×
NEW
20
    let files = files.unwrap_or_default();
×
21

NEW
22
    if files.is_empty() {
×
NEW
23
        enumerate(&owner, &repo, number, page).await
×
24
    } else {
NEW
25
        fetch(&owner, &repo, number, files.into_vec(), page).await
×
26
    }
NEW
27
}
×
28

29
/// List a page of changed files without their patches.
30
///
31
/// The `patch` field is intentionally omitted here — for a typical PR
32
/// (dozens of files) the patches together easily blow the LLM context
33
/// window. The caller picks which files they actually need and re-calls
34
/// with `files: [...]` to get those patches specifically.
NEW
35
async fn enumerate(owner: &str, repo: &str, number: u64, page: u64) -> Result<String> {
×
36
    #[derive(serde::Serialize)]
37
    struct ChangedFile {
38
        filename: String,
39
        status: DiffEntryStatus,
40
        additions: u64,
41
        deletions: u64,
42
        changes: u64,
43
        previous_filename: Option<String>,
44
    }
45

46
    #[derive(serde::Serialize)]
47
    struct Files {
48
        number: u64,
49
        page: u64,
50
        per_page: u8,
51
        changed_files_count: u64,
52
        file: Vec<ChangedFile>,
53
    }
54

NEW
55
    let client = jp_github::instance();
×
56

57
    // We fetch PR metadata first solely to get the authoritative
58
    // `changed_files` count; the LLM otherwise has no way to know whether
59
    // page 1 of 100 entries exhausted the PR or not.
NEW
60
    let pull = client
×
NEW
61
        .pulls(owner, repo)
×
NEW
62
        .get(number)
×
NEW
63
        .await
×
NEW
64
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {owner}/{repo}")))?;
×
65

NEW
66
    let entries = client
×
NEW
67
        .pulls(owner, repo)
×
NEW
68
        .list_files(number)
×
NEW
69
        .page(page)
×
NEW
70
        .per_page(FILES_PER_PAGE)
×
NEW
71
        .send()
×
NEW
72
        .await
×
NEW
73
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {owner}/{repo}")))?;
×
74

NEW
75
    let file = entries
×
NEW
76
        .into_iter()
×
NEW
77
        .map(|entry| ChangedFile {
×
NEW
78
            filename: entry.filename,
×
NEW
79
            status: entry.status,
×
NEW
80
            additions: entry.additions,
×
NEW
81
            deletions: entry.deletions,
×
NEW
82
            changes: entry.changes,
×
NEW
83
            previous_filename: entry.previous_filename,
×
NEW
84
        })
×
NEW
85
        .collect();
×
86

NEW
87
    to_xml(Files {
×
NEW
88
        number,
×
NEW
89
        page,
×
NEW
90
        per_page: FILES_PER_PAGE,
×
NEW
91
        changed_files_count: pull.changed_files,
×
NEW
92
        file,
×
NEW
93
    })
×
NEW
94
}
×
95

96
/// Fetch patches for a specific set of files.
97
///
98
/// Searches a single page of the changed-files list (per `page`) and
99
/// returns matching files with their `patch` field included. Files not
100
/// found on the requested page get an explicit `not_found` entry — the
101
/// LLM can either bump `page` or call `enumerate` mode to find which
102
/// page each file lives on.
NEW
103
async fn fetch(
×
NEW
104
    owner: &str,
×
NEW
105
    repo: &str,
×
NEW
106
    number: u64,
×
NEW
107
    files: Vec<String>,
×
NEW
108
    page: u64,
×
NEW
109
) -> Result<String> {
×
110
    #[derive(serde::Serialize)]
111
    struct ChangedFile {
112
        filename: String,
113
        status: DiffEntryStatus,
114
        additions: u64,
115
        deletions: u64,
116
        changes: u64,
117
        previous_filename: Option<String>,
118
        patch: Option<String>,
119
    }
120

121
    #[derive(serde::Serialize)]
122
    struct NotFound {
123
        filename: String,
124
        // Hint string to nudge the LLM toward the right next call.
125
        hint: &'static str,
126
    }
127

128
    #[derive(serde::Serialize)]
129
    struct Response {
130
        number: u64,
131
        page: u64,
132
        per_page: u8,
133
        file: Vec<ChangedFile>,
134
        #[serde(skip_serializing_if = "Vec::is_empty")]
135
        not_found: Vec<NotFound>,
136
    }
137

NEW
138
    let entries = jp_github::instance()
×
NEW
139
        .pulls(owner, repo)
×
NEW
140
        .list_files(number)
×
NEW
141
        .page(page)
×
NEW
142
        .per_page(FILES_PER_PAGE)
×
NEW
143
        .send()
×
NEW
144
        .await
×
NEW
145
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {owner}/{repo}")))?;
×
146

NEW
147
    let mut matched = Vec::new();
×
NEW
148
    let mut seen_filenames = Vec::with_capacity(entries.len());
×
149

NEW
150
    for entry in entries {
×
NEW
151
        seen_filenames.push(entry.filename.clone());
×
NEW
152
        if files.contains(&entry.filename) {
×
NEW
153
            matched.push(ChangedFile {
×
NEW
154
                filename: entry.filename,
×
NEW
155
                status: entry.status,
×
NEW
156
                additions: entry.additions,
×
NEW
157
                deletions: entry.deletions,
×
NEW
158
                changes: entry.changes,
×
NEW
159
                previous_filename: entry.previous_filename,
×
NEW
160
                patch: entry.patch,
×
NEW
161
            });
×
NEW
162
        }
×
163
    }
164

NEW
165
    let not_found: Vec<NotFound> = files
×
NEW
166
        .iter()
×
NEW
167
        .filter(|requested| !seen_filenames.iter().any(|seen| seen == *requested))
×
NEW
168
        .map(|filename| NotFound {
×
NEW
169
            filename: filename.clone(),
×
170
            hint: "not present on this page; bump `page` or call without `files` to enumerate \
171
                   changed files and locate the right page",
NEW
172
        })
×
NEW
173
        .collect();
×
174

NEW
175
    if matched.is_empty() && !not_found.is_empty() {
×
176
        // Render only the not-found block so the LLM gets a clear empty
177
        // result rather than an XML with one filler element.
NEW
178
        return to_xml_with_root(&not_found, "not_found");
×
NEW
179
    }
×
180

NEW
181
    to_xml(Response {
×
NEW
182
        number,
×
NEW
183
        page,
×
NEW
184
        per_page: FILES_PER_PAGE,
×
NEW
185
        file: matched,
×
NEW
186
        not_found,
×
NEW
187
    })
×
NEW
188
}
×
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