1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::{DescribeFormatOptions, DescribeOptions, Oid, Repository};
7use ironflow_core::error::OperationError;
8use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::helpers::{blocking, to_value};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct GraphAheadBehindOutput {
16 pub ahead: usize,
17 pub behind: usize,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct GraphDescendantOfOutput {
22 pub is_descendant: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct GraphDescribeOutput {
27 pub description: String,
28}
29
30pub struct GraphAheadBehind {
42 repo_path: PathBuf,
43 local: String,
44 upstream: String,
45}
46
47impl GraphAheadBehind {
48 pub fn new(
50 repo_path: impl Into<PathBuf>,
51 local: impl Into<String>,
52 upstream: impl Into<String>,
53 ) -> Self {
54 Self {
55 repo_path: repo_path.into(),
56 local: local.into(),
57 upstream: upstream.into(),
58 }
59 }
60
61 pub async fn run(
63 &self,
64 _ctx: &OperationContext,
65 ) -> Result<GraphAheadBehindOutput, OperationError> {
66 let repo_path = self.repo_path.clone();
67 let local = self.local.clone();
68 let upstream = self.upstream.clone();
69 blocking(move || {
70 let repo = Repository::open(&repo_path)?;
71 let local_oid = Oid::from_str(&local)?;
72 let upstream_oid = Oid::from_str(&upstream)?;
73 let (ahead, behind) = repo.graph_ahead_behind(local_oid, upstream_oid)?;
74 Ok(GraphAheadBehindOutput { ahead, behind })
75 })
76 .await
77 }
78}
79
80#[async_trait]
81impl Operation for GraphAheadBehind {
82 fn kind(&self) -> &str {
83 "git"
84 }
85 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
86 to_value(&self.run(ctx).await?)
87 }
88 fn input(&self) -> Option<Value> {
89 Some(
90 serde_json::json!({ "repo_path": self.repo_path, "local": self.local, "upstream": self.upstream }),
91 )
92 }
93}
94
95impl TypedOperation for GraphAheadBehind {
96 type Output = GraphAheadBehindOutput;
97}
98
99pub struct GraphDescendantOf {
111 repo_path: PathBuf,
112 commit: String,
113 ancestor: String,
114}
115
116impl GraphDescendantOf {
117 pub fn new(
119 repo_path: impl Into<PathBuf>,
120 commit: impl Into<String>,
121 ancestor: impl Into<String>,
122 ) -> Self {
123 Self {
124 repo_path: repo_path.into(),
125 commit: commit.into(),
126 ancestor: ancestor.into(),
127 }
128 }
129
130 pub async fn run(
132 &self,
133 _ctx: &OperationContext,
134 ) -> Result<GraphDescendantOfOutput, OperationError> {
135 let repo_path = self.repo_path.clone();
136 let commit = self.commit.clone();
137 let ancestor = self.ancestor.clone();
138 blocking(move || {
139 let repo = Repository::open(&repo_path)?;
140 let commit_oid = Oid::from_str(&commit)?;
141 let ancestor_oid = Oid::from_str(&ancestor)?;
142 let is_descendant = repo.graph_descendant_of(commit_oid, ancestor_oid)?;
143 Ok(GraphDescendantOfOutput { is_descendant })
144 })
145 .await
146 }
147}
148
149#[async_trait]
150impl Operation for GraphDescendantOf {
151 fn kind(&self) -> &str {
152 "git"
153 }
154 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
155 to_value(&self.run(ctx).await?)
156 }
157 fn input(&self) -> Option<Value> {
158 Some(
159 serde_json::json!({ "repo_path": self.repo_path, "commit": self.commit, "ancestor": self.ancestor }),
160 )
161 }
162}
163
164impl TypedOperation for GraphDescendantOf {
165 type Output = GraphDescendantOfOutput;
166}
167
168pub struct GraphDescribe {
180 repo_path: PathBuf,
181}
182
183impl GraphDescribe {
184 pub fn new(repo_path: impl Into<PathBuf>) -> Self {
186 Self {
187 repo_path: repo_path.into(),
188 }
189 }
190
191 pub async fn run(
193 &self,
194 _ctx: &OperationContext,
195 ) -> Result<GraphDescribeOutput, OperationError> {
196 let repo_path = self.repo_path.clone();
197 blocking(move || {
198 let repo = Repository::open(&repo_path)?;
199 let describe = repo.describe(DescribeOptions::new().describe_tags())?;
200 let formatted =
201 describe.format(Some(DescribeFormatOptions::new().dirty_suffix("-dirty")))?;
202 Ok(GraphDescribeOutput {
203 description: formatted,
204 })
205 })
206 .await
207 }
208}
209
210#[async_trait]
211impl Operation for GraphDescribe {
212 fn kind(&self) -> &str {
213 "git"
214 }
215 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
216 to_value(&self.run(ctx).await?)
217 }
218 fn input(&self) -> Option<Value> {
219 Some(serde_json::json!({ "repo_path": self.repo_path }))
220 }
221}
222
223impl TypedOperation for GraphDescribe {
224 type Output = GraphDescribeOutput;
225}
226
227#[cfg(test)]
228mod tests {
229 use git2::Repository;
230 use ironflow_core::operation::Operation;
231
232 use super::*;
233 use crate::test_helpers::{ctx, make_two_commits};
234
235 #[tokio::test]
236 async fn ahead_behind_counts() {
237 let tmp = tempfile::tempdir().unwrap();
238 let (c1, c2) = make_two_commits(tmp.path());
239 let result = GraphAheadBehind::new(tmp.path(), &c2, &c1)
240 .run(&ctx())
241 .await
242 .unwrap();
243 assert_eq!(result.ahead, 1);
244 assert_eq!(result.behind, 0);
245
246 let result = GraphAheadBehind::new(tmp.path(), &c1, &c2)
247 .run(&ctx())
248 .await
249 .unwrap();
250 assert_eq!(result.ahead, 0);
251 assert_eq!(result.behind, 1);
252 }
253
254 #[tokio::test]
255 async fn descendant_of() {
256 let tmp = tempfile::tempdir().unwrap();
257 let (c1, c2) = make_two_commits(tmp.path());
258 let result = GraphDescendantOf::new(tmp.path(), &c2, &c1)
259 .run(&ctx())
260 .await
261 .unwrap();
262 assert!(result.is_descendant);
263 let result = GraphDescendantOf::new(tmp.path(), &c1, &c2)
264 .run(&ctx())
265 .await
266 .unwrap();
267 assert!(!result.is_descendant);
268 }
269
270 #[tokio::test]
271 async fn describe_with_tag() {
272 let tmp = tempfile::tempdir().unwrap();
273 make_two_commits(tmp.path());
274 let repo = Repository::open(tmp.path()).unwrap();
275 let head = repo.head().unwrap().peel_to_commit().unwrap();
276 repo.tag_lightweight("v1.0", head.as_object(), false)
277 .unwrap();
278 let result = GraphDescribe::new(tmp.path()).run(&ctx()).await.unwrap();
279 assert!(result.description.contains("v1.0"));
280 }
281
282 #[tokio::test]
283 async fn execute_serializes_correctly() {
284 let tmp = tempfile::tempdir().unwrap();
285 let (c1, c2) = make_two_commits(tmp.path());
286 let value = GraphAheadBehind::new(tmp.path(), &c2, &c1)
287 .execute(&ctx())
288 .await
289 .unwrap();
290 assert_eq!(value["ahead"], 1);
291 }
292}