1use std::path::PathBuf;
4
5use async_trait::async_trait;
6use git2::{Oid, Repository, Signature};
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)]
16pub struct ReflogEntry {
17 pub id_new: String,
18 pub id_old: String,
19 pub message: String,
20 pub committer: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ReflogReadOutput {
25 pub refname: String,
26 pub entries: Vec<ReflogEntry>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ReflogAppendOutput {
31 pub refname: String,
32 pub appended: bool,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ReflogDropOutput {
37 pub refname: String,
38 pub index: usize,
39 pub dropped: bool,
40}
41
42pub struct ReflogRead {
54 repo_path: PathBuf,
55 refname: String,
56 limit: usize,
57}
58
59impl ReflogRead {
60 pub fn new(repo_path: impl Into<PathBuf>, refname: impl Into<String>, limit: usize) -> Self {
62 Self {
63 repo_path: repo_path.into(),
64 refname: refname.into(),
65 limit,
66 }
67 }
68
69 pub async fn run(&self, _ctx: &OperationContext) -> Result<ReflogReadOutput, OperationError> {
71 let repo_path = self.repo_path.clone();
72 let refname = self.refname.clone();
73 let limit = self.limit;
74 blocking(move || {
75 let repo = Repository::open(&repo_path)?;
76 let reflog = repo.reflog(&refname)?;
77 let entries: Vec<ReflogEntry> = (0..reflog.len().min(limit))
78 .filter_map(|i| reflog.get(i))
79 .map(|entry| ReflogEntry {
80 id_new: entry.id_new().to_string(),
81 id_old: entry.id_old().to_string(),
82 message: entry.message().unwrap_or("").to_string(),
83 committer: entry.committer().name().unwrap_or("").to_string(),
84 })
85 .collect();
86 Ok(ReflogReadOutput { refname, entries })
87 })
88 .await
89 }
90}
91
92#[async_trait]
93impl Operation for ReflogRead {
94 fn kind(&self) -> &str {
95 "git"
96 }
97 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
98 to_value(&self.run(ctx).await?)
99 }
100 fn input(&self) -> Option<Value> {
101 Some(serde_json::json!({ "repo_path": self.repo_path, "refname": self.refname }))
102 }
103}
104
105impl TypedOperation for ReflogRead {
106 type Output = ReflogReadOutput;
107}
108
109pub struct ReflogAppend {
121 repo_path: PathBuf,
122 refname: String,
123 oid: String,
124 message: String,
125 committer_name: String,
126 committer_email: String,
127}
128
129impl ReflogAppend {
130 pub fn new(
132 repo_path: impl Into<PathBuf>,
133 refname: impl Into<String>,
134 oid: impl Into<String>,
135 message: impl Into<String>,
136 committer_name: impl Into<String>,
137 committer_email: impl Into<String>,
138 ) -> Self {
139 Self {
140 repo_path: repo_path.into(),
141 refname: refname.into(),
142 oid: oid.into(),
143 message: message.into(),
144 committer_name: committer_name.into(),
145 committer_email: committer_email.into(),
146 }
147 }
148
149 pub async fn run(&self, _ctx: &OperationContext) -> Result<ReflogAppendOutput, OperationError> {
151 let repo_path = self.repo_path.clone();
152 let refname = self.refname.clone();
153 let oid_str = self.oid.clone();
154 let message = self.message.clone();
155 let name = self.committer_name.clone();
156 let email = self.committer_email.clone();
157 blocking(move || {
158 let repo = Repository::open(&repo_path)?;
159 let mut reflog = repo.reflog(&refname)?;
160 let oid = Oid::from_str(&oid_str)?;
161 let sig = Signature::now(&name, &email)?;
162 reflog.append(oid, &sig, Some(&message))?;
163 reflog.write()?;
164 Ok(ReflogAppendOutput {
165 refname,
166 appended: true,
167 })
168 })
169 .await
170 }
171}
172
173#[async_trait]
174impl Operation for ReflogAppend {
175 fn kind(&self) -> &str {
176 "git"
177 }
178 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
179 to_value(&self.run(ctx).await?)
180 }
181 fn input(&self) -> Option<Value> {
182 Some(serde_json::json!({ "repo_path": self.repo_path, "refname": self.refname }))
183 }
184}
185
186impl TypedOperation for ReflogAppend {
187 type Output = ReflogAppendOutput;
188}
189
190pub struct ReflogDrop {
202 repo_path: PathBuf,
203 refname: String,
204 index: usize,
205}
206
207impl ReflogDrop {
208 pub fn new(repo_path: impl Into<PathBuf>, refname: impl Into<String>, index: usize) -> Self {
210 Self {
211 repo_path: repo_path.into(),
212 refname: refname.into(),
213 index,
214 }
215 }
216
217 pub async fn run(&self, _ctx: &OperationContext) -> Result<ReflogDropOutput, OperationError> {
219 let repo_path = self.repo_path.clone();
220 let refname = self.refname.clone();
221 let index = self.index;
222 blocking(move || {
223 let repo = Repository::open(&repo_path)?;
224 let mut reflog = repo.reflog(&refname)?;
225 reflog.remove(index, true)?;
226 reflog.write()?;
227 Ok(ReflogDropOutput {
228 refname,
229 index,
230 dropped: true,
231 })
232 })
233 .await
234 }
235}
236
237#[async_trait]
238impl Operation for ReflogDrop {
239 fn kind(&self) -> &str {
240 "git"
241 }
242 async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
243 to_value(&self.run(ctx).await?)
244 }
245 fn input(&self) -> Option<Value> {
246 Some(
247 serde_json::json!({ "repo_path": self.repo_path, "refname": self.refname, "index": self.index }),
248 )
249 }
250}
251
252impl TypedOperation for ReflogDrop {
253 type Output = ReflogDropOutput;
254}
255
256#[cfg(test)]
257mod tests {
258 use ironflow_core::operation::Operation;
259
260 use super::*;
261 use crate::test_helpers::{ctx, init_repo};
262
263 #[tokio::test]
264 async fn read_reflog_has_entries() {
265 let tmp = tempfile::tempdir().unwrap();
266 init_repo(tmp.path());
267 let result = ReflogRead::new(tmp.path(), "HEAD", 50)
268 .run(&ctx())
269 .await
270 .unwrap();
271 assert_eq!(result.refname, "HEAD");
272 assert!(!result.entries.is_empty());
273 }
274
275 #[tokio::test]
276 async fn append_and_read() {
277 let tmp = tempfile::tempdir().unwrap();
278 let oid = init_repo(tmp.path()).to_string();
279 let before = ReflogRead::new(tmp.path(), "HEAD", 50)
280 .run(&ctx())
281 .await
282 .unwrap();
283 ReflogAppend::new(tmp.path(), "HEAD", &oid, "test entry", "Bot", "bot@t.com")
284 .run(&ctx())
285 .await
286 .unwrap();
287 let after = ReflogRead::new(tmp.path(), "HEAD", 50)
288 .run(&ctx())
289 .await
290 .unwrap();
291 assert_eq!(after.entries.len(), before.entries.len() + 1);
292 assert!(after.entries.iter().any(|e| e.message == "test entry"));
293 }
294
295 #[tokio::test]
296 async fn drop_entry() {
297 let tmp = tempfile::tempdir().unwrap();
298 let oid = init_repo(tmp.path()).to_string();
299 ReflogAppend::new(tmp.path(), "HEAD", &oid, "to-drop", "Bot", "bot@t.com")
300 .run(&ctx())
301 .await
302 .unwrap();
303 let result = ReflogDrop::new(tmp.path(), "HEAD", 0)
304 .run(&ctx())
305 .await
306 .unwrap();
307 assert!(result.dropped);
308 }
309
310 #[tokio::test]
311 async fn execute_serializes_correctly() {
312 let tmp = tempfile::tempdir().unwrap();
313 init_repo(tmp.path());
314 let value = ReflogRead::new(tmp.path(), "HEAD", 50)
315 .execute(&ctx())
316 .await
317 .unwrap();
318 assert_eq!(value["refname"], "HEAD");
319 assert!(value["entries"].is_array());
320 }
321}