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

dcdpr / jp / 26894181539

03 Jun 2026 03:13PM UTC coverage: 66.428%. First build
26894181539

Pull #739

github

web-flow
Merge d211b675a into fc2b73b16
Pull Request #739: chore(tools, github): Add `github_pr_commits` and `github_commit` tools

57 of 229 new or added lines in 5 files covered. (24.89%)

33869 of 50986 relevant lines covered (66.43%)

250.94 hits per line

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

0.0
/.config/jp/tools/src/github/commit.rs
1
use chrono::{DateTime, Utc};
2
use jp_github::models::{commits, repos::DiffEntryStatus};
3

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

7
/// Changed files per page for a single commit.
8
/// Fixed at 100; the GitHub commit endpoint caps the files array at 300 and
9
/// paginates the remainder.
10
const FILES_PER_PAGE: u8 = 100;
11

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

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

NEW
24
    let commit = jp_github::instance()
×
NEW
25
        .repos(&owner, &repo)
×
NEW
26
        .get_commit(reference.as_str())
×
NEW
27
        .page(page)
×
NEW
28
        .per_page(FILES_PER_PAGE)
×
NEW
29
        .send()
×
NEW
30
        .await
×
NEW
31
        .map_err(|e| {
×
NEW
32
            handle_404(
×
NEW
33
                e,
×
NEW
34
                format!("Commit `{reference}` not found in {owner}/{repo}"),
×
NEW
35
            )
×
NEW
36
        })?;
×
37

NEW
38
    if files.is_empty() {
×
NEW
39
        enumerate(page, commit)
×
40
    } else {
NEW
41
        fetch(page, commit, &files)
×
42
    }
NEW
43
}
×
44

45
/// Commit header fields shared by both modes.
46
struct Header {
47
    sha: String,
48
    message: String,
49
    author: Option<String>,
50
    date: Option<DateTime<Utc>>,
51
}
52

NEW
53
fn header(commit: &commits::Commit) -> Header {
×
NEW
54
    let git_author = commit.commit.author.as_ref();
×
55
    Header {
NEW
56
        sha: commit.sha.clone(),
×
NEW
57
        message: commit.commit.message.clone(),
×
NEW
58
        author: commit
×
NEW
59
            .author
×
NEW
60
            .as_ref()
×
NEW
61
            .map(|u| u.login.clone())
×
NEW
62
            .or_else(|| git_author.and_then(|a| a.name.clone())),
×
NEW
63
        date: git_author.and_then(|a| a.date),
×
64
    }
NEW
65
}
×
66

67
/// List the commit's changed files without patches, plus metadata and stats.
NEW
68
fn enumerate(page: u64, commit: commits::Commit) -> Result<String> {
×
69
    #[derive(serde::Serialize)]
70
    struct ChangedFile {
71
        filename: String,
72
        status: DiffEntryStatus,
73
        additions: u64,
74
        deletions: u64,
75
        changes: u64,
76
        previous_filename: Option<String>,
77
    }
78

79
    #[derive(serde::Serialize)]
80
    struct Commit {
81
        sha: String,
82
        message: String,
83
        author: Option<String>,
84
        date: Option<DateTime<Utc>>,
85
        additions: u64,
86
        deletions: u64,
87
        total: u64,
88
        page: u64,
89
        per_page: u8,
90
        file: Vec<ChangedFile>,
91
    }
92

NEW
93
    let h = header(&commit);
×
NEW
94
    let stats = commit.stats.unwrap_or(commits::CommitStats {
×
NEW
95
        additions: 0,
×
NEW
96
        deletions: 0,
×
NEW
97
        total: 0,
×
NEW
98
    });
×
99

NEW
100
    let file = commit
×
NEW
101
        .files
×
NEW
102
        .unwrap_or_default()
×
NEW
103
        .into_iter()
×
NEW
104
        .map(|f| ChangedFile {
×
NEW
105
            filename: f.filename,
×
NEW
106
            status: f.status,
×
NEW
107
            additions: f.additions,
×
NEW
108
            deletions: f.deletions,
×
NEW
109
            changes: f.changes,
×
NEW
110
            previous_filename: f.previous_filename,
×
NEW
111
        })
×
NEW
112
        .collect();
×
113

NEW
114
    to_xml(Commit {
×
NEW
115
        sha: h.sha,
×
NEW
116
        message: h.message,
×
NEW
117
        author: h.author,
×
NEW
118
        date: h.date,
×
NEW
119
        additions: stats.additions,
×
NEW
120
        deletions: stats.deletions,
×
NEW
121
        total: stats.total,
×
NEW
122
        page,
×
NEW
123
        per_page: FILES_PER_PAGE,
×
NEW
124
        file,
×
NEW
125
    })
×
NEW
126
}
×
127

