Skip to main content

ironflow_ops_git/
reflog.rs

1//! Reflog operations.
2
3use 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/// A single reflog entry.
15#[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
42/// Read the reflog for a reference.
43///
44/// # Examples
45///
46/// ```no_run
47/// use ironflow_ops_git::reflog::ReflogRead;
48/// use ironflow_core::operation::Operation;
49///
50/// let op = ReflogRead::new("/path/to/repo", "HEAD", 50);
51/// assert_eq!(op.kind(), "git");
52/// ```
53pub struct ReflogRead {
54    repo_path: PathBuf,
55    refname: String,
56    limit: usize,
57}
58
59impl ReflogRead {
60    /// Create a new reflog-read operation.
61    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    /// Execute and return a typed result.
70    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
109/// Append an entry to a reflog.
110///
111/// # Examples
112///
113/// ```no_run
114/// use ironflow_ops_git::reflog::ReflogAppend;
115/// use ironflow_core::operation::Operation;
116///
117/// let op = ReflogAppend::new("/path/to/repo", "HEAD", "abc123", "message", "Author", "author@example.com");
118/// assert_eq!(op.kind(), "git");
119/// ```
120pub 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    /// Create a new reflog-append operation.
131    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    /// Execute and return a typed result.
150    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
190/// Drop a reflog entry by index.
191///
192/// # Examples
193///
194/// ```no_run
195/// use ironflow_ops_git::reflog::ReflogDrop;
196/// use ironflow_core::operation::Operation;
197///
198/// let op = ReflogDrop::new("/path/to/repo", "HEAD", 0);
199/// assert_eq!(op.kind(), "git");
200/// ```
201pub struct ReflogDrop {
202    repo_path: PathBuf,
203    refname: String,
204    index: usize,
205}
206
207impl ReflogDrop {
208    /// Create a new reflog-drop operation.
209    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    /// Execute and return a typed result.
218    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}