gnostr_asyncgit/sync/
stash.rs

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