Skip to main content

gitkraft_core/features/stash/
ops.rs

1//! Stash operations — list, save, pop, and drop stash entries.
2
3use anyhow::{Context, Result};
4use git2::Repository;
5use tracing::debug;
6
7use super::types::StashEntry;
8
9/// List all stash entries in the repository.
10///
11/// Note: `stash_foreach` requires `&mut Repository` in git2, but only reads
12/// stash state. We accept `&mut Repository` here to be safe and correct.
13pub fn list_stashes(repo: &mut Repository) -> Result<Vec<StashEntry>> {
14    let mut stashes = Vec::new();
15
16    repo.stash_foreach(|index, message, oid| {
17        stashes.push(StashEntry {
18            index,
19            message: message.to_string(),
20            oid: oid.to_string(),
21        });
22        true // continue iterating
23    })
24    .context("Failed to iterate stashes")?;
25
26    debug!("Found {} stash entries", stashes.len());
27    Ok(stashes)
28}
29
30/// Save the current working directory and index state as a new stash entry.
31///
32/// If `message` is `None`, a default "WIP" message is used.
33/// Returns the newly created `StashEntry`.
34pub fn stash_save(repo: &mut Repository, message: Option<&str>) -> Result<StashEntry> {
35    let signature = repo.signature().context(
36        "Failed to determine default signature for stash — set user.name and user.email",
37    )?;
38
39    let msg = message.unwrap_or("WIP");
40
41    let oid = repo
42        .stash_save(&signature, msg, None)
43        .context("Failed to save stash (are there any changes to stash?)")?;
44
45    debug!("Stash saved: {} — {}", oid, msg);
46
47    Ok(StashEntry {
48        index: 0, // newly saved stash is always at index 0
49        message: msg.to_string(),
50        oid: oid.to_string(),
51    })
52}
53
54/// Pop (apply + drop) a stash entry by its zero-based index.
55pub fn stash_pop(repo: &mut Repository, index: usize) -> Result<()> {
56    repo.stash_pop(index, None)
57        .with_context(|| format!("Failed to pop stash at index {index}"))?;
58
59    debug!("Stash at index {} popped", index);
60    Ok(())
61}
62
63/// Drop (delete) a stash entry by its zero-based index without applying it.
64pub fn stash_drop(repo: &mut Repository, index: usize) -> Result<()> {
65    repo.stash_drop(index)
66        .with_context(|| format!("Failed to drop stash at index {index}"))?;
67
68    debug!("Stash at index {} dropped", index);
69    Ok(())
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use tempfile::TempDir;
76
77    fn setup_repo_with_commit() -> (TempDir, Repository) {
78        // same helper as branches
79        let dir = TempDir::new().unwrap();
80        let repo = Repository::init(dir.path()).unwrap();
81        let mut config = repo.config().unwrap();
82        config.set_str("user.name", "Test").unwrap();
83        config.set_str("user.email", "test@test.com").unwrap();
84        std::fs::write(dir.path().join("file.txt"), "hello\n").unwrap();
85        let mut index = repo.index().unwrap();
86        index.add_path(std::path::Path::new("file.txt")).unwrap();
87        index.write().unwrap();
88        let tree_oid = index.write_tree().unwrap();
89        {
90            let tree = repo.find_tree(tree_oid).unwrap();
91            let sig = repo.signature().unwrap();
92            repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
93                .unwrap();
94        }
95        (dir, repo)
96    }
97
98    #[test]
99    fn list_stashes_empty() {
100        let (_dir, mut repo) = setup_repo_with_commit();
101        let stashes = list_stashes(&mut repo).unwrap();
102        assert!(stashes.is_empty());
103    }
104
105    #[test]
106    fn stash_save_and_list() {
107        let (dir, mut repo) = setup_repo_with_commit();
108        // Make a change
109        std::fs::write(dir.path().join("file.txt"), "changed\n").unwrap();
110        let entry = stash_save(&mut repo, Some("test stash")).unwrap();
111        assert_eq!(entry.index, 0);
112        assert!(entry.message.contains("test stash"));
113
114        let stashes = list_stashes(&mut repo).unwrap();
115        assert_eq!(stashes.len(), 1);
116    }
117
118    #[test]
119    fn stash_pop_restores_changes() {
120        let (dir, mut repo) = setup_repo_with_commit();
121        std::fs::write(dir.path().join("file.txt"), "changed\n").unwrap();
122        stash_save(&mut repo, Some("test")).unwrap();
123
124        // File should be restored to committed state after stash
125        let content = std::fs::read_to_string(dir.path().join("file.txt")).unwrap();
126        assert_eq!(content, "hello\n");
127
128        stash_pop(&mut repo, 0).unwrap();
129        let content = std::fs::read_to_string(dir.path().join("file.txt")).unwrap();
130        assert_eq!(content, "changed\n");
131    }
132}