use std::path::PathBuf;
use async_trait::async_trait;
use git2::{DescribeFormatOptions, DescribeOptions, Oid, 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 GraphAheadBehindOutput {
pub ahead: usize,
pub behind: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphDescendantOfOutput {
pub is_descendant: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphDescribeOutput {
pub description: String,
}
pub struct GraphAheadBehind {
repo_path: PathBuf,
local: String,
upstream: String,
}
impl GraphAheadBehind {
pub fn new(
repo_path: impl Into<PathBuf>,
local: impl Into<String>,
upstream: impl Into<String>,
) -> Self {
Self {
repo_path: repo_path.into(),
local: local.into(),
upstream: upstream.into(),
}
}
pub async fn run(
&self,
_ctx: &OperationContext,
) -> Result<GraphAheadBehindOutput, OperationError> {
let repo_path = self.repo_path.clone();
let local = self.local.clone();
let upstream = self.upstream.clone();
blocking(move || {
let repo = Repository::open(&repo_path)?;
let local_oid = Oid::from_str(&local)?;
let upstream_oid = Oid::from_str(&upstream)?;
let (ahead, behind) = repo.graph_ahead_behind(local_oid, upstream_oid)?;
Ok(GraphAheadBehindOutput { ahead, behind })
})
.await
}
}
#[async_trait]
impl Operation for GraphAheadBehind {
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, "local": self.local, "upstream": self.upstream }),
)
}
}
impl TypedOperation for GraphAheadBehind {
type Output = GraphAheadBehindOutput;
}
pub struct GraphDescendantOf {
repo_path: PathBuf,
commit: String,
ancestor: String,
}
impl GraphDescendantOf {
pub fn new(
repo_path: impl Into<PathBuf>,
commit: impl Into<String>,
ancestor: impl Into<String>,
) -> Self {
Self {
repo_path: repo_path.into(),
commit: commit.into(),
ancestor: ancestor.into(),
}
}
pub async fn run(
&self,
_ctx: &OperationContext,
) -> Result<GraphDescendantOfOutput, OperationError> {
let repo_path = self.repo_path.clone();
let commit = self.commit.clone();
let ancestor = self.ancestor.clone();
blocking(move || {
let repo = Repository::open(&repo_path)?;
let commit_oid = Oid::from_str(&commit)?;
let ancestor_oid = Oid::from_str(&ancestor)?;
let is_descendant = repo.graph_descendant_of(commit_oid, ancestor_oid)?;
Ok(GraphDescendantOfOutput { is_descendant })
})
.await
}
}
#[async_trait]
impl Operation for GraphDescendantOf {
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, "commit": self.commit, "ancestor": self.ancestor }),
)
}
}
impl TypedOperation for GraphDescendantOf {
type Output = GraphDescendantOfOutput;
}
pub struct GraphDescribe {
repo_path: PathBuf,
}
impl GraphDescribe {
pub fn new(repo_path: impl Into<PathBuf>) -> Self {
Self {
repo_path: repo_path.into(),
}
}
pub async fn run(
&self,
_ctx: &OperationContext,
) -> Result<GraphDescribeOutput, OperationError> {
let repo_path = self.repo_path.clone();
blocking(move || {
let repo = Repository::open(&repo_path)?;
let describe = repo.describe(DescribeOptions::new().describe_tags())?;
let formatted =
describe.format(Some(DescribeFormatOptions::new().dirty_suffix("-dirty")))?;
Ok(GraphDescribeOutput {
description: formatted,
})
})
.await
}
}
#[async_trait]
impl Operation for GraphDescribe {
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 GraphDescribe {
type Output = GraphDescribeOutput;
}
#[cfg(test)]
mod tests {
use git2::Repository;
use ironflow_core::operation::Operation;
use super::*;
use crate::test_helpers::{ctx, make_two_commits};
#[tokio::test]
async fn ahead_behind_counts() {
let tmp = tempfile::tempdir().unwrap();
let (c1, c2) = make_two_commits(tmp.path());
let result = GraphAheadBehind::new(tmp.path(), &c2, &c1)
.run(&ctx())
.await
.unwrap();
assert_eq!(result.ahead, 1);
assert_eq!(result.behind, 0);
let result = GraphAheadBehind::new(tmp.path(), &c1, &c2)
.run(&ctx())
.await
.unwrap();
assert_eq!(result.ahead, 0);
assert_eq!(result.behind, 1);
}
#[tokio::test]
async fn descendant_of() {
let tmp = tempfile::tempdir().unwrap();
let (c1, c2) = make_two_commits(tmp.path());
let result = GraphDescendantOf::new(tmp.path(), &c2, &c1)
.run(&ctx())
.await
.unwrap();
assert!(result.is_descendant);
let result = GraphDescendantOf::new(tmp.path(), &c1, &c2)
.run(&ctx())
.await
.unwrap();
assert!(!result.is_descendant);
}
#[tokio::test]
async fn describe_with_tag() {
let tmp = tempfile::tempdir().unwrap();
make_two_commits(tmp.path());
let repo = Repository::open(tmp.path()).unwrap();
let head = repo.head().unwrap().peel_to_commit().unwrap();
repo.tag_lightweight("v1.0", head.as_object(), false)
.unwrap();
let result = GraphDescribe::new(tmp.path()).run(&ctx()).await.unwrap();
assert!(result.description.contains("v1.0"));
}
#[tokio::test]
async fn execute_serializes_correctly() {
let tmp = tempfile::tempdir().unwrap();
let (c1, c2) = make_two_commits(tmp.path());
let value = GraphAheadBehind::new(tmp.path(), &c2, &c1)
.execute(&ctx())
.await
.unwrap();
assert_eq!(value["ahead"], 1);
}
}