ironflow_ops_git/
checkout.rs1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::build::CheckoutBuilder;
7use git2::{Oid, Repository};
8use ironflow_core::error::OperationError;
9use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::helpers::{blocking, to_value};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct CheckoutOutput {
18 pub checked_out: String,
19}
20
21pub struct CheckoutHead {
33 repo_path: PathBuf,
34}
35
36impl CheckoutHead {
37 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
39 Self {
40 repo_path: repo_path.into(),
41 }
42 }
43
44 pub async fn run(&self, _ctx: &OperationContext) -> Result<CheckoutOutput, OperationError> {
46 let repo_path = self.repo_path.clone();
47 blocking(move || {
48 let repo = Repository::open(&repo_path)?;
49 repo.checkout_head(Some(CheckoutBuilder::new().force()))?;
50 Ok(CheckoutOutput {
51 checked_out: "HEAD".to_string(),
52 })
53 })
54 .await
55 }
56}
57
58#[async_trait]
59impl Operation for CheckoutHead {
60 fn kind(&self) -> &str {
61 "git"
62 }
63 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
64 to_value(&self.run(ctx).await?)
65 }
66 fn input(&self) -> Option<Value> {
67 Some(serde_json::json!({ "repo_path": self.repo_path }))
68 }
69}
70
71impl TypedOperation for CheckoutHead {
72 type Output = CheckoutOutput;
73}
74
75pub struct CheckoutIndex {
87 repo_path: PathBuf,
88}
89
90impl CheckoutIndex {
91 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
93 Self {
94 repo_path: repo_path.into(),
95 }
96 }
97
98 pub async fn run(&self, _ctx: &OperationContext) -> Result<CheckoutOutput, OperationError> {
100 let repo_path = self.repo_path.clone();
101 blocking(move || {
102 let repo = Repository::open(&repo_path)?;
103 repo.checkout_index(None, Some(CheckoutBuilder::new().force()))?;
104 Ok(CheckoutOutput {
105 checked_out: "index".to_string(),
106 })
107 })
108 .await
109 }
110}
111
112#[async_trait]
113impl Operation for CheckoutIndex {
114 fn kind(&self) -> &str {
115 "git"
116 }
117 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
118 to_value(&self.run(ctx).await?)
119 }
120 fn input(&self) -> Option<Value> {
121 Some(serde_json::json!({ "repo_path": self.repo_path }))
122 }
123}
124
125impl TypedOperation for CheckoutIndex {
126 type Output = CheckoutOutput;
127}
128
129pub struct CheckoutTree {
141 repo_path: PathBuf,
142 treeish: String,
143}
144
145impl CheckoutTree {
146 pub fn new(repo_path: impl Into<PathBuf>, treeish: impl Into<String>) -> Self {
148 Self {
149 repo_path: repo_path.into(),
150 treeish: treeish.into(),
151 }
152 }
153
154 pub async fn run(&self, _ctx: &OperationContext) -> Result<CheckoutOutput, OperationError> {
156 let repo_path = self.repo_path.clone();
157 let treeish = self.treeish.clone();
158 blocking(move || {
159 let repo = Repository::open(&repo_path)?;
160 let oid = Oid::from_str(&treeish)?;
161 let object = repo.find_object(oid, None)?;
162 repo.checkout_tree(&object, Some(CheckoutBuilder::new().force()))?;
163 Ok(CheckoutOutput {
164 checked_out: treeish,
165 })
166 })
167 .await
168 }
169}
170
171#[async_trait]
172impl Operation for CheckoutTree {
173 fn kind(&self) -> &str {
174 "git"
175 }
176 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
177 to_value(&self.run(ctx).await?)
178 }
179 fn input(&self) -> Option<Value> {
180 Some(serde_json::json!({ "repo_path": self.repo_path, "treeish": self.treeish }))
181 }
182}
183
184impl TypedOperation for CheckoutTree {
185 type Output = CheckoutOutput;
186}
187
188#[cfg(test)]
189mod tests {
190 use std::fs;
191
192 use git2::Repository;
193 use ironflow_core::operation::Operation;
194
195 use super::*;
196 use crate::test_helpers::{ctx, init_repo};
197
198 #[tokio::test]
199 async fn checkout_head_restores_workdir() {
200 let tmp = tempfile::tempdir().unwrap();
201 init_repo(tmp.path());
202 fs::write(tmp.path().join("file.txt"), "dirty").unwrap();
203 let result = CheckoutHead::new(tmp.path()).run(&ctx()).await.unwrap();
204 assert_eq!(result.checked_out, "HEAD");
205 assert_eq!(
206 fs::read_to_string(tmp.path().join("file.txt")).unwrap(),
207 "content"
208 );
209 }
210
211 #[tokio::test]
212 async fn checkout_index_restores_from_index() {
213 let tmp = tempfile::tempdir().unwrap();
214 init_repo(tmp.path());
215 fs::write(tmp.path().join("file.txt"), "dirty").unwrap();
216 let result = CheckoutIndex::new(tmp.path()).run(&ctx()).await.unwrap();
217 assert_eq!(result.checked_out, "index");
218 assert_eq!(
219 fs::read_to_string(tmp.path().join("file.txt")).unwrap(),
220 "content"
221 );
222 }
223
224 #[tokio::test]
225 async fn checkout_tree_with_commit_oid() {
226 let tmp = tempfile::tempdir().unwrap();
227 init_repo(tmp.path());
228 let repo = Repository::open(tmp.path()).unwrap();
229 let oid = repo
230 .head()
231 .unwrap()
232 .peel_to_commit()
233 .unwrap()
234 .id()
235 .to_string();
236 let result = CheckoutTree::new(tmp.path(), &oid)
237 .run(&ctx())
238 .await
239 .unwrap();
240 assert_eq!(result.checked_out, oid);
241 }
242
243 #[tokio::test]
244 async fn checkout_tree_invalid_oid_fails() {
245 let tmp = tempfile::tempdir().unwrap();
246 init_repo(tmp.path());
247 assert!(
248 CheckoutTree::new(tmp.path(), "bad")
249 .run(&ctx())
250 .await
251 .is_err()
252 );
253 }
254
255 #[tokio::test]
256 async fn execute_serializes_correctly() {
257 let tmp = tempfile::tempdir().unwrap();
258 init_repo(tmp.path());
259 let value = CheckoutHead::new(tmp.path()).execute(&ctx()).await.unwrap();
260 assert_eq!(value["checked_out"], "HEAD");
261 }
262}