128
/// Fetch patches for a specific set of files in the commit.
129
///
130
/// Files not present on the requested `page` get an explicit `not_found` entry
131
/// so the caller can bump `page` or re-enumerate to locate them.
NEW
132
fn fetch(page: u64, commit: commits::Commit, files: &[String]) -> Result<String> {
×
133
    #[derive(serde::Serialize)]
134
    struct ChangedFile {
135
        filename: String,
136
        status: DiffEntryStatus,
137
        additions: u64,
138
        deletions: u64,
139
        changes: u64,
140
        previous_filename: Option<String>,
141
        patch: Option<String>,
142
    }
143

144
    #[derive(serde::Serialize)]
145
    struct NotFound {
146
        filename: String,
147
        hint: &'static str,
148
    }
149

150
    #[derive(serde::Serialize)]
151
    struct Response {
152
        sha: String,
153
        message: String,
154
        page: u64,
155
        per_page: u8,
156
        file: Vec<ChangedFile>,
157
        #[serde(skip_serializing_if = "Vec::is_empty")]
158
        not_found: Vec<NotFound>,
159
    }
160

NEW
161
    let h = header(&commit);
×
NEW
162
    let entries = commit.files.unwrap_or_default();
×
163

NEW
164
    let mut matched = Vec::new();
×
NEW
165
    let mut seen = Vec::with_capacity(entries.len());
×
166

NEW
167
    for entry in entries {
×
NEW
168
        seen.push(entry.filename.clone());
×
NEW
169
        if files.contains(&entry.filename) {
×
NEW
170
            matched.push(ChangedFile {
×
NEW
171
                filename: entry.filename,
×
NEW
172
                status: entry.status,
×
NEW
173
                additions: entry.additions,
×
NEW
174
                deletions: entry.deletions,
×
NEW
175
                changes: entry.changes,
×
NEW
176
                previous_filename: entry.previous_filename,
×
NEW
177
                patch: entry.patch,
×
NEW
178
            });
×
NEW
179
        }
×
180
    }
181

NEW
182
    let not_found: Vec<NotFound> = files
×
NEW
183
        .iter()
×
NEW
184
        .filter(|requested| !seen.iter().any(|seen| seen == *requested))
×
NEW
185
        .map(|filename| NotFound {
×
NEW
186
            filename: filename.clone(),
×
187
            hint: "not present on this page; bump `page` or call without `files` to enumerate the \
188
                   commit's changed files and locate the right page",
NEW
189
        })
×
NEW
190
        .collect();
×
191

NEW
192
    if matched.is_empty() && !not_found.is_empty() {
×
NEW
193
        return to_xml_with_root(&not_found, "not_found");
×
NEW
194
    }
×
195

NEW
196
    to_xml(Response {
×
NEW
197
        sha: h.sha,
×
NEW
198
        message: h.message,
×
NEW
199
        page,
×
NEW
200
        per_page: FILES_PER_PAGE,
×
NEW
201
        file: matched,
×
NEW
202
        not_found,
×
NEW
203
    })
×
NEW
204
}
×
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