Skip to main content

ironflow_ops_git/
checkout.rs

1//! Checkout operations.
2
3use 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/// Output of checkout operations.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct CheckoutOutput {
18    pub checked_out: String,
19}
20
21/// Checkout HEAD (reset the working directory to HEAD).
22///
23/// # Examples
24///
25/// ```no_run
26/// use ironflow_ops_git::checkout::CheckoutHead;
27/// use ironflow_core::operation::Operation;
28///
29/// let op = CheckoutHead::new("/path/to/repo");
30/// assert_eq!(op.kind(), "git");
31/// ```
32pub struct CheckoutHead {
33    repo_path: PathBuf,
34}
35
36impl CheckoutHead {
37    /// Create a new checkout-head operation.
38    pub fn new(repo_path: impl Into<PathBuf>) -> Self {
39        Self {
40            repo_path: repo_path.into(),
41        }
42    }
43
44    /// Execute and return a typed result.
45    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
75/// Checkout the index (update the working directory from the index).
76///
77/// # Examples
78///
79/// ```no_run
80/// use ironflow_ops_git::checkout::CheckoutIndex;
81/// use ironflow_core::operation::Operation;
82///
83/// let op = CheckoutIndex::new("/path/to/repo");
84/// assert_eq!(op.kind(), "git");
85/// ```
86pub struct CheckoutIndex {
87    repo_path: PathBuf,
88}
89
90impl CheckoutIndex {
91    /// Create a new checkout-index operation.
92    pub fn new(repo_path: impl Into<PathBuf>) -> Self {
93        Self {
94            repo_path: repo_path.into(),
95        }
96    }
97
98    /// Execute and return a typed result.
99    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
129/// Checkout a specific tree object.
130///
131/// # Examples
132///
133/// ```no_run
134/// use ironflow_ops_git::checkout::CheckoutTree;
135/// use ironflow_core::operation::Operation;
136///
137/// let op = CheckoutTree::new("/path/to/repo", "abc123");
138/// assert_eq!(op.kind(), "git");
139/// ```
140pub struct CheckoutTree {
141    repo_path: PathBuf,
142    treeish: String,
143}
144
145impl CheckoutTree {
146    /// Create a new checkout-tree operation.
147    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    /// Execute and return a typed result.
155    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}