1use 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#[derive(PartialEq, Eq, Debug, Clone)]
22pub struct Head {
23 pub name: String,
25 pub id: CommitId,
27}
28
29pub 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
39pub(crate) fn work_dir(repo: &Repository) -> Result<&Path> {
41 repo.workdir().ok_or(Error::NoWorkDir)
42}
43
44pub fn repo_dir(repo_path: &RepoPath) -> Result<PathBuf> {
46 let repo = repo(repo_path)?;
47 Ok(repo.path().to_owned())
48}
49
50pub 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
59pub fn get_head(repo_path: &RepoPath) -> Result<CommitId> {
61 let repo = repo(repo_path)?;
62 get_head_repo(&repo)
63}
64
65pub 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
74pub 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
82pub 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
91pub 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
109pub 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
143pub 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
158pub 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
179pub(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
194pub 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 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 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 assert_eq!(c1, get_head_repo(&repo).unwrap());
337
338 assert_eq!(get_statuses(repo_path), (0, 1));
340
341 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 assert_eq!(remove_file(full_path).is_ok(), true);
400
401 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 #[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 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}