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

dcdpr / jp / 26661718239

29 May 2026 08:55PM UTC coverage: 66.375% (+0.003%) from 66.372%
26661718239

Pull #696

github

web-flow
Merge e55a0d78b into c68b45a1e
Pull Request #696: chore(comfort): enforce `comfort` markdown formatting on all files

5 of 5 new or added lines in 1 file covered. (100.0%)

4 existing lines in 4 files now uncovered.

32028 of 48253 relevant lines covered (66.38%)

278.54 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.
7
/// Fixed at 100 (the GitHub API max for `/pulls/{N}/files`).
8
const FILES_PER_PAGE: u8 = 100;
9

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

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

22
    if files.is_empty() {
×
23
        enumerate(&owner, &repo, number, page).await
×
24
    } else {
25
        fetch(&owner, &repo, number, files.into_vec(), page).await
×
26
    }
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 (dozens
32
/// of files) the patches together easily blow the LLM context window.
33
/// The caller picks which files they actually need and re-calls with `files:
34
/// [...]` to get those patches specifically.
UNCOV
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

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.
60
    let pull = client
×
61
        .pulls(owner, repo)
×
62
        .get(number)
×
63
        .await
×
64
        .map_err(|e| handle_404(e, format!("Pull #{number} not found in {owner}/{repo}")))?;
×
65

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

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

87
    to_xml(Files {
×
88
        number,
×
89
        page,
×
90
        per_page: FILES_PER_PAGE,
×
91
        changed_files_count: pull.changed_files,
×
92
        file,
×
93
    })
×
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 returns
99
/// matching files with their `patch` field included.
100
/// Files not found on the requested page get an explicit `not_found` entry —
101
/// the LLM can either bump `page` or call `enumerate` mode to find which page
102
/// each file lives on.
103
async fn fetch(
×
104
    owner: &str,
×
105
    repo: &str,
×
106
    number: u64,
×
107
    files: Vec<String>,
×
108
    page: u64,
×
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

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

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

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

165
    let not_found: Vec<NotFound> = files
×
166
        .iter()
×
167
        .filter(|requested| !seen_filenames.iter().any(|seen| seen == *requested))
×
168
        .map(|filename| NotFound {
×
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",
172
        })
×
173
        .collect();
×
174

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.
178
        return to_xml_with_root(&not_found, "not_found");
×
179
    }
×
180

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