1mod auth;
5mod blame;
6mod blobs;
7mod history;
8mod refs;
9mod search;
10mod transport;
11mod trees;
12
13use std::collections::HashMap;
14use std::sync::Mutex;
15use std::time::Duration;
16
17const API: &str = "https://api.github.com";
18const REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
19const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
20
21pub struct GitHubClient {
22 http: reqwest::blocking::Client,
23 token: Option<String>,
24 auth_source: &'static str,
27 blame_cache: Mutex<HashMap<blame::BlameCacheKey, Vec<rootle_provider::BlameRange>>>,
28}
29
30impl Default for GitHubClient {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl GitHubClient {
37 pub fn new() -> Self {
38 let (token, auth_source) = auth::token();
39 Self::build(token, auth_source)
40 }
41
42 pub fn anonymous() -> Self {
44 Self::build(None, "anonymous")
45 }
46
47 fn build(token: Option<String>, auth_source: &'static str) -> Self {
48 let http = reqwest::blocking::Client::builder()
49 .user_agent(concat!("rootle/", env!("CARGO_PKG_VERSION")))
50 .timeout(REQUEST_TIMEOUT)
51 .connect_timeout(CONNECT_TIMEOUT)
52 .build()
53 .expect("construct GitHub HTTP client");
54 Self {
55 http,
56 token,
57 auth_source,
58 blame_cache: Mutex::new(HashMap::new()),
59 }
60 }
61
62 pub fn is_anonymous(&self) -> bool {
63 self.token.is_none()
64 }
65}
66
67fn urlencoding(value: &str) -> String {
70 use std::fmt::Write;
71 let mut encoded = String::with_capacity(value.len());
72 for byte in value.bytes() {
73 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~' | b'/') {
74 encoded.push(char::from(byte));
75 } else {
76 write!(&mut encoded, "%{byte:02X}").expect("write to a String");
77 }
78 }
79 encoded
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn query_encoding_preserves_unicode_and_reserved_characters() {
88 assert_eq!(urlencoding("文字 +&"), "%E6%96%87%E5%AD%97%20%2B%26");
89 assert_eq!(urlencoding("owner/repo"), "owner/repo");
90 }
91}