use std::path::{Path, PathBuf};
use async_trait::async_trait;
use git2::{Repository, RepositoryState};
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};
fn repo_state_label(state: RepositoryState) -> &'static str {
match state {
RepositoryState::Clean => "clean",
RepositoryState::Merge => "merge",
RepositoryState::Revert | RepositoryState::RevertSequence => "revert",
RepositoryState::CherryPickSequence | RepositoryState::CherryPick => "cherrypick",
RepositoryState::Bisect => "bisect",
RepositoryState::Rebase
| RepositoryState::RebaseInteractive
| RepositoryState::RebaseMerge => "rebase",
RepositoryState::ApplyMailbox | RepositoryState::ApplyMailboxOrRebase => "apply-mailbox",
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoInitOutput {
pub path: PathBuf,
pub bare: bool,
}
pub struct RepoInit {
path: PathBuf,
bare: bool,
}
impl RepoInit {
pub fn new(path: impl Into<PathBuf>, bare: bool) -> Self {
Self {
path: path.into(),
bare,
}
}
pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoInitOutput, OperationError> {
let path = self.path.clone();
let bare = self.bare;
blocking(move || {
if bare {
Repository::init_bare(&path)?;
} else {
Repository::init(&path)?;
}
Ok(RepoInitOutput { path, bare })
})
.await
}
}
#[async_trait]
impl Operation for RepoInit {
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!({ "path": self.path, "bare": self.bare }))
}
}
impl TypedOperation for RepoInit {
type Output = RepoInitOutput;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoOpenOutput {
pub path: PathBuf,
pub bare: bool,
}
pub struct RepoOpen {
path: PathBuf,
}
impl RepoOpen {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoOpenOutput, OperationError> {
let path = self.path.clone();
blocking(move || {
let repo = Repository::open(&path)?;
let is_bare = repo.is_bare();
let workdir = repo.workdir().map(Path::to_path_buf);
Ok(RepoOpenOutput {
path: workdir.unwrap_or(path),
bare: is_bare,
})
})
.await
}
}
#[async_trait]
impl Operation for RepoOpen {
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!({ "path": self.path }))
}
}
impl TypedOperation for RepoOpen {
type Output = RepoOpenOutput;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoCloneOutput {
pub url: String,
pub path: PathBuf,
}
pub struct RepoClone {
url: String,
path: PathBuf,
}
impl RepoClone {
pub fn new(url: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self {
url: url.into(),
path: path.into(),
}
}
pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoCloneOutput, OperationError> {
let url = self.url.clone();
let path = self.path.clone();
blocking(move || {
Repository::clone(&url, &path)?;
Ok(RepoCloneOutput { url, path })
})
.await
}
}
#[async_trait]
impl Operation for RepoClone {
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!({ "url": self.url, "path": self.path }))
}
}
impl TypedOperation for RepoClone {
type Output = RepoCloneOutput;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoDiscoverOutput {
pub path: Option<PathBuf>,
pub bare: bool,
}
pub struct RepoDiscover {
start_path: PathBuf,
}
impl RepoDiscover {
pub fn new(start_path: impl Into<PathBuf>) -> Self {
Self {
start_path: start_path.into(),
}
}
pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoDiscoverOutput, OperationError> {
let start = self.start_path.clone();
blocking(move || {
let repo = Repository::discover(&start)?;
let workdir = repo.workdir().map(Path::to_path_buf);
let is_bare = repo.is_bare();
Ok(RepoDiscoverOutput {
path: workdir,
bare: is_bare,
})
})
.await
}
}
#[async_trait]
impl Operation for RepoDiscover {
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!({ "start_path": self.start_path }))
}
}
impl TypedOperation for RepoDiscover {
type Output = RepoDiscoverOutput;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoStateOutput {
pub state: String,
}
pub struct RepoState {
repo_path: PathBuf,
}
impl RepoState {
pub fn new(repo_path: impl Into<PathBuf>) -> Self {
Self {
repo_path: repo_path.into(),
}
}
pub async fn run(&self, _ctx: &OperationContext) -> Result<RepoStateOutput, OperationError> {
let path = self.repo_path.clone();
blocking(move || {
let repo = Repository::open(&path)?;
let state = repo_state_label(repo.state()).to_string();
Ok(RepoStateOutput { state })
})
.await
}
}
#[async_trait]
impl Operation for RepoState {
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 RepoState {
type Output = RepoStateOutput;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::ctx;
#[tokio::test]
async fn init_creates_repo() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("new-repo");
let op = RepoInit::new(&target, false);
let result = op.run(&ctx()).await.unwrap();
assert!(!result.bare);
assert!(target.join(".git").exists());
}
#[tokio::test]
async fn init_creates_bare_repo() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("bare-repo");
let op = RepoInit::new(&target, true);
let result = op.run(&ctx()).await.unwrap();
assert!(result.bare);
assert!(target.join("HEAD").exists());
}
#[tokio::test]
async fn clone_local() {
let tmp = tempfile::tempdir().unwrap();
let origin = tmp.path().join("origin");
Repository::init(&origin).unwrap();
let target = tmp.path().join("clone");
let url = origin.to_str().unwrap();
let op = RepoClone::new(url, &target);
let result = op.run(&ctx()).await.unwrap();
assert_eq!(result.path, target);
assert!(target.join(".git").exists());
}
#[tokio::test]
async fn discover_finds_repo() {
let tmp = tempfile::tempdir().unwrap();
Repository::init(tmp.path()).unwrap();
let subdir = tmp.path().join("a").join("b");
std::fs::create_dir_all(&subdir).unwrap();
let op = RepoDiscover::new(&subdir);
let result = op.run(&ctx()).await.unwrap();
assert!(!result.bare);
}
#[tokio::test]
async fn state_on_clean_repo() {
let tmp = tempfile::tempdir().unwrap();
Repository::init(tmp.path()).unwrap();
let op = RepoState::new(tmp.path());
let result = op.run(&ctx()).await.unwrap();
assert_eq!(result.state, "clean");
}
}