use std::path::{Path, PathBuf};
use async_trait::async_trait;
use git2::{IndexAddOption, Repository};
use ironflow_core::error::OperationError;
use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::helpers::{blocking, to_value};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexPathOutput {
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexPathspecsOutput {
pub pathspecs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexUpdateAllOutput {
pub updated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexWriteTreeOutput {
pub tree_oid: String,
}
pub struct IndexAdd {
repo_path: PathBuf,
path: String,
}
impl IndexAdd {
pub fn new(repo_path: impl Into<PathBuf>, path: impl Into<String>) -> Self {
Self {
repo_path: repo_path.into(),
path: path.into(),
}
}
pub async fn run(&self, _ctx: &OperationContext) -> Result<IndexPathOutput, OperationError> {
let repo_path = self.repo_path.clone();
let file_path = self.path.clone();
blocking(move || {
let repo = Repository::open(&repo_path)?;
let mut index = repo.index()?;
index.add_path(Path::new(&file_path))?;
index.write()?;
Ok(IndexPathOutput { path: file_path })
})
.await
}
}
#[async_trait]
impl Operation for IndexAdd {
fn kind(&self) -> &str {
"git"
}
async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
to_value(&self.run(ctx).await?)
}
fn input(&self) -> Option<Value> {
Some(serde_json::json!({ "repo_path": self.repo_path, "path": self.path }))
}
}
impl TypedOperation for IndexAdd {
type Output = IndexPathOutput;
}
pub struct IndexAddAll {
repo_path: PathBuf,
pathspecs: Vec<String>,
}
impl IndexAddAll {
pub fn new(repo_path: impl Into<PathBuf>, pathspecs: Vec<impl Into<String>>) -> Self {
Self {
repo_path: repo_path.into(),
pathspecs: pathspecs.into_iter().map(Into::into).collect(),
}
}
pub async fn run(
&self,
_ctx: &OperationContext,
) -> Result<IndexPathspecsOutput, OperationError> {
let repo_path = self.repo_path.clone();
let pathspecs = self.pathspecs.clone();
blocking(move || {
let repo = Repository::open(&repo_path)?;
let mut index = repo.index()?;
index.add_all(&pathspecs, IndexAddOption::DEFAULT, None)?;
index.write()?;
Ok(IndexPathspecsOutput { pathspecs })
})
.await
}
}
#[async_trait]
impl Operation for IndexAddAll {
fn kind(&self) -> &str {
"git"
}
async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
to_value(&self.run(ctx).await?)
}
fn input(&self) -> Option<Value> {
Some(serde_json::json!({ "repo_path": self.repo_path, "pathspecs": self.pathspecs }))
}
}
impl TypedOperation for IndexAddAll {
type Output = IndexPathspecsOutput;
}
pub struct IndexRemove {
repo_path: PathBuf,
path: String,
}
impl IndexRemove {
pub fn new(repo_path: impl Into<PathBuf>, path: impl Into<String>) -> Self {
Self {
repo_path: repo_path.into(),
path: path.into(),
}
}
pub async fn run(&self, _ctx: &OperationContext) -> Result<IndexPathOutput, OperationError> {
let repo_path = self.repo_path.clone();
let file_path = self.path.clone();
blocking(move || {
let repo = Repository::open(&repo_path)?;
let mut index = repo.index()?;
index.remove_path(Path::new(&file_path))?;
index.write()?;
Ok(IndexPathOutput { path: file_path })
})
.await
}
}
#[async_trait]
impl Operation for IndexRemove {
fn kind(&self) -> &str {
"git"
}
async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
to_value(&self.run(ctx).await?)
}
fn input(&self) -> Option<Value> {
Some(serde_json::json!({ "repo_path": self.repo_path, "path": self.path }))
}
}
impl TypedOperation for IndexRemove {
type Output = IndexPathOutput;
}
pub struct IndexRemoveAll {
repo_path: PathBuf,
pathspecs: Vec<String>,
}
impl IndexRemoveAll {
pub fn new(repo_path: impl Into<PathBuf>, pathspecs: Vec<impl Into<String>>) -> Self {
Self {
repo_path: repo_path.into(),
pathspecs: pathspecs.into_iter().map(Into::into).collect(),
}
}
pub async fn run(
&self,
_ctx: &OperationContext,
) -> Result<IndexPathspecsOutput, OperationError> {
let repo_path = self.repo_path.clone();
let pathspecs = self.pathspecs.clone();
blocking(move || {
let repo = Repository::open(&repo_path)?;
let mut index = repo.index()?;
index.remove_all(&pathspecs, None)?;
index.write()?;
Ok(IndexPathspecsOutput { pathspecs })
})
.await
}
}
#[async_trait]
impl Operation for IndexRemoveAll {
fn kind(&self) -> &str {
"git"
}
async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
to_value(&self.run(ctx).await?)
}
fn input(&self) -> Option<Value> {
Some(serde_json::json!({ "repo_path": self.repo_path, "pathspecs": self.pathspecs }))
}
}
impl TypedOperation for IndexRemoveAll {
type Output = IndexPathspecsOutput;
}
pub struct IndexUpdateAll {
repo_path: PathBuf,
}
impl IndexUpdateAll {
pub fn new(repo_path: impl Into<PathBuf>) -> Self {
Self {
repo_path: repo_path.into(),
}
}
pub async fn run(
&self,
_ctx: &OperationContext,
) -> Result<IndexUpdateAllOutput, OperationError> {
let repo_path = self.repo_path.clone();
blocking(move || {
let repo = Repository::open(&repo_path)?;
let mut index = repo.index()?;
index.update_all(["*"], None)?;
index.write()?;
Ok(IndexUpdateAllOutput { updated: true })
})
.await
}
}
#[async_trait]
impl Operation for IndexUpdateAll {
fn kind(&self) -> &str {
"git"
}
async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
to_value(&self.run(ctx).await?)
}
fn input(&self) -> Option<Value> {
Some(serde_json::json!({ "repo_path": self.repo_path }))
}
}
impl TypedOperation for IndexUpdateAll {
type Output = IndexUpdateAllOutput;
}
pub struct IndexWriteTree {
repo_path: PathBuf,
}
impl IndexWriteTree {
pub fn new(repo_path: impl Into<PathBuf>) -> Self {
Self {
repo_path: repo_path.into(),
}
}
pub async fn run(
&self,
_ctx: &OperationContext,
) -> Result<IndexWriteTreeOutput, OperationError> {
let repo_path = self.repo_path.clone();
blocking(move || {
let repo = Repository::open(&repo_path)?;
let mut index = repo.index()?;
let oid = index.write_tree()?;
Ok(IndexWriteTreeOutput {
tree_oid: oid.to_string(),
})
})
.await
}
}
#[async_trait]
impl Operation for IndexWriteTree {
fn kind(&self) -> &str {
"git"
}
async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
to_value(&self.run(ctx).await?)
}
fn input(&self) -> Option<Value> {
Some(serde_json::json!({ "repo_path": self.repo_path }))
}
}
impl TypedOperation for IndexWriteTree {
type Output = IndexWriteTreeOutput;
}
#[cfg(test)]
mod tests {
use std::fs;
use ironflow_core::operation::Operation;
use super::*;
use crate::test_helpers::{ctx, init_repo};
#[tokio::test]
async fn add_and_remove() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join("new.txt"), "n").unwrap();
let result = IndexAdd::new(tmp.path(), "new.txt")
.run(&ctx())
.await
.unwrap();
assert_eq!(result.path, "new.txt");
let result = IndexRemove::new(tmp.path(), "new.txt")
.run(&ctx())
.await
.unwrap();
assert_eq!(result.path, "new.txt");
}
#[tokio::test]
async fn add_all_with_glob() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join("a.rs"), "a").unwrap();
fs::write(tmp.path().join("b.rs"), "b").unwrap();
let result = IndexAddAll::new(tmp.path(), vec!["*.rs"])
.run(&ctx())
.await
.unwrap();
assert_eq!(result.pathspecs, vec!["*.rs"]);
}
#[tokio::test]
async fn update_all() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
fs::write(tmp.path().join("file.txt"), "modified").unwrap();
let result = IndexUpdateAll::new(tmp.path()).run(&ctx()).await.unwrap();
assert!(result.updated);
}
#[tokio::test]
async fn write_tree_returns_oid() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
let result = IndexWriteTree::new(tmp.path()).run(&ctx()).await.unwrap();
assert!(!result.tree_oid.is_empty());
}
#[tokio::test]
async fn add_nonexistent_file_fails() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
assert!(
IndexAdd::new(tmp.path(), "nope.txt")
.run(&ctx())
.await
.is_err()
);
}
#[tokio::test]
async fn execute_serializes_correctly() {
let tmp = tempfile::tempdir().unwrap();
init_repo(tmp.path());
let value = IndexWriteTree::new(tmp.path())
.execute(&ctx())
.await
.unwrap();
assert!(value["tree_oid"].is_string());
}
}