gnostr_asyncgit/sync/
stash.rs

1use git2::{build::CheckoutBuilder, Oid, Repository, StashApplyOptions, StashFlags};
2use scopetime::scope_time;
3
4use super::{CommitId, RepoPath};
5use crate::{
6    error::{Error, Result},
7    sync::repository::repo,
8};
9
10///
11pub fn get_stashes(repo_path: &RepoPath) -> Result<Vec<CommitId>> {
12    scope_time!("get_stashes");
13
14    let mut repo = repo(repo_path)?;
15    let mut list = Vec::new();
16    repo.stash_foreach(|_index, _msg, id| {
17        list.push((*id).into());
18        true
19    })?;
20
21    Ok(list)
22}
23
24///
25pub fn stash_drop(repo_path: &RepoPath, stash_id: CommitId) -> Result<()> {
26    scope_time!("stash_drop");
27
28    let mut repo = repo(repo_path)?;
29
30    let index = get_stash_index(&mut repo, stash_id.into())?;
31
32    repo.stash_drop(index)?;
33
34    Ok(())
35}
36
37///
38pub fn stash_pop(repo_path: &RepoPath, stash_id: CommitId) -> Result<()> {
39    scope_time!("stash_pop");
40
41    let mut repo = repo(repo_path)?;
42
43    let index = get_stash_index(&mut repo, stash_id.into())?;
44
45    repo.stash_pop(index, None)?;
46
47    Ok(())
48}
49
50///
51pub fn stash_apply(repo_path: &RepoPath, stash_id: CommitId, allow_conflicts: bool) -> Result<()> {
52    scope_time!("stash_apply");
53
54    let mut repo = repo(repo_path)?;
55
56    let index = get_stash_index(&mut repo, stash_id.get_oid())?;
57
58    let mut checkout = CheckoutBuilder::new();
59    checkout.allow_conflicts(allow_conflicts);
60
61    let mut opt = StashApplyOptions::default();
62    opt.checkout_options(checkout);
63    repo.stash_apply(index, Some(&mut opt))?;
64
65    Ok(())
66}
67
68fn get_stash_index(repo: &mut Repository, stash_id: Oid) -> Result<usize> {
69    let mut idx = None;
70
71    repo.stash_foreach(|index, _msg, id| {
72        if *id == stash_id {
73            idx = Some(index);
74            false
75        } else {
76            true
77        }
78    })?;
79
80    idx.ok_or_else(|| Error::Generic("stash commit not found".to_string()))
81}
82
83///
84pub fn stash_save(
85    repo_path: &RepoPath,
86    message: Option<&str>,
87    include_untracked: bool,
88    keep_index: bool,
89) -> Result<CommitId> {
90    scope_time!("stash_save");
91
92    let mut repo = repo(repo_path)?;
93
94    let sig = repo.signature()?;
95
96    let mut options = StashFlags::DEFAULT;
97
98    if include_untracked {
99        options.insert(StashFlags::INCLUDE_UNTRACKED);
100    }
101    if keep_index {
102        options.insert(StashFlags::KEEP_INDEX);
103    }
104
105    let id = repo.stash_save2(&sig, message, Some(options))?;
106
107    Ok(CommitId::new(id))
108}
109
110#[cfg(test)]
111mod tests {
112    use std::{fs::File, io::Write, path::Path};
113
114    use super::*;
115    use crate::sync::{
116        commit, get_commit_files, get_commits_info, stage_add_file,
117        tests::{debug_cmd_print, get_statuses, repo_init, write_commit_file},
118        utils::{repo_read_file, repo_write_file},
119    };
120
121    #[test]
122    fn test_smoke() {
123        let (_td, repo) = repo_init().unwrap();
124        let root = repo.path().parent().unwrap();
125        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
126
127        assert_eq!(stash_save(repo_path, None, true, false).is_ok(), false);
128
129        assert_eq!(get_stashes(repo_path).unwrap().is_empty(), true);
130    }
131
132    #[test]
133    fn test_stashing() -> Result<()> {
134        let (_td, repo) = repo_init().unwrap();
135        let root = repo.path().parent().unwrap();
136        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
137
138        File::create(root.join("foo.txt"))?.write_all(b"test\nfoo")?;
139
140        assert_eq!(get_statuses(repo_path), (1, 0));
141
142        stash_save(repo_path, None, true, false)?;
143
144        assert_eq!(get_statuses(repo_path), (0, 0));
145
146        Ok(())
147    }
148
149    #[test]
150    fn test_stashes() -> Result<()> {
151        let (_td, repo) = repo_init().unwrap();
152        let root = repo.path().parent().unwrap();
153        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
154
155        File::create(root.join("foo.txt"))?.write_all(b"test\nfoo")?;
156
157        stash_save(repo_path, Some("foo"), true, false)?;
158
159        let res = get_stashes(repo_path)?;
160
161        assert_eq!(res.len(), 1);
162
163        let infos = get_commits_info(repo_path, &[res[0]], 100).unwrap();
164
165        assert_eq!(infos[0].message, "On master: foo");
166
167        Ok(())
168    }
169
170    #[test]
171    fn test_stash_nothing_untracked() -> Result<()> {
172        let (_td, repo) = repo_init().unwrap();
173        let root = repo.path().parent().unwrap();
174        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
175
176        File::create(root.join("foo.txt"))?.write_all(b"test\nfoo")?;
177
178        assert!(stash_save(repo_path, Some("foo"), false, false).is_err());
179
180        Ok(())
181    }
182
183    #[test]
184    fn test_stash_without_second_parent() -> Result<()> {
185        let file_path1 = Path::new("file1.txt");
186        let (_td, repo) = repo_init()?;
187        let root = repo.path().parent().unwrap();
188        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
189
190        File::create(root.join(file_path1))?.write_all(b"test")?;
191        stage_add_file(repo_path, file_path1)?;
192        commit(repo_path, "c1")?;
193
194        File::create(root.join(file_path1))?.write_all(b"modified")?;
195
196        //NOTE: apparently `libgit2` works differently to git stash
197        // in always creating the third parent for untracked files
198        // while the cli skips that step when no new files exist
199        debug_cmd_print(repo_path, "git stash");
200
201        let stash = get_stashes(repo_path)?[0];
202
203        let diff = get_commit_files(repo_path, stash, None)?;
204
205        assert_eq!(diff.len(), 1);
206
207        Ok(())
208    }
209
210    #[test]
211    fn test_stash_apply_conflict() {
212        let (_td, repo) = repo_init().unwrap();
213        let root = repo.path().parent().unwrap();
214        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
215
216        repo_write_file(&repo, "test.txt", "test").unwrap();
217
218        let id = stash_save(repo_path, Some("foo"), true, false).unwrap();
219
220        repo_write_file(&repo, "test.txt", "foo").unwrap();
221
222        let res = stash_apply(repo_path, id, false);
223
224        assert!(res.is_err());
225    }
226
227    #[test]
228    fn test_stash_apply_conflict2() {
229        let (_td, repo) = repo_init().unwrap();
230        let root = repo.path().parent().unwrap();
231        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
232
233        write_commit_file(&repo, "test.txt", "test", "c1");
234
235        repo_write_file(&repo, "test.txt", "test2").unwrap();
236
237        let id = stash_save(repo_path, Some("foo"), true, false).unwrap();
238
239        repo_write_file(&repo, "test.txt", "test3").unwrap();
240
241        let res = stash_apply(repo_path, id, false);
242
243        assert!(res.is_err());
244    }
245
246    #[test]
247    fn test_stash_apply_creating_conflict() {
248        let (_td, repo) = repo_init().unwrap();
249        let root = repo.path().parent().unwrap();
250        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
251
252        write_commit_file(&repo, "test.txt", "test", "c1");
253
254        repo_write_file(&repo, "test.txt", "test2").unwrap();
255
256        let id = stash_save(repo_path, Some("foo"), true, false).unwrap();
257
258        repo_write_file(&repo, "test.txt", "test3").unwrap();
259
260        let res = stash_apply(repo_path, id, false);
261
262        assert!(res.is_err());
263
264        let res = stash_apply(repo_path, id, true);
265
266        assert!(res.is_ok());
267    }
268
269    #[test]
270    fn test_stash_pop_no_conflict() {
271        let (_td, repo) = repo_init().unwrap();
272        let root = repo.path().parent().unwrap();
273        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
274
275        write_commit_file(&repo, "test.txt", "test", "c1");
276
277        repo_write_file(&repo, "test.txt", "test2").unwrap();
278
279        let id = stash_save(repo_path, Some("foo"), true, false).unwrap();
280
281        let res = stash_pop(repo_path, id);
282
283        assert!(res.is_ok());
284        assert_eq!(repo_read_file(&repo, "test.txt").unwrap(), "test2");
285    }
286
287    #[test]
288    fn test_stash_pop_conflict() {
289        let (_td, repo) = repo_init().unwrap();
290        let root = repo.path().parent().unwrap();
291        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
292
293        repo_write_file(&repo, "test.txt", "test").unwrap();
294
295        let id = stash_save(repo_path, Some("foo"), true, false).unwrap();
296
297        repo_write_file(&repo, "test.txt", "test2").unwrap();
298
299        let res = stash_pop(repo_path, id);
300
301        assert!(res.is_err());
302        assert_eq!(repo_read_file(&repo, "test.txt").unwrap(), "test2");
303    }
304
305    #[test]
306    fn test_stash_pop_conflict_after_commit() {
307        let (_td, repo) = repo_init().unwrap();
308        let root = repo.path().parent().unwrap();
309        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
310
311        write_commit_file(&repo, "test.txt", "test", "c1");
312
313        repo_write_file(&repo, "test.txt", "test2").unwrap();
314
315        let id = stash_save(repo_path, Some("foo"), true, false).unwrap();
316
317        repo_write_file(&repo, "test.txt", "test3").unwrap();
318
319        let res = stash_pop(repo_path, id);
320
321        assert!(res.is_err());
322        assert_eq!(repo_read_file(&repo, "test.txt").unwrap(), "test3");
323    }
324}