gnostr_asyncgit/sync/
utils.rs

1//! sync git api (various methods)
2
3use std::{
4	fs::File,
5	io::Write,
6	path::{Path, PathBuf},
7};
8
9use git2::{IndexAddOption, Repository, RepositoryOpenFlags};
10use scopetime::scope_time;
11
12use super::{
13	CommitId, RepoPath, ShowUntrackedFilesConfig, repository::repo,
14};
15use crate::{
16	error::{Error, Result},
17	sync::config::untracked_files_config_repo,
18};
19
20///
21#[derive(PartialEq, Eq, Debug, Clone)]
22pub struct Head {
23	///
24	pub name: String,
25	///
26	pub id: CommitId,
27}
28
29///
30pub fn repo_open_error(repo_path: &RepoPath) -> Option<String> {
31	Repository::open_ext(
32		repo_path.gitpath(),
33		RepositoryOpenFlags::empty(),
34		Vec::<&Path>::new(),
35	)
36	.map_or_else(|e| Some(e.to_string()), |_| None)
37}
38
39///
40pub(crate) fn work_dir(repo: &Repository) -> Result<&Path> {
41	repo.workdir().ok_or(Error::NoWorkDir)
42}
43
44/// path to .git folder
45pub fn repo_dir(repo_path: &RepoPath) -> Result<PathBuf> {
46	let repo = repo(repo_path)?;
47	Ok(repo.path().to_owned())
48}
49
50///
51pub fn repo_work_dir(repo_path: &RepoPath) -> Result<String> {
52	let repo = repo(repo_path)?;
53	work_dir(&repo)?.to_str().map_or_else(
54		|| Err(Error::Generic("invalid workdir".to_string())),
55		|workdir| Ok(workdir.to_string()),
56	)
57}
58
59///
60pub fn get_head(repo_path: &RepoPath) -> Result<CommitId> {
61	let repo = repo(repo_path)?;
62	get_head_repo(&repo)
63}
64
65///
66pub fn get_head_tuple(repo_path: &RepoPath) -> Result<Head> {
67	let repo = repo(repo_path)?;
68	let id = get_head_repo(&repo)?;
69	let name = get_head_refname(&repo)?;
70
71	Ok(Head { name, id })
72}
73
74///
75pub fn get_head_refname(repo: &Repository) -> Result<String> {
76	let head = repo.head()?;
77	let ref_name = bytes2string(head.name_bytes())?;
78
79	Ok(ref_name)
80}
81
82///
83pub fn get_head_repo(repo: &Repository) -> Result<CommitId> {
84	scope_time!("get_head_repo");
85
86	let head = repo.head()?.target();
87
88	head.map_or(Err(Error::NoHead), |head_id| Ok(head_id.into()))
89}
90
91/// add a file diff from workingdir to stage (will not add removed
92/// files see `stage_addremoved`)
93pub fn stage_add_file(
94	repo_path: &RepoPath,
95	path: &Path,
96) -> Result<()> {
97	scope_time!("stage_add_file");
98
99	let repo = repo(repo_path)?;
100
101	let mut index = repo.index()?;
102
103	index.add_path(path)?;
104	index.write()?;
105
106	Ok(())
107}
108
109/// like `stage_add_file` but uses a pattern to match/glob multiple
110/// files/folders
111pub fn stage_add_all(
112	repo_path: &RepoPath,
113	pattern: &str,
114	stage_untracked: Option<ShowUntrackedFilesConfig>,
115) -> Result<()> {
116	scope_time!("stage_add_all");
117
118	let repo = repo(repo_path)?;
119
120	let mut index = repo.index()?;
121
122	let stage_untracked = if let Some(config) = stage_untracked {
123		config
124	} else {
125		untracked_files_config_repo(&repo)?
126	};
127
128	if stage_untracked.include_untracked() {
129		index.add_all(
130			vec![pattern],
131			IndexAddOption::DEFAULT,
132			None,
133		)?;
134	} else {
135		index.update_all(vec![pattern], None)?;
136	}
137
138	index.write()?;
139
140	Ok(())
141}
142
143/// Undo last commit in repo
144pub fn undo_last_commit(repo_path: &RepoPath) -> Result<()> {
145	let repo = repo(repo_path)?;
146	let previous_commit = repo.revparse_single("HEAD~")?;
147
148	Repository::reset(
149		&repo,
150		&previous_commit,
151		git2::ResetType::Soft,
152		None,
153	)?;
154
155	Ok(())
156}
157
158/// stage a removed file
159pub fn stage_addremoved(
160	repo_path: &RepoPath,
161	path: &Path,
162) -> Result<()> {
163	scope_time!("stage_addremoved");
164
165	let repo = repo(repo_path)?;
166
167	let mut index = repo.index()?;
168
169	index.remove_path(path)?;
170	index.write()?;
171
172	Ok(())
173}
174
175pub(crate) fn bytes2string(bytes: &[u8]) -> Result<String> {
176	Ok(String::from_utf8(bytes.to_vec())?)
177}
178
179/// write a file in repo
180pub(crate) fn repo_write_file(
181	repo: &Repository,
182	file: &str,
183	content: &str,
184) -> Result<()> {
185	let dir = work_dir(repo)?.join(file);
186	let file_path = dir.to_str().ok_or_else(|| {
187		Error::Generic(String::from("invalid file path"))
188	})?;
189	let mut file = File::create(file_path)?;
190	file.write_all(content.as_bytes())?;
191	Ok(())
192}
193
194///
195pub fn read_file(path: &Path) -> Result<String> {
196	use std::io::Read;
197
198	let mut file = File::open(path)?;
199	let mut buffer = Vec::new();
200	file.read_to_end(&mut buffer)?;
201
202	Ok(String::from_utf8(buffer)?)
203}
204
205#[cfg(test)]
206pub(crate) fn repo_read_file(
207	repo: &Repository,
208	file: &str,
209) -> Result<String> {
210	use std::io::Read;
211
212	let dir = work_dir(repo)?.join(file);
213	let file_path = dir.to_str().ok_or_else(|| {
214		Error::Generic(String::from("invalid file path"))
215	})?;
216
217	let mut file = File::open(file_path)?;
218	let mut buffer = Vec::new();
219	file.read_to_end(&mut buffer)?;
220
221	Ok(String::from_utf8(buffer)?)
222}
223
224#[cfg(test)]
225mod tests {
226	use std::{
227		fs::{self, File, remove_file},
228		io::Write,
229		path::Path,
230	};
231
232	use super::*;
233	use crate::sync::{
234		commit,
235		diff::get_diff,
236		status::{StatusType, get_status},
237		tests::{
238			debug_cmd_print, get_statuses, repo_init,
239			repo_init_empty, write_commit_file,
240		},
241	};
242
243	#[test]
244	fn test_stage_add_smoke() {
245		let file_path = Path::new("foo");
246		let (_td, repo) = repo_init_empty().unwrap();
247		let root = repo.path().parent().unwrap();
248		let repo_path = root.as_os_str().to_str().unwrap();
249
250		assert_eq!(
251			stage_add_file(&repo_path.into(), file_path).is_ok(),
252			false
253		);
254	}
255
256	#[test]
257	fn test_staging_one_file() {
258		let file_path = Path::new("file1.txt");
259		let (_td, repo) = repo_init().unwrap();
260		let root = repo.path().parent().unwrap();
261		let repo_path: &RepoPath =
262			&root.as_os_str().to_str().unwrap().into();
263
264		File::create(root.join(file_path))
265			.unwrap()
266			.write_all(b"test file1 content")
267			.unwrap();
268
269		File::create(root.join(Path::new("file2.txt")))
270			.unwrap()
271			.write_all(b"test file2 content")
272			.unwrap();
273
274		assert_eq!(get_statuses(repo_path), (2, 0));
275
276		stage_add_file(repo_path, file_path).unwrap();
277
278		assert_eq!(get_statuses(repo_path), (1, 1));
279	}
280
281	#[test]
282	fn test_staging_folder() -> Result<()> {
283		let (_td, repo) = repo_init().unwrap();
284		let root = repo.path().parent().unwrap();
285		let repo_path: &RepoPath =
286			&root.as_os_str().to_str().unwrap().into();
287
288		let status_count = |s: StatusType| -> usize {
289			get_status(repo_path, s, None).unwrap().len()
290		};
291
292		fs::create_dir_all(root.join("a/d"))?;
293		File::create(root.join(Path::new("a/d/f1.txt")))?
294			.write_all(b"foo")?;
295		File::create(root.join(Path::new("a/d/f2.txt")))?
296			.write_all(b"foo")?;
297		File::create(root.join(Path::new("a/f3.txt")))?
298			.write_all(b"foo")?;
299
300		assert_eq!(status_count(StatusType::WorkingDir), 3);
301
302		stage_add_all(repo_path, "a/d", None).unwrap();
303
304		assert_eq!(status_count(StatusType::WorkingDir), 1);
305		assert_eq!(status_count(StatusType::Stage), 2);
306
307		Ok(())
308	}
309
310	#[test]
311	fn test_undo_commit_empty_repo() {
312		let (_td, repo) = repo_init().unwrap();
313		let root = repo.path().parent().unwrap();
314		let repo_path: &RepoPath =
315			&root.as_os_str().to_str().unwrap().into();
316
317		// expect to fail
318		assert!(undo_last_commit(repo_path).is_err());
319	}
320
321	#[test]
322	fn test_undo_commit() {
323		let (_td, repo) = repo_init().unwrap();
324		let root = repo.path().parent().unwrap();
325		let repo_path: &RepoPath =
326			&root.as_os_str().to_str().unwrap().into();
327
328		// write commit file test.txt
329		let c1 =
330			write_commit_file(&repo, "test.txt", "content1", "c1");
331		let _c2 =
332			write_commit_file(&repo, "test.txt", "content2", "c2");
333		assert!(undo_last_commit(repo_path).is_ok());
334
335		// Make sure that HEAD points to c1
336		assert_eq!(c1, get_head_repo(&repo).unwrap());
337
338		// Make sure that now we have 1 file staged
339		assert_eq!(get_statuses(repo_path), (0, 1));
340
341		// And that file is test.txt
342		let diff =
343			get_diff(repo_path, "test.txt", true, None).unwrap();
344		assert_eq!(&*diff.hunks[0].lines[0].content, "@@ -1 +1 @@");
345	}
346
347	#[test]
348	fn test_not_staging_untracked_folder() -> Result<()> {
349		let (_td, repo) = repo_init().unwrap();
350		let root = repo.path().parent().unwrap();
351		let repo_path: &RepoPath =
352			&root.as_os_str().to_str().unwrap().into();
353
354		fs::create_dir_all(root.join("a/d"))?;
355		File::create(root.join(Path::new("a/d/f1.txt")))?
356			.write_all(b"foo")?;
357		File::create(root.join(Path::new("a/d/f2.txt")))?
358			.write_all(b"foo")?;
359		File::create(root.join(Path::new("f3.txt")))?
360			.write_all(b"foo")?;
361
362		assert_eq!(get_statuses(repo_path), (3, 0));
363
364		repo.config()?.set_str("status.showUntrackedFiles", "no")?;
365
366		assert_eq!(get_statuses(repo_path), (0, 0));
367
368		stage_add_all(repo_path, "*", None).unwrap();
369
370		assert_eq!(get_statuses(repo_path), (0, 0));
371
372		Ok(())
373	}
374
375	#[test]
376	fn test_staging_deleted_file() {
377		let file_path = Path::new("file1.txt");
378		let (_td, repo) = repo_init().unwrap();
379		let root = repo.path().parent().unwrap();
380		let repo_path: &RepoPath =
381			&root.as_os_str().to_str().unwrap().into();
382
383		let status_count = |s: StatusType| -> usize {
384			get_status(repo_path, s, None).unwrap().len()
385		};
386
387		let full_path = &root.join(file_path);
388
389		File::create(full_path)
390			.unwrap()
391			.write_all(b"test file1 content")
392			.unwrap();
393
394		stage_add_file(repo_path, file_path).unwrap();
395
396		commit(repo_path, "commit msg").unwrap();
397
398		// delete the file now
399		assert_eq!(remove_file(full_path).is_ok(), true);
400
401		// deleted file in diff now
402		assert_eq!(status_count(StatusType::WorkingDir), 1);
403
404		stage_addremoved(repo_path, file_path).unwrap();
405
406		assert_eq!(status_count(StatusType::WorkingDir), 0);
407		assert_eq!(status_count(StatusType::Stage), 1);
408	}
409
410	// see https://github.com/extrawurst/gitui/issues/108
411	#[test]
412	fn test_staging_sub_git_folder() -> Result<()> {
413		let (_td, repo) = repo_init().unwrap();
414		let root = repo.path().parent().unwrap();
415		let repo_path: &RepoPath =
416			&root.as_os_str().to_str().unwrap().into();
417
418		let status_count = |s: StatusType| -> usize {
419			get_status(repo_path, s, None).unwrap().len()
420		};
421
422		let sub = &root.join("sub");
423
424		fs::create_dir_all(sub)?;
425
426		debug_cmd_print(
427			&sub.to_str().unwrap().into(),
428			"git init subgit",
429		);
430
431		File::create(sub.join("subgit/foo.txt"))
432			.unwrap()
433			.write_all(b"content")
434			.unwrap();
435
436		assert_eq!(status_count(StatusType::WorkingDir), 1);
437
438		//expect to fail
439		assert!(stage_add_all(repo_path, "sub", None).is_err());
440
441		Ok(())
442	}
443
444	#[test]
445	fn test_head_empty() -> Result<()> {
446		let (_td, repo) = repo_init_empty()?;
447		let root = repo.path().parent().unwrap();
448		let repo_path: &RepoPath =
449			&root.as_os_str().to_str().unwrap().into();
450
451		assert_eq!(get_head(repo_path).is_ok(), false);
452
453		Ok(())
454	}
455
456	#[test]
457	fn test_head() -> Result<()> {
458		let (_td, repo) = repo_init()?;
459		let root = repo.path().parent().unwrap();
460		let repo_path: &RepoPath =
461			&root.as_os_str().to_str().unwrap().into();
462
463		assert_eq!(get_head(repo_path).is_ok(), true);
464
465		Ok(())
466	}
467}