mockforge_core/pr_generation/
gitlab.rs1use crate::pr_generation::types::{PRFileChange, PRFileChangeType, PRRequest, PRResult};
6use crate::Error;
7use reqwest::Client;
8
9#[derive(Debug, Clone)]
11pub struct GitLabPRClient {
12 owner: String,
13 repo: String,
14 token: String,
15 base_branch: String,
16 client: Client,
17 api_url: String,
18}
19
20impl GitLabPRClient {
21 pub fn new(owner: String, repo: String, token: String, base_branch: String) -> Self {
23 Self {
24 owner,
25 repo,
26 token,
27 base_branch,
28 client: Client::new(),
29 api_url: "https://gitlab.com/api/v4".to_string(),
30 }
31 }
32
33 pub async fn create_pr(&self, request: PRRequest) -> crate::Result<PRResult> {
35 let project_path = format!("{}/{}", self.owner, self.repo);
37
38 self.create_branch(&request.branch).await?;
40
41 for file_change in &request.files {
43 match file_change.change_type {
44 PRFileChangeType::Create | PRFileChangeType::Update => {
45 self.commit_file(&request.branch, file_change).await?;
46 }
47 PRFileChangeType::Delete => {
48 self.delete_file(&request.branch, file_change).await?;
49 }
50 }
51 }
52
53 let mr = self.create_merge_request(&request, &project_path).await?;
55
56 if !request.labels.is_empty() {
58 self.add_labels(mr.number, &request.labels, &project_path).await?;
59 }
60
61 Ok(mr)
62 }
63
64 async fn create_branch(&self, branch: &str) -> crate::Result<()> {
65 let project_path = format!("{}/{}", self.owner, self.repo);
66 let url = format!(
67 "{}/projects/{}/repository/branches",
68 self.api_url,
69 urlencoding::encode(&project_path)
70 );
71
72 let body = serde_json::json!({
73 "branch": branch,
74 "ref": self.base_branch
75 });
76
77 let response = self
78 .client
79 .post(&url)
80 .header("PRIVATE-TOKEN", &self.token)
81 .json(&body)
82 .send()
83 .await
84 .map_err(|e| Error::generic(format!("Failed to create branch: {}", e)))?;
85
86 let status = response.status();
87 if !status.is_success() {
88 let error_text = response.text().await.unwrap_or_default();
89 if !error_text.contains("already exists") {
91 return Err(Error::generic(format!(
92 "Failed to create branch: {} - {}",
93 status, error_text
94 )));
95 }
96 }
97
98 Ok(())
99 }
100
101 async fn commit_file(&self, branch: &str, file_change: &PRFileChange) -> crate::Result<()> {
102 let project_path = format!("{}/{}", self.owner, self.repo);
103 let url = format!(
104 "{}/projects/{}/repository/files/{}",
105 self.api_url,
106 urlencoding::encode(&project_path),
107 urlencoding::encode(&file_change.path)
108 );
109
110 let content = base64::encode(&file_change.content);
111
112 let body = serde_json::json!({
113 "branch": branch,
114 "content": content,
115 "encoding": "base64",
116 "commit_message": format!("Update {}", file_change.path)
117 });
118
119 let response = self
120 .client
121 .put(&url)
122 .header("PRIVATE-TOKEN", &self.token)
123 .json(&body)
124 .send()
125 .await
126 .map_err(|e| Error::generic(format!("Failed to commit file: {}", e)))?;
127
128 let status = response.status();
129 if !status.is_success() {
130 if status == 404 {
132 return self.create_file(branch, file_change).await;
133 }
134
135 let error_text = response.text().await.unwrap_or_default();
136 return Err(Error::generic(format!(
137 "Failed to commit file: {} - {}",
138 status, error_text
139 )));
140 }
141
142 Ok(())
143 }
144
145 async fn create_file(&self, branch: &str, file_change: &PRFileChange) -> crate::Result<()> {
146 let project_path = format!("{}/{}", self.owner, self.repo);
147 let url = format!(
148 "{}/projects/{}/repository/files/{}",
149 self.api_url,
150 urlencoding::encode(&project_path),
151 urlencoding::encode(&file_change.path)
152 );
153
154 let content = base64::encode(&file_change.content);
155
156 let body = serde_json::json!({
157 "branch": branch,
158 "content": content,
159 "encoding": "base64",
160 "commit_message": format!("Create {}", file_change.path)
161 });
162
163 let response = self
164 .client
165 .post(&url)
166 .header("PRIVATE-TOKEN", &self.token)
167 .json(&body)
168 .send()
169 .await
170 .map_err(|e| Error::generic(format!("Failed to create file: {}", e)))?;
171
172 let status = response.status();
173 if !status.is_success() {
174 let error_text = response.text().await.unwrap_or_default();
175 return Err(Error::generic(format!(
176 "Failed to create file: {} - {}",
177 status, error_text
178 )));
179 }
180
181 Ok(())
182 }
183
184 async fn delete_file(&self, branch: &str, file_change: &PRFileChange) -> crate::Result<()> {
185 let project_path = format!("{}/{}", self.owner, self.repo);
186 let url = format!(
187 "{}/projects/{}/repository/files/{}",
188 self.api_url,
189 urlencoding::encode(&project_path),
190 urlencoding::encode(&file_change.path)
191 );
192
193 let body = serde_json::json!({
194 "branch": branch,
195 "commit_message": format!("Delete {}", file_change.path)
196 });
197
198 let response = self
199 .client
200 .delete(&url)
201 .header("PRIVATE-TOKEN", &self.token)
202 .json(&body)
203 .send()
204 .await
205 .map_err(|e| Error::generic(format!("Failed to delete file: {}", e)))?;
206
207 let status = response.status();
208 if !status.is_success() {
209 let error_text = response.text().await.unwrap_or_default();
210 return Err(Error::generic(format!(
211 "Failed to delete file: {} - {}",
212 status, error_text
213 )));
214 }
215
216 Ok(())
217 }
218
219 async fn create_merge_request(
220 &self,
221 request: &PRRequest,
222 project_path: &str,
223 ) -> crate::Result<PRResult> {
224 let url = format!(
225 "{}/projects/{}/merge_requests",
226 self.api_url,
227 urlencoding::encode(project_path)
228 );
229
230 let body = serde_json::json!({
231 "source_branch": request.branch,
232 "target_branch": self.base_branch,
233 "title": request.title,
234 "description": request.body
235 });
236
237 let response = self
238 .client
239 .post(&url)
240 .header("PRIVATE-TOKEN", &self.token)
241 .json(&body)
242 .send()
243 .await
244 .map_err(|e| Error::generic(format!("Failed to create MR: {}", e)))?;
245
246 let status = response.status();
247 if !status.is_success() {
248 let error_text = response.text().await.unwrap_or_default();
249 return Err(Error::generic(format!(
250 "Failed to create MR: {} - {}",
251 status, error_text
252 )));
253 }
254
255 let json: serde_json::Value = response
256 .json()
257 .await
258 .map_err(|e| Error::generic(format!("Failed to parse response: {}", e)))?;
259
260 Ok(PRResult {
261 number: json["iid"].as_u64().ok_or_else(|| Error::generic("Missing MR number"))?,
262 url: json["web_url"]
263 .as_str()
264 .ok_or_else(|| Error::generic("Missing MR URL"))?
265 .to_string(),
266 branch: request.branch.clone(),
267 title: request.title.clone(),
268 })
269 }
270
271 async fn add_labels(
272 &self,
273 mr_number: u64,
274 labels: &[String],
275 project_path: &str,
276 ) -> crate::Result<()> {
277 let url = format!(
278 "{}/projects/{}/merge_requests/{}",
279 self.api_url,
280 urlencoding::encode(project_path),
281 mr_number
282 );
283
284 let body = serde_json::json!({
285 "add_labels": labels.join(",")
286 });
287
288 let response = self
289 .client
290 .put(&url)
291 .header("PRIVATE-TOKEN", &self.token)
292 .json(&body)
293 .send()
294 .await
295 .map_err(|e| Error::generic(format!("Failed to add labels: {}", e)))?;
296
297 let status = response.status();
298 if !status.is_success() {
299 return Err(Error::generic(format!("Failed to add labels: {}", status)));
300 }
301
302 Ok(())
303 }
304}