1pub use posix_errors::{PosixError, EACCES, EINVAL, ENOENT};
22use std::collections::HashMap;
23use std::ffi::OsStr;
24use std::path::{Path, PathBuf};
25use std::process::Command;
26use std::process::Output;
27
28mod bare_repo;
29pub use crate::bare_repo::*;
30
31pub mod x;
33
34macro_rules! cmd {
35 ($args:expr) => {
36 Command::new("git").args($args).output()
37 };
38 ($name:expr, $args:expr) => {
39 Command::new("git").arg($name).args($args).output()
40 };
41}
42
43#[inline]
49pub fn ls_remote(args: &[&str]) -> Result<Output, PosixError> {
50 let result = cmd!("ls-remote", args);
51
52 if let Ok(value) = result {
53 return Ok(value);
54 }
55
56 Err(PosixError::from(result.unwrap_err()))
57}
58
59#[inline]
65pub fn tags_from_remote(url: &str) -> Result<Vec<String>, PosixError> {
66 let mut vec = Vec::new();
67 let output = ls_remote(&["--refs", "--tags", url])?;
68 if output.status.success() {
69 let tmp = String::from_utf8(output.stdout).expect("Expected UTF-8");
70 for s in tmp.lines() {
71 let mut split = s.splitn(3, '/');
72 split.next();
73 split.next();
74 let split_result = split.next();
75 if let Some(value) = split_result {
76 vec.push(String::from(value));
77 }
78 }
79 Ok(vec)
80 } else {
81 Err(PosixError::from(output))
82 }
83}
84
85#[allow(missing_docs)]
87#[derive(thiserror::Error, Debug)]
88pub enum ConfigReadError {
89 #[error("Invalid section key in config {0}")]
90 InvalidSectionOrKey(String),
91 #[error("Invalid config file {0}")]
92 InvalidConfigFile(String),
93 #[error("{0}")]
94 Failure(String, i32),
95}
96
97#[allow(missing_docs)]
99#[derive(thiserror::Error, Debug)]
100pub enum ConfigSetError {
101 #[error("{0}")]
102 InvalidSectionOrKey(String),
103 #[error("{0}")]
104 InvalidConfigFile(String),
105 #[error("{0}")]
106 WriteFailed(String),
107 #[error("{0}")]
108 Failure(String, i32),
109}
110
111#[inline]
119pub fn config_file_set(file: &Path, key: &str, value: &str) -> Result<(), ConfigSetError> {
120 let args = &["--file", file.to_str().expect("UTF-8 encoding"), key, value];
121 let mut cmd = Command::new("git");
122 cmd.arg("config").args(args);
123 let out = cmd.output().expect("Failed to execute git-config(1)");
124 if out.status.success() {
125 Ok(())
126 } else {
127 let msg = String::from_utf8(out.stdout).expect("UTF-8 encoding");
128 match out.status.code().unwrap_or(1) {
129 1 => Err(ConfigSetError::InvalidSectionOrKey(msg)),
130 3 => Err(ConfigSetError::InvalidConfigFile(msg)),
131 4 => Err(ConfigSetError::WriteFailed(msg)),
132 code => Err(ConfigSetError::Failure(msg, code)),
133 }
134 }
135}
136
137#[inline]
151pub fn resolve_head(remote: &str) -> Result<String, PosixError> {
152 let proc =
153 cmd!("ls-remote", vec!["--symref", remote, "HEAD"]).expect("Failed to execute git command");
154 if proc.status.success() {
155 let stdout = String::from_utf8(proc.stdout).expect("UTF-8 encoding");
156 let mut lines = stdout.lines();
157 let first_line = lines.next().expect("Failed to parse HEAD from remote");
158 let mut split = first_line
159 .split('\t')
160 .next()
161 .expect("Failed to parse HEAD from remote")
162 .splitn(3, '/');
163 split.next();
164 split.next();
165 return Ok(split
166 .next()
167 .expect("Failed to parse default branch")
168 .to_owned());
169 }
170
171 Err(PosixError::from(proc))
172}
173
174enum RemoteDir {
175 Fetch,
176 Push,
177}
178
179struct RemoteLine {
180 name: String,
181 url: String,
182 dir: RemoteDir,
183}
184
185#[allow(missing_docs)]
187#[derive(Clone, Debug, Eq, PartialEq)]
188pub struct Remote {
189 pub name: String,
190 pub push: Option<String>,
191 pub fetch: Option<String>,
192}
193
194fn cwd() -> Result<PathBuf, RepoError> {
195 if let Ok(result) = std::env::current_dir() {
196 Ok(result)
197 } else {
198 Err(RepoError::FailAccessCwd)
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct AbsoluteDirPath(PathBuf);
205impl AsRef<OsStr> for AbsoluteDirPath {
206 #[inline]
207 fn as_ref(&self) -> &OsStr {
208 self.0.as_os_str()
209 }
210}
211
212impl TryFrom<&Path> for AbsoluteDirPath {
213 type Error = RepoError;
214
215 #[inline]
216 fn try_from(value: &Path) -> Result<Self, Self::Error> {
217 let path_buf;
218 if value.is_absolute() {
219 path_buf = value.to_path_buf();
220 } else if let Ok(p) = value.canonicalize() {
221 path_buf = p;
222 } else {
223 return Err(RepoError::AbsolutionError(value.to_path_buf()));
224 }
225
226 Ok(Self(path_buf))
227 }
228}
229
230trait GenericRepository {
231 #[inline]
241 fn gen_config(&self, key: &str) -> Result<String, ConfigReadError> {
242 let out = self
243 .gen_git()
244 .arg("config")
245 .arg(key)
246 .output()
247 .expect("Failed to execute git-config(1)");
248 if out.status.success() {
249 Ok(String::from_utf8(out.stdout)
250 .expect("UTF-8 encoding")
251 .trim()
252 .to_owned())
253 } else {
254 match out.status.code().unwrap_or(3) {
255 1 => Err(ConfigReadError::InvalidSectionOrKey(key.to_owned())),
256 3 => {
257 let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
258 Err(ConfigReadError::InvalidConfigFile(msg))
259 }
260 code => {
261 let msg = String::from_utf8_lossy(out.stderr.as_ref());
262 Err(ConfigReadError::Failure(msg.to_string(), code))
263 }
264 }
265 }
266 }
267
268 #[must_use]
271 fn gen_git(&self) -> Command;
272}
273
274#[derive(Clone, Debug)]
279pub struct Repository {
280 git_dir: AbsoluteDirPath,
282 work_tree: AbsoluteDirPath,
284}
285
286#[allow(missing_docs)]
288#[derive(thiserror::Error, Debug, PartialEq, Eq)]
289pub enum RepoError {
290 #[error("GIT_DIR Not found")]
291 GitDirNotFound,
292 #[error("Bare repository")]
293 BareRepo,
294 #[error("Invalid directory: `{0}`")]
295 InvalidDirectory(PathBuf),
296 #[error("Failed to canonicalize the path buffer: `{0}`")]
297 AbsolutionError(PathBuf),
298 #[error("Failed to access current working directory")]
299 FailAccessCwd,
300}
301
302impl From<RepoError> for PosixError {
303 #[inline]
304 fn from(e: RepoError) -> Self {
305 let msg = format!("{}", e);
306 match e {
307 RepoError::GitDirNotFound | RepoError::InvalidDirectory(_) => Self::new(ENOENT, msg),
308 RepoError::AbsolutionError(_) => Self::new(EINVAL, msg),
309 RepoError::FailAccessCwd => Self::new(EACCES, msg),
310 RepoError::BareRepo => Self::new(EINVAL, format!("{}", e)),
311 }
312 }
313}
314
315fn search_git_dir(start: &Path) -> Result<AbsoluteDirPath, RepoError> {
316 let path;
317 if start.is_absolute() {
318 path = start.to_path_buf();
319 } else {
320 match start.canonicalize() {
321 Ok(path_buf) => path = path_buf,
322 Err(_) => return Err(RepoError::InvalidDirectory(start.to_path_buf())),
323 }
324 }
325
326 if let (Ok(head_path), Ok(objects_path)) = (
327 path.join("HEAD").canonicalize(),
328 path.join("objects").canonicalize(),
329 ) {
330 if head_path.is_file() && objects_path.is_dir() {
331 return AbsoluteDirPath::try_from(path.as_path());
332 }
333 }
334
335 for parent in path.ancestors() {
336 let candidate = parent.join(".git");
337 if candidate.is_dir() && candidate.exists() {
338 return candidate.as_path().try_into();
339 }
340 }
341 Err(RepoError::GitDirNotFound)
342}
343
344fn work_tree_from_git_dir(git_dir: &AbsoluteDirPath) -> Result<AbsoluteDirPath, RepoError> {
345 let mut cmd = Command::new("git");
346 cmd.arg("--git-dir");
347 cmd.arg(git_dir.0.as_os_str());
348 cmd.args(&["rev-parse", "--is-bare-repository"]);
349 let output = cmd.output().expect("failed to execute rev-parse");
350 if output.status.success() {
351 let tmp = String::from_utf8_lossy(&output.stdout);
352 if tmp.trim() == "true" {
353 return Err(RepoError::BareRepo);
354 }
355 }
356
357 match git_dir.0.parent() {
358 Some(dir) => Ok(AbsoluteDirPath::try_from(dir)?),
359 None => Err(RepoError::BareRepo),
360 }
361}
362
363fn git_dir_from_work_tree(work_tree: &AbsoluteDirPath) -> Result<AbsoluteDirPath, RepoError> {
364 let result = work_tree.0.join(".git");
365 result.as_path().try_into()
366}
367
368#[allow(missing_docs)]
370#[derive(thiserror::Error, Debug, PartialEq, Eq)]
371#[error("Invalid git reference {0}")]
372pub struct InvalidRefError(String);
373
374impl Repository {
376 #[must_use]
380 #[inline]
381 pub fn is_clean(&self) -> bool {
382 let output = self
383 .git()
384 .args(&["diff", "--quiet", "HEAD"])
385 .output()
386 .expect("Failed to execute git-diff(1)");
387 output.status.success()
388 }
389
390 #[must_use]
394 #[inline]
395 pub fn is_shallow(&self) -> bool {
396 let out = self
397 .git()
398 .args(&["rev-parse", "--is-shallow-repository"])
399 .output()
400 .expect("Failed to execute git-rev-parse(1)");
401 String::from_utf8_lossy(&out.stdout).trim() != "false"
402 }
403
404 #[must_use]
406 #[inline]
407 pub fn remotes(&self) -> Option<HashMap<String, Remote>> {
408 let args = &["remote", "-v"];
409 let mut cmd = self.git();
410 let out = cmd
411 .args(args)
412 .output()
413 .expect("Failed to execute git-remote(1)");
414 if !out.status.success() {
415 return None;
416 }
417
418 let text = String::from_utf8_lossy(&out.stdout);
419 let mut my_map: HashMap<String, Remote> = HashMap::new();
420 let mut remote_lines: Vec<RemoteLine> = vec![];
421 for line in text.lines() {
422 let mut split = line.trim().split('\t');
423 let name = split.next().expect("Remote name").to_owned();
424 let rest = split.next().expect("Remote rest");
425 let mut rest_split = rest.split(' ');
426 let url = rest_split.next().expect("Remote url").to_owned();
427 let dir = if rest_split.next().expect("Remote direction") == "(fetch)" {
428 RemoteDir::Fetch
429 } else {
430 RemoteDir::Push
431 };
432 remote_lines.push(RemoteLine { name, url, dir });
433 }
434 for remote_line in remote_lines {
435 let mut remote = my_map.remove(&remote_line.name).unwrap_or(Remote {
436 name: remote_line.name.clone(),
437 push: None,
438 fetch: None,
439 });
440 match remote_line.dir {
441 RemoteDir::Fetch => remote.fetch = Some(remote_line.url.clone()),
442 RemoteDir::Push => remote.push = Some(remote_line.url.clone()),
443 }
444 my_map.insert(remote_line.name.clone(), remote);
445 }
446
447 Some(my_map)
448 }
449
450 #[must_use]
457 #[inline]
458 pub fn head(&self) -> String {
459 let args = &["rev-parse", "HEAD"];
460 let mut cmd = self.git();
461 let out = cmd
462 .args(args)
463 .output()
464 .expect("Failed to execute git-rev-parse(1)");
465 assert!(
466 out.status.success(),
467 "git rev-parse returned unexpected error"
468 );
469 String::from_utf8_lossy(&out.stdout).trim().to_owned()
470 }
471
472 #[must_use]
477 #[inline]
478 pub fn work_tree(&self) -> Option<PathBuf> {
479 Some(self.work_tree.0.clone())
480 }
481
482 #[must_use]
484 #[inline]
485 pub fn is_sparse(&self) -> bool {
486 let path = self.git_dir_path().join("info").join("sparse-checkout");
487 path.exists()
488 }
489
490 const fn git_dir(&self) -> &AbsoluteDirPath {
492 &self.git_dir
493 }
494
495 const fn git_dir_path(&self) -> &PathBuf {
496 &self.git_dir.0
497 }
498
499 #[inline]
503 pub fn short_ref(&self, long_ref: &str) -> Result<String, InvalidRefError> {
504 let args = vec!["rev-parse", "--short", long_ref];
505 let mut cmd = self.git();
506 let out = cmd
507 .args(args)
508 .output()
509 .expect("Failed to execute git-commit(1)");
510 if !out.status.success() {
511 return Err(InvalidRefError(long_ref.to_owned()));
512 }
513
514 let short_ref = String::from_utf8_lossy(&out.stderr).to_string();
515 Ok(short_ref)
516 }
517}
518
519impl Repository {
521 #[inline]
525 pub fn discover(path: &Path) -> Result<Self, RepoError> {
526 let git_dir = search_git_dir(path)?;
527 let work_tree = work_tree_from_git_dir(&git_dir)?;
528 Ok(Self { git_dir, work_tree })
529 }
530
531 #[inline]
535 pub fn default() -> Result<Self, RepoError> {
536 Self::from_args(None, None, None)
537 }
538
539 #[inline]
547 pub fn create(path: &Path) -> Result<Self, String> {
548 let mut cmd = Command::new("git");
549 let out = cmd
550 .arg("init")
551 .current_dir(&path)
552 .output()
553 .expect("Executed git-init(1)");
554
555 if out.status.success() {
556 let work_tree = path.try_into().map_err(|e| format!("{}", e))?;
557 let git_dir = path
558 .join(".git")
559 .as_path()
560 .try_into()
561 .map_err(|e| format!("{}", e))?;
562 Ok(Self { git_dir, work_tree })
563 } else {
564 Err(String::from_utf8_lossy(&out.stderr).to_string())
565 }
566 }
567
568 #[inline]
572 pub fn from_args(
573 change: Option<&str>,
574 git: Option<&str>,
575 work: Option<&str>,
576 ) -> Result<Self, RepoError> {
577 if (change, git, work) == (None, None, None) {
578 let git_dir = if let Ok(gd) = std::env::var("GIT_DIR") {
579 AbsoluteDirPath::try_from(gd.as_ref())?
580 } else {
581 search_git_dir(&cwd()?)?
582 };
583
584 let work_tree = if let Ok(wt) = std::env::var("GIT_WORK_TREE") {
585 AbsoluteDirPath::try_from(wt.as_ref())?
586 } else {
587 work_tree_from_git_dir(&git_dir)?
588 };
589
590 Ok(Self { git_dir, work_tree })
591 } else {
592 let root = change.map_or_else(PathBuf::new, PathBuf::from);
593 match (git, work) {
594 (Some(g_dir), None) => {
595 let git_dir = root.join(g_dir).as_path().try_into()?;
596 let work_tree = work_tree_from_git_dir(&git_dir)?;
597 Ok(Self { git_dir, work_tree })
598 }
599 (None, Some(w_dir)) => {
600 let work_tree = root.join(w_dir).as_path().try_into()?;
601 let git_dir = git_dir_from_work_tree(&work_tree)?;
602 Ok(Self { git_dir, work_tree })
603 }
604 (Some(g_dir), Some(w_dir)) => {
605 let git_dir = root.join(g_dir).as_path().try_into()?;
606 let work_tree = root.join(w_dir).as_path().try_into()?;
607 Ok(Self { git_dir, work_tree })
608 }
609 (None, None) => {
610 let git_dir = search_git_dir(&root)?;
611 let work_tree = work_tree_from_git_dir(&git_dir)?;
612 Ok(Self { git_dir, work_tree })
613 }
614 }
615 }
616 }
617}
618
619#[allow(missing_docs)]
621#[derive(thiserror::Error, Debug, PartialEq, Eq)]
622pub enum SubtreeAddError {
623 #[error("Bare repository")]
624 BareRepository,
625 #[error("Working tree dirty")]
626 WorkTreeDirty,
627 #[error("{0}")]
628 Failure(String, i32),
629}
630
631impl From<SubtreeAddError> for PosixError {
632 #[inline]
633 fn from(err: SubtreeAddError) -> Self {
634 match err {
635 SubtreeAddError::BareRepository | SubtreeAddError::WorkTreeDirty => {
636 Self::new(EINVAL, format!("{}", err))
637 }
638 SubtreeAddError::Failure(msg, code) => Self::new(code, msg),
639 }
640 }
641}
642
643#[allow(missing_docs)]
645#[derive(thiserror::Error, Debug, PartialEq, Eq)]
646pub enum SubtreePullError {
647 #[error("Working tree dirty")]
648 WorkTreeDirty,
649 #[error("{0}")]
650 Failure(String, i32),
651}
652
653#[allow(missing_docs)]
655#[derive(thiserror::Error, Debug, PartialEq, Eq)]
656pub enum SubtreePushError {
657 #[error("{0}")]
658 Failure(String, i32),
659}
660
661#[allow(missing_docs)]
663#[derive(thiserror::Error, Debug, PartialEq, Eq)]
664pub enum SubtreeSplitError {
665 #[error("Work tree is dirty")]
666 WorkTreeDirty,
667 #[error("{0}")]
668 Failure(String, i32),
669}
670
671#[allow(missing_docs)]
673#[derive(thiserror::Error, Debug, PartialEq, Eq)]
674pub enum StagingError {
675 #[error("`{0}`")]
676 Failure(String, i32),
677 #[error("File does not exist: `{0}`")]
678 FileDoesNotExist(PathBuf),
679}
680
681impl From<StagingError> for PosixError {
682 #[inline]
683 fn from(e: StagingError) -> Self {
684 let msg = format!("{}", e);
685 match e {
686 StagingError::FileDoesNotExist(_) => Self::new(ENOENT, msg),
687 StagingError::Failure(_, code) => Self::new(code, msg),
688 }
689 }
690}
691
692#[allow(missing_docs)]
694#[derive(thiserror::Error, Debug)]
695pub enum StashingError {
696 #[error("Failed to stash changes in GIT_WORK_TREE")]
697 Save(i32, String),
698 #[error("Failed to pop stashed changes in GIT_WORK_TREE")]
699 Pop(i32, String),
700}
701
702#[allow(missing_docs)]
704#[derive(thiserror::Error, Debug)]
705pub enum CommitError {
706 #[error("`{0}`")]
707 Failure(String, i32),
708}
709
710#[derive(thiserror::Error, Debug)]
712pub enum RefSearchError {
713 #[error("{0}")]
715 Failure(String),
716 #[error("{0}")]
718 IOError(#[from] std::io::Error),
719 #[error("Failed to find reference {0}")]
721 NotFound(String),
722 #[error("Failed to parse git-ls-remote(1) output: {0}")]
724 ParsingFailure(String),
725}
726
727impl From<RefSearchError> for PosixError {
728 #[inline]
729 fn from(err: RefSearchError) -> Self {
730 match err {
731 RefSearchError::Failure(msg) => Self::new(ENOENT, msg),
732 RefSearchError::IOError(e) => e.into(),
733 RefSearchError::NotFound(s) => Self::new(ENOENT, s),
734 RefSearchError::ParsingFailure(e) => Self::new(EINVAL, e),
735 }
736 }
737}
738
739impl Repository {
741 #[inline]
751 pub fn commit(&self, message: &str) -> Result<(), CommitError> {
752 let out = self
753 .git()
754 .args(&["commit", "-m", message])
755 .output()
756 .expect("Executed git-commit(1)");
757 if out.status.code().unwrap_or(1) != 0 {
758 let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
759 let code = out.status.code().unwrap_or(1);
760 return Err(CommitError::Failure(msg, code));
761 }
762 Ok(())
763 }
764
765 #[inline]
769 pub fn commit_extended(
770 &self,
771 message: &str,
772 allow_empty: bool,
773 no_verify: bool,
774 ) -> Result<(), CommitError> {
775 let mut cmd = self.git();
776 cmd.args(&["commit", "--quiet", "--no-edit"]);
777
778 if allow_empty {
779 cmd.arg("--allow-empty");
780 }
781
782 if no_verify {
783 cmd.arg("--no-verify");
784 }
785
786 cmd.args(&["--message", message]);
787
788 let out = cmd.output().expect("Failed to execute git-commit(1)");
789 if out.status.code().expect("Expected exit code") != 0 {
790 let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
791 let code = out.status.code().unwrap_or(1);
792 return Err(CommitError::Failure(msg, code));
793 }
794 Ok(())
795 }
796 #[inline]
807 pub fn hack_read_file(&self, path: &Path) -> std::io::Result<Vec<u8>> {
808 let absolute_path = self.work_tree.0.join(path);
809 std::fs::read(absolute_path)
810 }
811
812 #[must_use]
814 #[inline]
815 pub fn is_ancestor(&self, first: &str, second: &str) -> bool {
816 let args = vec!["merge-base", "--is-ancestor", first, second];
817 let mut cmd = self.git();
818 cmd.args(args);
819 let proc = cmd.output().expect("Failed to run git-merge-base(1)");
820 proc.status.success()
821 }
822
823 #[inline]
827 pub fn remote_ref_to_id(&self, remote: &str, git_ref: &str) -> Result<String, RefSearchError> {
828 let proc = self.git().args(&["ls-remote", remote, git_ref]).output()?;
829 if !proc.status.success() {
830 let msg = String::from_utf8_lossy(proc.stderr.as_ref()).to_string();
831 return Err(RefSearchError::Failure(msg));
832 }
833 let stdout = String::from_utf8_lossy(&proc.stdout);
834 if let Some(first_line) = stdout.lines().next() {
835 if let Some(id) = first_line.split('\t').next() {
836 return Ok(id.to_owned());
837 }
838 return Err(RefSearchError::ParsingFailure(first_line.to_owned()));
839 }
840
841 Err(RefSearchError::NotFound(git_ref.to_owned()))
842 }
843
844 #[inline]
852 pub fn sparse_checkout_add(&self, pattern: &str) -> Result<(), String> {
853 let out = self
854 .git()
855 .args(["sparse-checkout", "add"])
856 .arg(pattern)
857 .output()
858 .expect("Failed to execute git sparse-checkout");
859
860 if out.status.success() {
861 Ok(())
862 } else {
863 Err(String::from_utf8_lossy(out.stderr.as_ref()).to_string())
864 }
865 }
866
867 #[inline]
875 pub fn stage(&self, path: &Path) -> Result<(), StagingError> {
876 let relative_path = if path.is_absolute() {
877 path.strip_prefix(self.work_tree().expect("Non bare repo"))
878 .expect("Stripped path prefix")
879 } else {
880 path
881 };
882
883 let file = relative_path.as_os_str();
884 let out = self
885 .git()
886 .args(&["add", "--"])
887 .arg(file)
888 .output()
889 .expect("Executed git-add(1)");
890 match out.status.code().unwrap_or(1) {
891 0 => Ok(()),
892 128 => Err(StagingError::FileDoesNotExist(relative_path.to_path_buf())),
893 e => {
894 let msg = String::from_utf8_lossy(&out.stdout).to_string();
895 Err(StagingError::Failure(msg, e))
896 }
897 }
898 }
899
900 #[inline]
906 pub fn stash_almost_all(&self, message: &str) -> Result<(), StashingError> {
907 let mut cmd = self.git();
908 cmd.arg("stash");
909 cmd.arg("--quiet");
910 cmd.args(&["--include-untracked", "-m", message]);
911
912 let out = cmd.output().expect("Failed to execute git-stash(1)");
913 if !out.status.success() {
914 let stderr = String::from_utf8_lossy(&out.stderr).to_string();
915 let code = out.status.code().unwrap_or(1);
916 return Err(StashingError::Save(code, stderr));
917 }
918 Ok(())
919 }
920
921 #[inline]
927 pub fn stash_pop(&self) -> Result<(), StashingError> {
928 let mut cmd = self.git();
929 let out = cmd
930 .args(&["stash", "pop", "--quiet", "--index"])
931 .output()
932 .expect("Failed to execute git-stash(1)");
933
934 if !out.status.success() {
935 let stderr = String::from_utf8_lossy(&out.stderr).to_string();
936 let code = out.status.code().unwrap_or(1);
937 return Err(StashingError::Pop(code, stderr));
938 }
939 Ok(())
940 }
941
942 #[inline]
950 pub fn subtree_add(
951 &self,
952 url: &str,
953 prefix: &str,
954 revision: &str,
955 message: &str,
956 ) -> Result<(), SubtreeAddError> {
957 if !self.is_clean() {
958 return Err(SubtreeAddError::WorkTreeDirty);
959 }
960
961 let args = vec!["-q", "-P", prefix, url, revision, "-m", message];
962 let mut cmd = self.git();
963 cmd.arg("subtree").arg("add").args(args);
964 let out = cmd.output().expect("Failed to execute git-subtree(1)");
965 if out.status.success() {
966 Ok(())
967 } else {
968 let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
969 let code = out.status.code().unwrap_or(1);
970 Err(SubtreeAddError::Failure(msg, code))
971 }
972 }
973
974 #[inline]
982 pub fn subtree_split(&self, prefix: &str) -> Result<(), SubtreeSplitError> {
983 if !self.is_clean() {
984 return Err(SubtreeSplitError::WorkTreeDirty);
985 }
986
987 let args = vec!["-P", prefix, "--rejoin", "HEAD"];
988 let mut cmd = self.git();
989 cmd.arg("subtree").arg("split").args(args);
990 let result = cmd
991 .spawn()
992 .expect("Failed to execute git-subtree(1)")
993 .wait();
994 match result {
995 Ok(code) => {
996 if code.success() {
997 Ok(())
998 } else {
999 Err(SubtreeSplitError::Failure(
1000 "git-subtree split failed".to_owned(),
1001 1,
1002 ))
1003 }
1004 }
1005 Err(e) => {
1006 let msg = format!("{}", e);
1007 Err(SubtreeSplitError::Failure(msg, 1))
1008 }
1009 }
1010 }
1011
1012 #[inline]
1020 pub fn subtree_pull(
1021 &self,
1022 remote: &str,
1023 prefix: &str,
1024 git_ref: &str,
1025 message: &str,
1026 ) -> Result<(), SubtreePullError> {
1027 if !self.is_clean() {
1028 return Err(SubtreePullError::WorkTreeDirty);
1029 }
1030 let args = vec!["-q", "-P", prefix, remote, git_ref, "-m", message];
1031 let mut cmd = self.git();
1032 cmd.arg("subtree").arg("pull").args(args);
1033 let out = cmd.output().expect("Failed to execute git-subtree(1)");
1034 if out.status.success() {
1035 Ok(())
1036 } else {
1037 let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
1038 let code = out.status.code().unwrap_or(1);
1039 Err(SubtreePullError::Failure(msg, code))
1040 }
1041 }
1042
1043 #[inline]
1047 pub fn subtree_push(
1048 &self,
1049 remote: &str,
1050 prefix: &str,
1051 git_ref: &str,
1052 ) -> Result<(), SubtreePushError> {
1053 let args = vec!["subtree", "push", "-q", "-P", prefix, remote, git_ref];
1054 let mut cmd = self.git();
1055 cmd.args(args);
1056 let out = cmd.output().expect("Failed to execute git-subtree(1)");
1057 if out.status.success() {
1058 Ok(())
1059 } else {
1060 let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
1061 let code = out.status.code().unwrap_or(1);
1062 Err(SubtreePushError::Failure(msg, code))
1063 }
1064 }
1065}
1066
1067#[allow(missing_docs)]
1069#[derive(thiserror::Error, Debug)]
1070pub enum InvalidCommitishError {
1071 #[error("Invalid reference or commit id: `{0}`")]
1072 One(String),
1073 #[error("One or Multiple invalid reference or commit ids: `{0:?}`")]
1074 Multiple(Vec<String>),
1075 #[error("{0}")]
1076 Failure(String, i32),
1077}
1078
1079impl Repository {
1081 #[inline]
1092 pub fn merge_base(&self, ids: &[&str]) -> Result<Option<String>, InvalidCommitishError> {
1093 let output = self
1094 .git()
1095 .arg("merge-base")
1096 .args(ids)
1097 .output()
1098 .expect("Executing git-merge-base(1)");
1099 if output.status.success() {
1100 let tmp = String::from_utf8_lossy(&output.stdout);
1101 if tmp.is_empty() {
1102 return Ok(None);
1103 }
1104 let result = tmp.trim_end();
1105 return Ok(Some(result.to_owned()));
1106 }
1107 match output.status.code().expect("Getting status code") {
1108 128 => {
1109 let tmp = ids.to_vec();
1110 let e_ids = tmp.iter().map(ToString::to_string).collect();
1111 Err(InvalidCommitishError::Multiple(e_ids))
1112 }
1113 1 => Ok(None),
1114 code => {
1115 let msg = String::from_utf8_lossy(&output.stdout);
1116 Err(InvalidCommitishError::Failure(msg.to_string(), code))
1117 }
1118 }
1119 }
1120
1121 #[must_use]
1124 #[inline]
1125 pub fn git(&self) -> Command {
1126 let mut cmd = Command::new("git");
1127 let git_dir = self.git_dir().0.to_str().expect("Convert to string");
1128 cmd.env("GIT_DIR", git_dir);
1129 cmd.env("GIT_WORK_TREE", &self.work_tree.0);
1130 cmd.current_dir(&self.work_tree.0);
1131 cmd
1132 }
1133}
1134
1135impl GenericRepository for Repository {
1136 fn gen_git(&self) -> Command {
1137 self.git()
1138 }
1139}
1140
1141#[inline]
1143pub fn setup_test_author() {
1144 std::env::set_var("GIT_AUTHOR_NAME", "Max Musterman");
1145 std::env::set_var("GIT_AUTHOR_EMAIL", "max@example.com");
1146 std::env::set_var("GIT_COMMITTER_NAME", "Max Musterman");
1147 std::env::set_var("GIT_COMMITTER_EMAIL", "max@example.com");
1148}
1149
1150#[cfg(test)]
1151mod test {
1152
1153 mod repository_initialization {
1154 use crate::{RepoError, Repository};
1155 use tempfile::TempDir;
1156
1157 #[test]
1158 fn git_dir_not_found() {
1159 let tmp_dir = TempDir::new().unwrap();
1160 let discovery_path = tmp_dir.path();
1161 let err = Repository::discover(discovery_path)
1162 .expect_err("Fail to find repo in an empty directory");
1163 assert!(err == RepoError::GitDirNotFound);
1164 }
1165
1166 #[test]
1167 fn normal_repo() {
1168 let tmp_dir = TempDir::new().unwrap();
1169 let repo_path = tmp_dir.path();
1170 let _repo = Repository::create(repo_path).unwrap();
1171 }
1172 }
1173
1174 mod is_clean {
1175 use crate::Repository;
1176 use tempfile::TempDir;
1177
1178 #[test]
1179 fn unstaged() {
1180 let tmp_dir = TempDir::new().unwrap();
1181 let repo_path = tmp_dir.path();
1182 let repo = Repository::create(repo_path).expect("Created repository");
1183
1184 let readme = repo_path.join("README.md");
1185 std::fs::File::create(&readme).unwrap();
1186 std::fs::write(&readme, "# README").unwrap();
1187 assert!(!repo.is_clean(), "Repo is unclean if sth. is unstaged");
1188 }
1189
1190 #[test]
1191 fn staged() {
1192 let tmp_dir = TempDir::new().unwrap();
1193 let repo_path = tmp_dir.path();
1194 let repo = Repository::create(repo_path).expect("Created repository");
1195
1196 let readme = repo_path.join("README.md");
1197 std::fs::File::create(&readme).unwrap();
1198 repo.stage(&readme).unwrap();
1199 assert!(!repo.is_clean(), "Repo is unclean if sth. is staged");
1200 }
1201 }
1202
1203 mod config {
1204 use crate::BareRepository;
1205 use tempfile::TempDir;
1206
1207 #[test]
1208 fn config() {
1209 let tmp_dir = TempDir::new().unwrap();
1210 let repo_path = tmp_dir.path();
1211 let repo = BareRepository::create(repo_path).expect("Created bare repository");
1212 let actual = repo.config("core.bare").unwrap();
1213 assert_eq!(actual, "true".to_owned(), "Expected true");
1214
1215 tmp_dir.close().unwrap();
1216 }
1217 }
1218
1219 mod sparse_checkout {
1220 use crate::Repository;
1221 use std::process::Command;
1222 use tempfile::TempDir;
1223
1224 #[test]
1225 fn is_sparse() {
1226 let tmp_dir = TempDir::new().unwrap();
1227 let repo_path = tmp_dir.path();
1228 let repo = Repository::create(repo_path).expect("Created repository");
1229 let mut cmd = Command::new("git");
1230 let out = cmd
1231 .args(&["sparse-checkout", "init"])
1232 .current_dir(repo_path)
1233 .output()
1234 .unwrap();
1235 assert!(out.status.success(), "Try to make repository sparse");
1236 assert!(repo.is_sparse(), "Not sparse repository");
1237 }
1238
1239 #[test]
1240 fn not_sparse() {
1241 let tmp_dir = TempDir::new().unwrap();
1242 let repo_path = tmp_dir.path();
1243 let repo = Repository::create(repo_path).expect("Created repository");
1244 assert!(!repo.is_sparse(), "Not sparse repository");
1245 }
1246
1247 #[test]
1248 fn add() {
1249 let tmp_dir = TempDir::new().unwrap();
1250 let repo_path = tmp_dir.path();
1251 let repo = Repository::create(repo_path).expect("Created repository");
1252 repo.git()
1253 .args(["sparse-checkout", "init"])
1254 .output()
1255 .unwrap();
1256 let actual = repo.sparse_checkout_add("foo/bar");
1257 assert!(actual.is_ok(), "Expected successfull execution");
1258
1259 tmp_dir.close().unwrap();
1260 }
1261 }
1262
1263 mod subtree_add {
1264 use crate::{setup_test_author, Repository, SubtreeAddError};
1265 use tempfile::TempDir;
1266
1267 #[test]
1268 fn dirty_work_tree() {
1269 let tmp_dir = TempDir::new().unwrap();
1270 let repo_path = tmp_dir.path();
1271 let repo = Repository::create(repo_path).expect("Created repository");
1272 let err = repo
1273 .subtree_add("https://example.com/foo/bar", "bar", "HEAD", "Some Message")
1274 .expect_err("Expected an error");
1275 assert_eq!(err, SubtreeAddError::WorkTreeDirty);
1276 tmp_dir.close().unwrap();
1277 }
1278
1279 #[test]
1280 fn successfull() {
1281 let tmp_dir = TempDir::new().unwrap();
1282 let repo_path = tmp_dir.path();
1283 setup_test_author();
1284 let repo = Repository::create(repo_path).expect("Created repository");
1285 let readme = repo_path.join("README.md");
1286 std::fs::File::create(&readme).unwrap();
1287 std::fs::write(&readme, "# README").unwrap();
1288 repo.stage(&readme).unwrap();
1289 repo.commit("Test").unwrap();
1290 let actual = repo.subtree_add(
1291 "https://github.com/kalkin/file-expert",
1292 "bar",
1293 "HEAD",
1294 "Some Message",
1295 );
1296 assert!(actual.is_ok(), "Failure to add subtree");
1297 }
1298 }
1299
1300 mod subtree_pull {
1301 use crate::{setup_test_author, Repository, SubtreePullError};
1302 use tempfile::TempDir;
1303
1304 #[test]
1305 fn dirty_work_tree() {
1306 let tmp_dir = TempDir::new().unwrap();
1307 let repo_path = tmp_dir.path();
1308 let repo = Repository::create(repo_path).expect("Created repository");
1309 let err = repo
1310 .subtree_pull("https://example.com/foo/bar", "bar", "HEAD", "Some Message")
1311 .expect_err("Expected an error");
1312 assert_eq!(err, SubtreePullError::WorkTreeDirty);
1313 }
1314
1315 #[test]
1316 fn successfull() {
1317 let tmp_dir = TempDir::new().unwrap();
1318 let repo_path = tmp_dir.path();
1319 setup_test_author();
1320 let repo = Repository::create(repo_path).expect("Created repository");
1321 let readme = repo_path.join("README.md");
1322 std::fs::File::create(&readme).unwrap();
1323 std::fs::write(&readme, "# README").unwrap();
1324 repo.stage(&readme).unwrap();
1325 repo.commit("Test").unwrap();
1326 repo.subtree_add(
1327 "https://github.com/kalkin/file-expert",
1328 "bar",
1329 "v0.10.1",
1330 "Some Message",
1331 )
1332 .unwrap();
1333
1334 let actual = repo.subtree_pull(
1335 "https://github.com/kalkin/file-expert",
1336 "bar",
1337 "v0.13.1",
1338 "Some message",
1339 );
1340 assert!(actual.is_ok(), "Failure to pull subtree");
1341 }
1342 }
1343
1344 mod remote_ref_resolution {
1345 use crate::RefSearchError;
1346 use crate::Repository;
1347 use tempfile::TempDir;
1348
1349 #[test]
1350 fn not_found() {
1351 let tmp_dir = TempDir::new().unwrap();
1352 let repo_path = tmp_dir.path();
1353 let repo = Repository::create(repo_path).expect("Created repository");
1354 let result =
1355 repo.remote_ref_to_id("https://github.com/kalkin/file-expert", "v230.40.50");
1356 assert!(result.is_err());
1357 #[allow(clippy::shadow_unrelated)]
1358 {
1359 let _expected =
1360 RefSearchError::NotFound("Failed to find reference v230.40.50".to_owned());
1361 assert!(
1362 matches!(result.unwrap_err(), _expected),
1363 "should not find v230.40.50"
1364 );
1365 }
1366 }
1367
1368 #[test]
1369 fn failure() {
1370 let tmp_dir = TempDir::new().unwrap();
1371 let repo_path = tmp_dir.path();
1372 let repo = Repository::create(repo_path).expect("Created repository");
1373 let result = repo.remote_ref_to_id("https://example.com/asd/foo", "v230.40.50");
1374 assert!(result.is_err());
1375 let actual = matches!(result.unwrap_err(), RefSearchError::Failure(_));
1376 assert!(actual, "should not find any repo");
1377 }
1378
1379 #[test]
1380 fn successfull_search() {
1381 let tmp_dir = TempDir::new().unwrap();
1382 let repo_path = tmp_dir.path();
1383 let repo = Repository::create(repo_path).expect("Created repository");
1384 let result = repo.remote_ref_to_id("https://github.com/kalkin/file-expert", "v0.9.0");
1385 assert!(result.is_ok());
1386 let actual = result.unwrap();
1387 let expected = "24f624a0268f6cbcfc163abef5f3acbc6c11085e".to_owned();
1388 assert_eq!(expected, actual, "Find commit id for v0.9.0");
1389 }
1390 }
1391}