daytona_client/
git.rs

1//! Git operations within sandboxes and workspaces
2
3use tracing::{debug, info};
4use uuid::Uuid;
5
6use crate::{client::DaytonaClient, error::Result, models::git::*};
7
8/// Manager for Git operations
9pub struct GitManager<'a> {
10    client: &'a DaytonaClient,
11}
12
13impl<'a> GitManager<'a> {
14    /// Create a new Git manager
15    pub fn new(client: &'a DaytonaClient) -> Self {
16        Self { client }
17    }
18
19    /// Clone a repository into a sandbox
20    pub async fn clone(&self, sandbox_id: &Uuid, request: GitCloneRequest) -> Result<()> {
21        info!(
22            "Cloning repository {} into sandbox {}",
23            request.url, sandbox_id
24        );
25
26        let response = self
27            .client
28            .build_request(
29                reqwest::Method::POST,
30                &format!("/toolbox/{}/toolbox/git/clone", sandbox_id),
31            )
32            .json(&request)
33            .send()
34            .await?;
35
36        if response.status().is_success() {
37            Ok(())
38        } else {
39            let error_text = response.text().await?;
40            Err(crate::error::DaytonaError::RequestFailed(format!(
41                "Failed to clone repository: {}",
42                error_text
43            )))
44        }
45    }
46
47    /// Get Git status
48    pub async fn status(&self, sandbox_id: &Uuid, path: &str) -> Result<GitStatus> {
49        debug!("Getting Git status for {} in sandbox {}", path, sandbox_id);
50
51        let response = self
52            .client
53            .build_request(
54                reqwest::Method::GET,
55                &format!(
56                    "/toolbox/{}/toolbox/git/status?path={}",
57                    sandbox_id,
58                    urlencoding::encode(path)
59                ),
60            )
61            .send()
62            .await?;
63
64        self.client.handle_response(response).await
65    }
66
67    /// Commit changes
68    pub async fn commit(&self, sandbox_id: &Uuid, request: GitCommitRequest) -> Result<String> {
69        info!(
70            "Committing changes in {} for sandbox {}",
71            request.path, sandbox_id
72        );
73
74        let response = self
75            .client
76            .build_request(
77                reqwest::Method::POST,
78                &format!("/toolbox/{}/toolbox/git/commit", sandbox_id),
79            )
80            .json(&request)
81            .send()
82            .await?;
83
84        if response.status().is_success() {
85            let result: serde_json::Value = self.client.handle_response(response).await?;
86            Ok(result["commit_hash"].as_str().unwrap_or("").to_string())
87        } else {
88            let error_text = response.text().await?;
89            Err(crate::error::DaytonaError::RequestFailed(format!(
90                "Failed to commit changes: {}",
91                error_text
92            )))
93        }
94    }
95
96    /// Push changes to remote
97    pub async fn push(&self, sandbox_id: &Uuid, path: &str, request: GitPushRequest) -> Result<()> {
98        info!("Pushing changes from {} in sandbox {}", path, sandbox_id);
99
100        let response = self
101            .client
102            .build_request(
103                reqwest::Method::POST,
104                &format!(
105                    "/toolbox/{}/toolbox/git/push?path={}",
106                    sandbox_id,
107                    urlencoding::encode(path)
108                ),
109            )
110            .json(&request)
111            .send()
112            .await?;
113
114        if response.status().is_success() {
115            Ok(())
116        } else {
117            let error_text = response.text().await?;
118            Err(crate::error::DaytonaError::RequestFailed(format!(
119                "Failed to push changes: {}",
120                error_text
121            )))
122        }
123    }
124
125    /// Pull changes from remote
126    pub async fn pull(&self, sandbox_id: &Uuid, path: &str, request: GitPullRequest) -> Result<()> {
127        info!("Pulling changes to {} in sandbox {}", path, sandbox_id);
128
129        let response = self
130            .client
131            .build_request(
132                reqwest::Method::POST,
133                &format!(
134                    "/toolbox/{}/toolbox/git/pull?path={}",
135                    sandbox_id,
136                    urlencoding::encode(path)
137                ),
138            )
139            .json(&request)
140            .send()
141            .await?;
142
143        if response.status().is_success() {
144            Ok(())
145        } else {
146            let error_text = response.text().await?;
147            Err(crate::error::DaytonaError::RequestFailed(format!(
148                "Failed to pull changes: {}",
149                error_text
150            )))
151        }
152    }
153
154    /// Get Git history (log)
155    pub async fn history(&self, sandbox_id: &Uuid, path: &str) -> Result<Vec<GitCommitInfo>> {
156        debug!("Getting Git history for {} in sandbox {}", path, sandbox_id);
157
158        let url = format!(
159            "/toolbox/{}/toolbox/git/history?path={}",
160            sandbox_id,
161            urlencoding::encode(path)
162        );
163
164        let response = self
165            .client
166            .build_request(reqwest::Method::GET, &url)
167            .send()
168            .await?;
169
170        self.client.handle_response(response).await
171    }
172
173    /// Get Git log (alias for history)
174    pub async fn log(&self, sandbox_id: &Uuid, path: &str) -> Result<Vec<GitCommitInfo>> {
175        self.history(sandbox_id, path).await
176    }
177
178    /// List branches
179    pub async fn list_branches(&self, sandbox_id: &Uuid, path: &str) -> Result<Vec<String>> {
180        debug!(
181            "Listing Git branches for {} in sandbox {}",
182            path, sandbox_id
183        );
184
185        let response = self
186            .client
187            .build_request(
188                reqwest::Method::GET,
189                &format!(
190                    "/toolbox/{}/toolbox/git/branches?path={}",
191                    sandbox_id,
192                    urlencoding::encode(path)
193                ),
194            )
195            .send()
196            .await?;
197
198        let result: ListBranchResponse = self.client.handle_response(response).await?;
199        Ok(result.branches)
200    }
201
202    /// Create branch
203    pub async fn create_branch(
204        &self,
205        sandbox_id: &Uuid,
206        path: &str,
207        branch_name: &str,
208    ) -> Result<()> {
209        info!(
210            "Creating branch {} in {} for sandbox {}",
211            branch_name, path, sandbox_id
212        );
213
214        let body = serde_json::json!({
215            "path": path,
216            "name": branch_name
217        });
218
219        let response = self
220            .client
221            .build_request(
222                reqwest::Method::POST,
223                &format!("/toolbox/{}/toolbox/git/branches", sandbox_id),
224            )
225            .json(&body)
226            .send()
227            .await?;
228
229        if response.status().is_success() {
230            Ok(())
231        } else {
232            let error_text = response.text().await?;
233            Err(crate::error::DaytonaError::RequestFailed(format!(
234                "Failed to create branch: {}",
235                error_text
236            )))
237        }
238    }
239
240    /// Switch branch
241    pub async fn switch_branch(
242        &self,
243        sandbox_id: &Uuid,
244        path: &str,
245        branch_name: &str,
246    ) -> Result<()> {
247        info!(
248            "Switching to branch {} in {} for sandbox {}",
249            branch_name, path, sandbox_id
250        );
251
252        let body = serde_json::json!({
253            "path": path,
254            "branch": branch_name
255        });
256
257        let response = self
258            .client
259            .build_request(
260                reqwest::Method::POST,
261                &format!("/toolbox/{}/toolbox/git/checkout", sandbox_id),
262            )
263            .json(&body)
264            .send()
265            .await?;
266
267        if response.status().is_success() {
268            Ok(())
269        } else {
270            let error_text = response.text().await?;
271            Err(crate::error::DaytonaError::RequestFailed(format!(
272                "Failed to switch branch: {}",
273                error_text
274            )))
275        }
276    }
277
278    /// Delete branch
279    pub async fn delete_branch(
280        &self,
281        sandbox_id: &Uuid,
282        path: &str,
283        branch_name: &str,
284    ) -> Result<()> {
285        info!(
286            "Deleting branch {} in {} for sandbox {}",
287            branch_name, path, sandbox_id
288        );
289
290        let body = serde_json::json!({
291            "path": path,
292            "name": branch_name
293        });
294
295        let response = self
296            .client
297            .build_request(
298                reqwest::Method::DELETE,
299                &format!("/toolbox/{}/toolbox/git/branches", sandbox_id),
300            )
301            .json(&body)
302            .send()
303            .await?;
304
305        if response.status().is_success() {
306            Ok(())
307        } else {
308            let error_text = response.text().await?;
309            Err(crate::error::DaytonaError::RequestFailed(format!(
310                "Failed to delete branch: {}",
311                error_text
312            )))
313        }
314    }
315
316    /// Stage files
317    pub async fn stage(&self, sandbox_id: &Uuid, path: &str, files: Vec<String>) -> Result<()> {
318        info!(
319            "Staging {} files in {} for sandbox {}",
320            files.len(),
321            path,
322            sandbox_id
323        );
324
325        let body = serde_json::json!({
326            "path": path,
327            "files": files
328        });
329
330        let response = self
331            .client
332            .build_request(
333                reqwest::Method::POST,
334                &format!("/toolbox/{}/toolbox/git/add", sandbox_id),
335            )
336            .json(&body)
337            .send()
338            .await?;
339
340        if response.status().is_success() {
341            Ok(())
342        } else {
343            let error_text = response.text().await?;
344            Err(crate::error::DaytonaError::RequestFailed(format!(
345                "Failed to stage files: {}",
346                error_text
347            )))
348        }
349    }
350}
351
352impl DaytonaClient {
353    /// Get Git manager
354    pub fn git(&self) -> GitManager<'_> {
355        GitManager::new(self)
356    }
357}