Skip to main content

rootle_github/client/
history.rs

1//! Commit history and commit-detail retrieval.
2
3use super::{API, GitHubClient, urlencoding};
4use rootle_provider::ProviderResult;
5
6impl GitHubClient {
7    /// Commit log newest-first. `limit+1` probing decides `truncated`
8    /// without parsing Link headers; a hard cap of 100 keeps it to one
9    /// call (the spec's ~N reading).
10    pub fn log(
11        &self,
12        owner: &str,
13        repo: &str,
14        path: Option<&str>,
15        ref_: Option<&str>,
16        limit: Option<usize>,
17    ) -> ProviderResult<(Vec<rootle_provider::LogEntry>, bool)> {
18        #[derive(serde::Deserialize)]
19        struct CommitItem {
20            sha: String,
21            commit: CommitDetail,
22        }
23        #[derive(serde::Deserialize)]
24        struct CommitDetail {
25            message: String,
26            author: CommitAuthor,
27        }
28        #[derive(serde::Deserialize)]
29        struct CommitAuthor {
30            name: String,
31            date: String,
32        }
33        let want = limit.unwrap_or(50).min(99);
34        let mut url = format!("{API}/repos/{owner}/{repo}/commits?per_page={}", want + 1);
35        if let Some(p) = path {
36            url.push_str(&format!("&path={}", urlencoding(p)));
37        }
38        if let Some(r) = ref_ {
39            url.push_str(&format!("&sha={}", urlencoding(r)));
40        }
41        let mut items: Vec<CommitItem> = self.get(&url)?;
42        let truncated = items.len() > want;
43        items.truncate(want);
44        Ok((
45            items
46                .into_iter()
47                .map(|c| rootle_provider::LogEntry {
48                    sha: c.sha,
49                    subject: c.commit.message.lines().next().unwrap_or("").to_string(),
50                    author: c.commit.author.name,
51                    date: c.commit.author.date,
52                })
53                .collect(),
54            truncated,
55        ))
56    }
57
58    /// v1.6 (plans/0028 M1): one commit's detail — message, parents,
59    /// changed files with their unified hunks. The wire model is
60    /// `types::CommitResponse`; the seam mapping lives in lib.rs.
61    pub fn commit(
62        &self,
63        owner: &str,
64        repo: &str,
65        sha: &str,
66    ) -> ProviderResult<crate::types::CommitResponse> {
67        const FILES_PER_PAGE: usize = 100;
68        const FILE_BUDGET: usize = rootle_provider::RENDER_BUDGET;
69        let mut url = reqwest::Url::parse(API).expect("GitHub API URL");
70        url.path_segments_mut()
71            .expect("HTTP URL supports path segments")
72            .extend(["repos", owner, repo, "commits", sha]);
73        url.query_pairs_mut()
74            .append_pair("per_page", &FILES_PER_PAGE.to_string());
75        let first = self.get_page::<crate::types::CommitResponse>(url.as_str())?;
76        let mut response = first.body;
77        let mut has_next = first.has_next;
78        let mut page = 1u32;
79        // Pagination is file-only. Pin subsequent pages to the resolved
80        // content ID, even if a caller supplied a resolvable revision.
81        url.path_segments_mut()
82            .expect("HTTP URL supports path segments")
83            .pop()
84            .push(&response.sha);
85        while has_next && response.files.len() < FILE_BUDGET {
86            page += 1;
87            url.query_pairs_mut()
88                .clear()
89                .append_pair("per_page", &FILES_PER_PAGE.to_string())
90                .append_pair("page", &page.to_string());
91            let next = self.get_page::<crate::types::CommitResponse>(url.as_str())?;
92            if next.body.sha != response.sha {
93                return Err(rootle_provider::ProviderError::other(
94                    "commit changed across file pages",
95                ));
96            }
97            response.files.extend(next.body.files);
98            has_next = next.has_next;
99        }
100        response.truncated = has_next || response.files.len() > FILE_BUDGET;
101        response.files.truncate(FILE_BUDGET);
102        Ok(response)
103    }
104}