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 blame_cache: Mutex<HashMap<blame::BlameCacheKey, Vec<rootle_provider::BlameRange>>>,
25}
26
27impl Default for GitHubClient {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl GitHubClient {
34 pub fn new() -> Self {
35 Self::build(auth::token())
36 }
37
38 pub fn anonymous() -> Self {
40 Self::build(None)
41 }
42
43 fn build(token: Option<String>) -> Self {
44 let http = reqwest::blocking::Client::builder()
45 .user_agent(concat!("rootle/", env!("CARGO_PKG_VERSION")))
46 .timeout(REQUEST_TIMEOUT)
47 .connect_timeout(CONNECT_TIMEOUT)
48 .build()
49 .expect("construct GitHub HTTP client");
50 Self {
51 http,
52 token,
53 blame_cache: Mutex::new(HashMap::new()),
54 }
55 }
56
57 pub fn is_anonymous(&self) -> bool {
58 self.token.is_none()
59 }
60}
61
62fn urlencoding(value: &str) -> String {
65 use std::fmt::Write;
66 let mut encoded = String::with_capacity(value.len());
67 for byte in value.bytes() {
68 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~' | b'/') {
69 encoded.push(char::from(byte));
70 } else {
71 write!(&mut encoded, "%{byte:02X}").expect("write to a String");
72 }
73 }
74 encoded
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn query_encoding_preserves_unicode_and_reserved_characters() {
83 assert_eq!(urlencoding("文字 +&"), "%E6%96%87%E5%AD%97%20%2B%26");
84 assert_eq!(urlencoding("owner/repo"), "owner/repo");
85 }
86}