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
10pub 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
24pub 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
37pub 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
50pub 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
83pub 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 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}