qualia_client_core/
github.rs1use reqwest::blocking::Client;
2use serde_json::{json, Value};
3use std::collections::HashMap;
4use std::time::Duration;
5
6const GITHUB_API_URL: &str = "https://api.github.com";
7const USER_AGENT: &str = "qualia-webizen-client";
8
9fn github_client(token: &str) -> Result<Client, String> {
11 let mut headers = reqwest::header::HeaderMap::new();
12 headers.insert(
13 reqwest::header::AUTHORIZATION,
14 reqwest::header::HeaderValue::from_str(&format!("token {}", token))
15 .map_err(|e| format!("Invalid token header: {}", e))?,
16 );
17 headers.insert(
18 reqwest::header::ACCEPT,
19 reqwest::header::HeaderValue::from_static("application/vnd.github.v3+json"),
20 );
21 headers.insert(
22 reqwest::header::USER_AGENT,
23 reqwest::header::HeaderValue::from_static(USER_AGENT),
24 );
25
26 let client = Client::builder()
27 .default_headers(headers)
28 .timeout(Duration::from_secs(30))
29 .build()
30 .map_err(|e| format!("Failed to build client: {}", e))?;
31
32 Ok(client)
33}
34
35fn handle_response(resp: reqwest::blocking::Response) -> Result<Value, String> {
37 let status = resp.status();
38 let text = resp
39 .text()
40 .map_err(|e| format!("Failed to read response: {}", e))?;
41 if !status.is_success() {
42 return Err(format!("GitHub API error ({}): {}", status, text));
43 }
44 serde_json::from_str(&text).map_err(|e| format!("Failed to parse GitHub JSON: {e}"))
45}
46
47pub fn verify_github_token(token: &str) -> Result<String, String> {
49 let client = github_client(token)?;
50 let resp = client
51 .get(&format!("{}/user", GITHUB_API_URL))
52 .send()
53 .map_err(|e| format!("Failed to send request: {}", e))?;
54 let json = handle_response(resp)?;
55 let login = json["login"].as_str().unwrap_or("unknown").to_string();
56 Ok(login)
57}
58
59pub fn create_repository(token: &str, name: &str) -> Result<String, String> {
61 let client = github_client(token)?;
62
63 let _login = verify_github_token(token)?;
65
66 let resp = client
68 .post(&format!("{}/user/repos", GITHUB_API_URL))
69 .json(&json!({
70 "name": name,
71 "description": "Generated by Qualia Webizen",
72 "private": false,
73 "auto_init": true, }))
75 .send()
76 .map_err(|e| format!("Request failed: {}", e))?;
77
78 let status = resp.status();
79 let text = resp
80 .text()
81 .map_err(|e| format!("Failed to read text: {}", e))?;
82
83 if status.as_u16() == 422 && text.contains("already exists") {
85 return Ok(format!("{}/{}", _login, name));
86 }
87
88 if !status.is_success() {
89 return Err(format!(
90 "GitHub API error creating repo ({}): {}",
91 status, text
92 ));
93 }
94
95 let result_json: Value =
96 serde_json::from_str(&text).map_err(|e| format!("Invalid JSON: {}", e))?;
97 let full_name = result_json["full_name"]
98 .as_str()
99 .ok_or_else(|| "No full_name in response".to_string())?;
100 Ok(full_name.to_string())
101}
102
103pub fn push_static_site(
106 token: &str,
107 full_name: &str,
108 files: HashMap<String, String>,
109) -> Result<(), String> {
110 if files.is_empty() {
111 return Ok(());
112 }
113
114 let client = github_client(token)?;
115 let base_url = format!("{}/repos/{}", GITHUB_API_URL, full_name);
116
117 let ref_resp = client
119 .get(&format!("{}/git/ref/heads/main", base_url))
120 .send()
121 .map_err(|e| format!("Req failed: {}", e))?;
122 let ref_json = if ref_resp.status().is_success() {
123 handle_response(ref_resp)?
124 } else {
125 let master_resp = client
127 .get(&format!("{}/git/ref/heads/master", base_url))
128 .send()
129 .map_err(|e| format!("Req failed: {}", e))?;
130 handle_response(master_resp)?
131 };
132
133 let latest_commit_sha = ref_json["object"]["sha"].as_str().unwrap_or_default();
134 let ref_name = ref_json["ref"].as_str().unwrap_or_default();
135
136 let commit_resp = client
138 .get(&format!("{}/git/commits/{}", base_url, latest_commit_sha))
139 .send()
140 .map_err(|e| format!("Req failed: {}", e))?;
141 let commit_json = handle_response(commit_resp)?;
142 let base_tree_sha = commit_json["tree"]["sha"].as_str().unwrap_or_default();
143
144 let mut tree_nodes = Vec::new();
146 for (path, content) in files {
147 tree_nodes.push(json!({
148 "path": path,
149 "mode": "100644",
150 "type": "blob",
151 "content": content
152 }));
153 }
154
155 let tree_resp = client
156 .post(&format!("{}/git/trees", base_url))
157 .json(&json!({
158 "base_tree": base_tree_sha,
159 "tree": tree_nodes
160 }))
161 .send()
162 .map_err(|e| format!("Req failed: {}", e))?;
163 let new_tree_json = handle_response(tree_resp)?;
164 let new_tree_sha = new_tree_json["sha"].as_str().unwrap_or_default();
165
166 let commit_resp = client
168 .post(&format!("{}/git/commits", base_url))
169 .json(&json!({
170 "message": "Deploy static site from Webizen",
171 "tree": new_tree_sha,
172 "parents": [latest_commit_sha]
173 }))
174 .send()
175 .map_err(|e| format!("Req failed: {}", e))?;
176 let new_commit_json = handle_response(commit_resp)?;
177 let new_commit_sha = new_commit_json["sha"].as_str().unwrap_or_default();
178
179 let _update_ref_resp = client
181 .patch(&format!("{}/git/{}", base_url, ref_name))
182 .json(&json!({
183 "sha": new_commit_sha,
184 "force": false
185 }))
186 .send()
187 .map_err(|e| format!("Req failed: {}", e))?;
188
189 Ok(())
190}