1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::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, credentials_callbacks, to_value};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct FetchPushOutput {
16 pub remote: String,
17 pub refspecs: Vec<String>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct RemotePruneOutput {
22 pub remote: String,
23 pub pruned: bool,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct RemoteDefaultBranchOutput {
28 pub remote: String,
29 pub default_branch: Option<String>,
30}
31
32pub struct FetchRemote {
44 repo_path: PathBuf,
45 remote_name: String,
46 refspecs: Vec<String>,
47}
48
49impl FetchRemote {
50 pub fn new(
52 repo_path: impl Into<PathBuf>,
53 remote_name: impl Into<String>,
54 refspecs: Vec<impl Into<String>>,
55 ) -> Self {
56 Self {
57 repo_path: repo_path.into(),
58 remote_name: remote_name.into(),
59 refspecs: refspecs.into_iter().map(Into::into).collect(),
60 }
61 }
62
63 pub async fn run(&self, _ctx: &OperationContext) -> Result<FetchPushOutput, OperationError> {
65 let repo_path = self.repo_path.clone();
66 let remote_name = self.remote_name.clone();
67 let refspecs = self.refspecs.clone();
68 blocking(move || {
69 let repo = Repository::open(&repo_path)?;
70 let mut remote = repo.find_remote(&remote_name)?;
71 let refs: Vec<&str> = refspecs.iter().map(String::as_str).collect();
72 let mut fetch_opts = git2::FetchOptions::new();
73 fetch_opts.remote_callbacks(credentials_callbacks());
74 remote.fetch(&refs, Some(&mut fetch_opts), None)?;
75 Ok(FetchPushOutput {
76 remote: remote_name,
77 refspecs,
78 })
79 })
80 .await
81 }
82}
83
84#[async_trait]
85impl Operation for FetchRemote {
86 fn kind(&self) -> &str {
87 "git"
88 }
89 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
90 to_value(&self.run(ctx).await?)
91 }
92 fn input(&self) -> Option<Value> {
93 Some(serde_json::json!({ "repo_path": self.repo_path, "remote": self.remote_name }))
94 }
95}
96
97impl TypedOperation for FetchRemote {
98 type Output = FetchPushOutput;
99}
100
101pub struct PushRemote {
113 repo_path: PathBuf,
114 remote_name: String,
115 refspecs: Vec<String>,
116}
117
118impl PushRemote {
119 pub fn new(
121 repo_path: impl Into<PathBuf>,
122 remote_name: impl Into<String>,
123 refspecs: Vec<impl Into<String>>,
124 ) -> Self {
125 Self {
126 repo_path: repo_path.into(),
127 remote_name: remote_name.into(),
128 refspecs: refspecs.into_iter().map(Into::into).collect(),
129 }
130 }
131
132 pub async fn run(&self, _ctx: &OperationContext) -> Result<FetchPushOutput, OperationError> {
134 let repo_path = self.repo_path.clone();
135 let remote_name = self.remote_name.clone();
136 let refspecs = self.refspecs.clone();
137 blocking(move || {
138 let repo = Repository::open(&repo_path)?;
139 let mut remote = repo.find_remote(&remote_name)?;
140 let refs: Vec<&str> = refspecs.iter().map(String::as_str).collect();
141 let mut push_opts = git2::PushOptions::new();
142 push_opts.remote_callbacks(credentials_callbacks());
143 remote.push(&refs, Some(&mut push_opts))?;
144 Ok(FetchPushOutput {
145 remote: remote_name,
146 refspecs,
147 })
148 })
149 .await
150 }
151}
152
153#[async_trait]
154impl Operation for PushRemote {
155 fn kind(&self) -> &str {
156 "git"
157 }
158 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
159 to_value(&self.run(ctx).await?)
160 }
161 fn input(&self) -> Option<Value> {
162 Some(serde_json::json!({ "repo_path": self.repo_path, "remote": self.remote_name }))
163 }
164}
165
166impl TypedOperation for PushRemote {
167 type Output = FetchPushOutput;
168}
169
170pub struct RemotePrune {
182 repo_path: PathBuf,
183 remote_name: String,
184}
185
186impl RemotePrune {
187 pub fn new(repo_path: impl Into<PathBuf>, remote_name: impl Into<String>) -> Self {
189 Self {
190 repo_path: repo_path.into(),
191 remote_name: remote_name.into(),
192 }
193 }
194
195 pub async fn run(&self, _ctx: &OperationContext) -> Result<RemotePruneOutput, OperationError> {
197 let repo_path = self.repo_path.clone();
198 let remote_name = self.remote_name.clone();
199 blocking(move || {
200 let repo = Repository::open(&repo_path)?;
201 let mut remote = repo.find_remote(&remote_name)?;
202 remote.prune(None)?;
203 Ok(RemotePruneOutput {
204 remote: remote_name,
205 pruned: true,
206 })
207 })
208 .await
209 }
210}
211
212#[async_trait]
213impl Operation for RemotePrune {
214 fn kind(&self) -> &str {
215 "git"
216 }
217 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
218 to_value(&self.run(ctx).await?)
219 }
220 fn input(&self) -> Option<Value> {
221 Some(serde_json::json!({ "repo_path": self.repo_path, "remote": self.remote_name }))
222 }
223}
224
225impl TypedOperation for RemotePrune {
226 type Output = RemotePruneOutput;
227}
228
229pub struct RemoteDefaultBranch {
241 repo_path: PathBuf,
242 remote_name: String,
243}
244
245impl RemoteDefaultBranch {
246 pub fn new(repo_path: impl Into<PathBuf>, remote_name: impl Into<String>) -> Self {
248 Self {
249 repo_path: repo_path.into(),
250 remote_name: remote_name.into(),
251 }
252 }
253
254 pub async fn run(
256 &self,
257 _ctx: &OperationContext,
258 ) -> Result<RemoteDefaultBranchOutput, OperationError> {
259 let repo_path = self.repo_path.clone();
260 let remote_name = self.remote_name.clone();
261 blocking(move || {
262 let repo = Repository::open(&repo_path)?;
263 let remote = repo.find_remote(&remote_name)?;
264 let default = remote.default_branch()?;
265 let name = default.as_str().map(String::from);
266 Ok(RemoteDefaultBranchOutput {
267 remote: remote_name,
268 default_branch: name,
269 })
270 })
271 .await
272 }
273}
274
275#[async_trait]
276impl Operation for RemoteDefaultBranch {
277 fn kind(&self) -> &str {
278 "git"
279 }
280 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
281 to_value(&self.run(ctx).await?)
282 }
283 fn input(&self) -> Option<Value> {
284 Some(serde_json::json!({ "repo_path": self.repo_path, "remote": self.remote_name }))
285 }
286}
287
288impl TypedOperation for RemoteDefaultBranch {
289 type Output = RemoteDefaultBranchOutput;
290}
291
292#[cfg(test)]
293mod tests {
294 use git2::Repository;
295
296 use super::*;
297 use crate::test_helpers::{ctx, init_repo};
298
299 fn setup_with_bare_remote() -> (tempfile::TempDir, tempfile::TempDir) {
305 let work = tempfile::tempdir().unwrap();
306 init_repo(work.path());
307 let bare = tempfile::tempdir().unwrap();
308 Repository::init_bare(bare.path()).unwrap();
309 let repo = Repository::open(work.path()).unwrap();
310 repo.remote("origin", bare.path().to_str().unwrap())
311 .unwrap();
312 (work, bare)
313 }
314
315 #[tokio::test]
316 async fn push_to_local_bare_remote() {
317 let (work, bare) = setup_with_bare_remote();
318 let result = PushRemote::new(
319 work.path(),
320 "origin",
321 vec!["refs/heads/master:refs/heads/master"],
322 )
323 .run(&ctx())
324 .await
325 .unwrap();
326 assert_eq!(result.remote, "origin");
327 let bare_repo = Repository::open(bare.path()).unwrap();
329 assert!(bare_repo.find_reference("refs/heads/master").is_ok());
330 }
331
332 #[tokio::test]
333 async fn fetch_from_local_bare_remote() {
334 let (work, bare) = setup_with_bare_remote();
335 PushRemote::new(
336 work.path(),
337 "origin",
338 vec!["refs/heads/master:refs/heads/master"],
339 )
340 .run(&ctx())
341 .await
342 .unwrap();
343 let clone_dir = tempfile::tempdir().unwrap();
345 let clone = Repository::init(clone_dir.path()).unwrap();
346 clone
347 .remote("origin", bare.path().to_str().unwrap())
348 .unwrap();
349 let result = FetchRemote::new(clone_dir.path(), "origin", vec!["master"])
350 .run(&ctx())
351 .await
352 .unwrap();
353 assert_eq!(result.remote, "origin");
354 }
355
356 #[tokio::test]
357 async fn push_missing_remote_fails() {
358 let work = tempfile::tempdir().unwrap();
359 init_repo(work.path());
360 let result = PushRemote::new(work.path(), "nope", vec!["refs/heads/master"])
361 .run(&ctx())
362 .await;
363 assert!(result.is_err());
364 }
365}