1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3
4use super::Client;
5
6#[derive(Debug, Serialize)]
7struct CreateBlobRequest {
8 content: String,
9 encoding: String,
10}
11
12#[derive(Debug, Deserialize)]
13struct CreateBlobResponse {
14 sha: String,
15}
16
17#[derive(Debug, Serialize)]
18struct TreeEntry {
19 path: String,
20 mode: String,
21 #[serde(rename = "type")]
22 entry_type: String,
23 sha: String,
24}
25
26#[derive(Debug, Serialize)]
27struct CreateTreeRequest {
28 base_tree: String,
29 tree: Vec<TreeEntry>,
30}
31
32#[derive(Debug, Deserialize)]
33struct CreateTreeResponse {
34 sha: String,
35}
36
37#[derive(Debug, Serialize)]
38struct CreateCommitRequest {
39 message: String,
40 tree: String,
41 parents: Vec<String>,
42}
43
44#[derive(Debug, Deserialize)]
45struct CreateCommitResponse {
46 sha: String,
47}
48
49#[derive(Debug, Serialize)]
50struct UpdateRefRequest {
51 sha: String,
52 force: bool,
53}
54
55#[derive(Debug, Deserialize)]
56struct RefResponse {
57 object: RefObject,
58}
59
60#[derive(Debug, Deserialize)]
61struct RefObject {
62 sha: String,
63}
64
65#[derive(Debug, Clone)]
67pub struct CommitFile {
68 pub path: String,
69 pub content: String,
70}
71
72impl Client {
73 pub async fn create_commit(
76 &self,
77 repo: &str,
78 branch: &str,
79 message: &str,
80 files: &[CommitFile],
81 ) -> Result<String> {
82 let ref_path = format!("/repos/{}/{repo}/git/ref/heads/{branch}", self.org);
83 let resp = self.get(&ref_path).await?;
84 let status = resp.status();
85 if !status.is_success() {
86 let body = resp.text().await.unwrap_or_default();
87 anyhow::bail!("Failed to get ref heads/{branch} for {repo} (HTTP {status}): {body}");
88 }
89 let ref_info: RefResponse = resp.json().await.context("Failed to parse ref response")?;
90 let base_commit_sha = ref_info.object.sha;
91
92 let commit_resp = self
94 .get(&format!(
95 "/repos/{}/{repo}/git/commits/{base_commit_sha}",
96 self.org
97 ))
98 .await?;
99 let commit_data: serde_json::Value = commit_resp.json().await?;
100 let base_tree_sha = commit_data["tree"]["sha"]
101 .as_str()
102 .context("Missing tree SHA in commit")?
103 .to_owned();
104
105 let mut tree_entries = Vec::new();
107 for file in files {
108 let blob_req = CreateBlobRequest {
109 content: file.content.clone(),
110 encoding: "utf-8".to_owned(),
111 };
112
113 let resp = self
114 .post_json(&format!("/repos/{}/{repo}/git/blobs", self.org), &blob_req)
115 .await?;
116
117 let blob: CreateBlobResponse = resp.json().await.context("Failed to create blob")?;
118
119 tree_entries.push(TreeEntry {
120 path: file.path.clone(),
121 mode: "100644".to_owned(),
122 entry_type: "blob".to_owned(),
123 sha: blob.sha,
124 });
125 }
126
127 let tree_req = CreateTreeRequest {
129 base_tree: base_tree_sha,
130 tree: tree_entries,
131 };
132
133 let resp = self
134 .post_json(&format!("/repos/{}/{repo}/git/trees", self.org), &tree_req)
135 .await?;
136 let tree: CreateTreeResponse = resp.json().await.context("Failed to create tree")?;
137
138 let commit_req = CreateCommitRequest {
140 message: message.to_owned(),
141 tree: tree.sha,
142 parents: vec![base_commit_sha],
143 };
144
145 let resp = self
146 .post_json(
147 &format!("/repos/{}/{repo}/git/commits", self.org),
148 &commit_req,
149 )
150 .await?;
151 let commit: CreateCommitResponse = resp.json().await.context("Failed to create commit")?;
152
153 let update_ref = UpdateRefRequest {
155 sha: commit.sha.clone(),
156 force: false,
157 };
158
159 let resp = self
160 .patch_json(
161 &format!("/repos/{}/{repo}/git/refs/heads/{branch}", self.org),
162 &update_ref,
163 )
164 .await?;
165
166 if !resp.status().is_success() {
167 let status = resp.status();
168 let body = resp.text().await.unwrap_or_default();
169 anyhow::bail!("Failed to update ref for {repo} (HTTP {status}): {body}");
170 }
171
172 Ok(commit.sha)
173 }
174
175 pub async fn create_branch(
177 &self,
178 repo: &str,
179 branch_name: &str,
180 from_branch: &str,
181 ) -> Result<()> {
182 let resp = self
184 .get(&format!(
185 "/repos/{}/{repo}/git/ref/heads/{from_branch}",
186 self.org
187 ))
188 .await?;
189
190 if !resp.status().is_success() {
191 let body = resp.text().await.unwrap_or_default();
192 anyhow::bail!("Failed to get ref for {from_branch} in {repo}: {body}");
193 }
194
195 let ref_info: RefResponse = resp.json().await?;
196
197 let body = serde_json::json!({
198 "ref": format!("refs/heads/{branch_name}"),
199 "sha": ref_info.object.sha
200 });
201
202 let resp = self
203 .post_json(&format!("/repos/{}/{repo}/git/refs", self.org), &body)
204 .await?;
205
206 let status = resp.status();
207 if status.is_success() || status.as_u16() == 422 {
208 Ok(())
210 } else {
211 let body = resp.text().await.unwrap_or_default();
212 anyhow::bail!(
213 "Failed to create branch {branch_name} in {repo} (HTTP {status}): {body}"
214 );
215 }
216 }
217}