1use std::borrow::Cow;
8use std::collections::hash_map::HashMap;
9use std::ffi::OsStr;
10use std::fmt::{self, Debug};
11use std::fs::{self, File};
12use std::io::{self, Read, Write};
13use std::iter;
14use std::marker::PhantomData;
15use std::path::{Path, PathBuf};
16use std::process::{Command, Stdio};
17
18#[cfg(unix)]
19use std::os::unix::ffi::OsStrExt;
20
21use chrono::{DateTime, Utc};
22use lazy_static::lazy_static;
23use log::{debug, error, warn};
24use regex::Regex;
25use tempfile::TempDir;
26use thiserror::Error;
27
28use crate::git::{CommitId, GitContext, GitError, GitResult, Identity};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum SubmoduleIntent {
34 CreateDirectory,
36 CreateGitFile,
38 WriteGitFile,
40}
41
42impl fmt::Display for SubmoduleIntent {
43 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44 let intent = match self {
45 SubmoduleIntent::CreateDirectory => "create the directory structure",
46 SubmoduleIntent::CreateGitFile => "create the .git file",
47 SubmoduleIntent::WriteGitFile => "write the .git file",
48 };
49
50 write!(f, "{}", intent)
51 }
52}
53
54#[derive(Debug, Error)]
56#[non_exhaustive]
57pub enum WorkAreaError {
58 #[error("failed to create workarea's temporary directory")]
60 #[deprecated(since = "4.3.0", note = "Use `CreateTempDirectoryPath` instead")]
61 CreateTempDirectory {
62 #[source]
64 source: io::Error,
65 },
66 #[error("failed to create workarea's work tree directory")]
68 CreateWorkTree {
69 #[source]
71 source: io::Error,
72 },
73 #[error("failed to {} for the {} submodule", intent, submodule)]
75 SubmoduleSetup {
76 intent: SubmoduleIntent,
78 submodule: String,
80 #[source]
82 source: io::Error,
83 },
84 #[error("git error: {}", source)]
86 Git {
87 #[from]
89 source: GitError,
90 },
91 #[error("failed to create workarea's temporary directory under {}", directory.display())]
93 CreateTempDirectoryPath {
94 directory: PathBuf,
96 #[source]
98 source: io::Error,
99 },
100}
101
102impl WorkAreaError {
103 pub(crate) fn temp_directory(directory: PathBuf, source: io::Error) -> Self {
104 WorkAreaError::CreateTempDirectoryPath {
105 directory,
106 source,
107 }
108 }
109
110 pub(crate) fn work_tree(source: io::Error) -> Self {
111 WorkAreaError::CreateWorkTree {
112 source,
113 }
114 }
115
116 pub(crate) fn submodule<S>(intent: SubmoduleIntent, submodule: S, source: io::Error) -> Self
117 where
118 S: Into<String>,
119 {
120 WorkAreaError::SubmoduleSetup {
121 intent,
122 submodule: submodule.into(),
123 source,
124 }
125 }
126}
127
128pub(crate) type WorkAreaResult<T> = Result<T, WorkAreaError>;
129
130#[derive(Debug)]
132pub enum Conflict {
133 Path(PathBuf),
135 SubmoduleNotMerged(PathBuf),
137 SubmoduleNotPresent(PathBuf),
139 SubmoduleWithFix(PathBuf, CommitId),
145}
146
147impl Conflict {
148 pub fn path(&self) -> &Path {
150 match *self {
151 Conflict::Path(ref p)
152 | Conflict::SubmoduleNotMerged(ref p)
153 | Conflict::SubmoduleNotPresent(ref p)
154 | Conflict::SubmoduleWithFix(ref p, _) => p,
155 }
156 }
157}
158
159impl PartialEq for Conflict {
160 fn eq(&self, rhs: &Self) -> bool {
161 self.path() == rhs.path()
162 }
163}
164
165pub struct MergeCommand<'a> {
167 command: Command,
169
170 _phantom: PhantomData<&'a str>,
173}
174
175impl MergeCommand<'_> {
176 pub fn committer(&mut self, committer: &Identity) -> &mut Self {
178 self.command
179 .env("GIT_COMMITTER_NAME", &committer.name)
180 .env("GIT_COMMITTER_EMAIL", &committer.email);
181 self
182 }
183
184 pub fn author(&mut self, author: &Identity) -> &mut Self {
186 self.command
187 .env("GIT_AUTHOR_NAME", &author.name)
188 .env("GIT_AUTHOR_EMAIL", &author.email);
189 self
190 }
191
192 pub fn author_date(&mut self, when: DateTime<Utc>) -> &mut Self {
194 self.command.env("GIT_AUTHOR_DATE", when.to_rfc2822());
195 self
196 }
197
198 pub fn commit<M>(self, message: M) -> GitResult<CommitId>
202 where
203 M: AsRef<str>,
204 {
205 self.commit_impl(message.as_ref())
206 }
207
208 fn commit_impl(mut self, message: &str) -> GitResult<CommitId> {
213 let mut commit_tree = self
214 .command
215 .spawn()
216 .map_err(|err| GitError::subcommand("commit-tree", err))?;
217
218 {
219 let commit_tree_stdin = commit_tree
220 .stdin
221 .as_mut()
222 .expect("expected commit-tree to have a stdin");
223 commit_tree_stdin
224 .write_all(message.as_bytes())
225 .map_err(|err| {
226 GitError::git_with_source(
227 "failed to write the commit message to commit-tree",
228 err,
229 )
230 })?;
231 }
232
233 let commit_tree = commit_tree
234 .wait_with_output()
235 .map_err(|err| GitError::subcommand("commit-tree", err))?;
236 if !commit_tree.status.success() {
237 return Err(GitError::git(format!(
238 "failed to commit the merged tree: {}",
239 String::from_utf8_lossy(&commit_tree.stderr),
240 )));
241 }
242
243 let merge_commit = String::from_utf8_lossy(&commit_tree.stdout);
244 Ok(CommitId::new(merge_commit.trim()))
245 }
246}
247
248impl Debug for MergeCommand<'_> {
249 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
250 f.debug_struct("MergeCommand").finish()
251 }
252}
253
254#[derive(Debug)]
256pub enum MergeResult<'a> {
257 Conflict(Vec<Conflict>),
259 Ready(MergeCommand<'a>),
263}
264
265pub type SubmoduleConfig = HashMap<String, HashMap<String, String>>;
267
268struct PreparingGitWorkArea {
270 context: GitContext,
272 dir: TempDir,
274}
275
276#[derive(Debug)]
279pub struct GitWorkArea {
280 context: GitContext,
282 dir: TempDir,
284 submodule_config: SubmoduleConfig,
286}
287
288lazy_static! {
289 static ref SUBMODULE_CONFIG_RE: Regex =
292 Regex::new(r"^submodule\.(?P<name>.*)\.(?P<key>[^=]*)=(?P<value>.*)$").unwrap();
293}
294
295trait WorkAreaGitContext {
297 fn cmd(&self) -> Command;
299}
300
301fn checkout<I, P>(ctx: &dyn WorkAreaGitContext, paths: I) -> GitResult<()>
303where
304 I: IntoIterator<Item = P>,
305 P: AsRef<OsStr>,
306{
307 let ls_files = ctx
308 .cmd()
309 .arg("ls-files")
310 .arg("--")
311 .args(paths.into_iter())
312 .output()
313 .map_err(|err| GitError::subcommand("ls-files", err))?;
314 if !ls_files.status.success() {
315 return Err(GitError::git(format!(
316 "listing paths in the index: {}",
317 String::from_utf8_lossy(&ls_files.stderr),
318 )));
319 }
320
321 checkout_files(ctx, &ls_files.stdout)
322}
323
324fn checkout_files(ctx: &dyn WorkAreaGitContext, files: &[u8]) -> GitResult<()> {
326 let mut checkout_index = ctx
327 .cmd()
328 .arg("checkout-index")
329 .arg("--force")
330 .arg("--quiet")
331 .arg("--stdin")
332 .stdin(Stdio::piped())
333 .stdout(Stdio::piped())
334 .stderr(Stdio::piped())
335 .spawn()
336 .map_err(|err| GitError::subcommand("checkout-index", err))?;
337 checkout_index
338 .stdin
339 .as_mut()
340 .expect("expected checkout-index to have a stdin")
341 .write_all(files)
342 .map_err(|err| GitError::git_with_source("writing to checkout-index", err))?;
343 let res = checkout_index
344 .wait()
345 .expect("expected checkout-index to execute successfully");
346 if !res.success() {
347 let mut stderr = Vec::new();
348 checkout_index
349 .stderr
350 .as_mut()
351 .expect("expected checkout-index to have a stderr")
352 .read_to_end(&mut stderr)
353 .map_err(|err| GitError::git_with_source("failed to read from checkout-index", err))?;
354 return Err(GitError::git(format!(
355 "running checkout-index: {}",
356 String::from_utf8_lossy(&stderr),
357 )));
358 }
359
360 let mut update_index = ctx
362 .cmd()
363 .arg("update-index")
364 .arg("--stdin")
365 .stdin(Stdio::piped())
366 .stdout(Stdio::piped())
367 .stderr(Stdio::piped())
368 .spawn()
369 .map_err(|err| GitError::subcommand("update-index", err))?;
370 update_index
371 .stdin
372 .as_mut()
373 .expect("expected update-index to have a stdin")
374 .write_all(files)
375 .map_err(|err| GitError::git_with_source("writing to update-index", err))?;
376 let res = update_index
377 .wait()
378 .expect("expected update-index to execute successfully");
379 if !res.success() {
380 let mut stderr = Vec::new();
381 update_index
382 .stderr
383 .as_mut()
384 .expect("expected update-index to have a stderr")
385 .read_to_end(&mut stderr)
386 .map_err(|err| GitError::git_with_source("failed to read from update-index", err))?;
387 return Err(GitError::git(format!(
388 "running update-index: {}",
389 String::from_utf8_lossy(&stderr),
390 )));
391 }
392
393 Ok(())
394}
395
396impl PreparingGitWorkArea {
397 fn new(context: GitContext, rev: &CommitId) -> Result<Self, WorkAreaError> {
399 let common_dir: Cow<Path> = {
400 let rev_parse = context
401 .git()
402 .arg("rev-parse")
403 .arg("--path-format=absolute")
404 .arg("--git-common-dir")
405 .output()
406 .map_err(|err| GitError::subcommand("rev-parse", err))?;
407 if rev_parse.status.success() {
408 if let Some(path) = rev_parse.stdout.strip_suffix(b"\n") {
409 if cfg!(unix) {
410 PathBuf::from(OsStr::from_bytes(path)).into()
411 } else {
412 PathBuf::from(String::from_utf8_lossy(path).as_ref()).into()
413 }
414 } else {
415 context.gitdir().into()
416 }
417 } else {
418 warn!(
419 "failed to determine the common directory for {}: {}",
420 context.gitdir().display(),
421 String::from_utf8_lossy(&rev_parse.stderr),
422 );
423
424 context.gitdir().into()
425 }
426 };
427
428 let tempdir = TempDir::new_in(&common_dir)
429 .map_err(|err| WorkAreaError::temp_directory(common_dir.into(), err))?;
430
431 let workarea = Self {
432 context,
433 dir: tempdir,
434 };
435
436 debug!(
437 target: "git.workarea",
438 "creating prepared workarea under {}",
439 workarea.dir.path().display(),
440 );
441
442 fs::create_dir_all(workarea.work_tree()).map_err(WorkAreaError::work_tree)?;
443 workarea.prepare(rev)?;
444
445 debug!(
446 target: "git.workarea",
447 "created prepared workarea under {}",
448 workarea.dir.path().display(),
449 );
450
451 Ok(workarea)
452 }
453
454 fn prepare(&self, rev: &CommitId) -> GitResult<()> {
458 let res = self
460 .git()
461 .arg("read-tree")
462 .arg("-i") .arg("-m") .arg(rev.as_str())
465 .output()
466 .map_err(|err| GitError::subcommand("read-tree", err))?;
467 if !res.status.success() {
468 return Err(GitError::git(format!(
469 "reading the tree from {}: {}",
470 rev,
471 String::from_utf8_lossy(&res.stderr),
472 )));
473 }
474
475 self.git()
477 .arg("update-index")
478 .arg("--refresh")
479 .arg("--ignore-missing")
480 .arg("--skip-worktree")
481 .stdout(Stdio::null())
482 .status()
483 .map_err(|err| GitError::subcommand("update-index", err))?;
484 checkout(self, iter::once(".gitmodules"))
488 }
489
490 fn git(&self) -> Command {
492 let mut git = self.context.git();
493
494 git.env("GIT_WORK_TREE", self.work_tree())
495 .env("GIT_INDEX_FILE", self.index());
496
497 git
498 }
499
500 fn query_submodules(&self) -> GitResult<SubmoduleConfig> {
502 let module_path = self.work_tree().join(".gitmodules");
503 if !module_path.exists() {
504 return Ok(SubmoduleConfig::new());
505 }
506
507 let config = self
508 .git()
509 .arg("config")
510 .arg("--file")
511 .arg(module_path)
512 .arg("--list")
513 .output()
514 .map_err(|err| GitError::subcommand("config --file .gitmodules", err))?;
515 if !config.status.success() {
516 return Err(GitError::git(format!(
517 "reading the submodule configuration: {}",
518 String::from_utf8_lossy(&config.stderr),
519 )));
520 }
521 let config = String::from_utf8_lossy(&config.stdout);
522
523 let mut submodule_config = SubmoduleConfig::new();
524
525 let captures = config
526 .lines()
527 .filter_map(|l| SUBMODULE_CONFIG_RE.captures(l));
528 for capture in captures {
529 submodule_config
530 .entry(
531 capture
532 .name("name")
533 .expect("the submodule regex should have a 'name' group")
534 .as_str()
535 .to_string(),
536 )
537 .or_default()
538 .insert(
539 capture
540 .name("key")
541 .expect("the submodule regex should have a 'key' group")
542 .as_str()
543 .to_string(),
544 capture
545 .name("value")
546 .expect("the submodule regex should have a 'value' group")
547 .as_str()
548 .to_string(),
549 );
550 }
551
552 let gitmoduledir = self.context.gitdir().join("modules");
553 Ok(submodule_config
554 .into_iter()
555 .filter(|(name, _)| gitmoduledir.join(name).exists())
556 .collect())
557 }
558
559 fn index(&self) -> PathBuf {
561 self.dir.path().join("index")
562 }
563
564 fn work_tree(&self) -> PathBuf {
566 self.dir.path().join("work")
567 }
568}
569
570impl WorkAreaGitContext for PreparingGitWorkArea {
571 fn cmd(&self) -> Command {
572 self.git()
573 }
574}
575
576impl GitWorkArea {
577 pub fn new(context: GitContext, rev: &CommitId) -> WorkAreaResult<Self> {
579 let intermediate = PreparingGitWorkArea::new(context, rev)?;
580
581 let workarea = Self {
582 submodule_config: intermediate.query_submodules()?,
583 context: intermediate.context,
584 dir: intermediate.dir,
585 };
586
587 debug!(
588 target: "git.workarea",
589 "creating prepared workarea with submodules under {}",
590 workarea.dir.path().display(),
591 );
592
593 workarea.prepare_submodules()?;
594
595 debug!(
596 target: "git.workarea",
597 "created prepared workarea with submodules under {}",
598 workarea.dir.path().display(),
599 );
600
601 Ok(workarea)
602 }
603
604 fn prepare_submodules(&self) -> WorkAreaResult<()> {
606 if self.submodule_config.is_empty() {
607 return Ok(());
608 }
609
610 debug!(
611 target: "git.workarea",
612 "preparing submodules for {}",
613 self.dir.path().display(),
614 );
615
616 for (name, config) in &self.submodule_config {
617 let gitdir = self.context.gitdir().join("modules").join(name);
618
619 if !gitdir.exists() {
620 error!(
621 target: "git.workarea",
622 "{}: submodule configuration for {} does not exist: {}",
623 self.dir.path().display(),
624 name,
625 gitdir.display(),
626 );
627
628 continue;
629 }
630
631 let path = match config.get("path") {
632 Some(path) => path,
633 None => {
634 error!(
635 target: "git.workarea",
636 "{}: submodule configuration for {}.path does not exist (skipping): {}",
637 self.dir.path().display(),
638 name,
639 gitdir.display(),
640 );
641 continue;
642 },
643 };
644 let gitfiledir = self.work_tree().join(path);
645 fs::create_dir_all(&gitfiledir).map_err(|err| {
646 WorkAreaError::submodule(SubmoduleIntent::CreateDirectory, name as &str, err)
647 })?;
648
649 let mut gitfile = File::create(gitfiledir.join(".git")).map_err(|err| {
650 WorkAreaError::submodule(SubmoduleIntent::CreateGitFile, name as &str, err)
651 })?;
652 writeln!(gitfile, "gitdir: {}", gitdir.display()).map_err(|err| {
653 WorkAreaError::submodule(SubmoduleIntent::WriteGitFile, name as &str, err)
654 })?;
655 }
656
657 Ok(())
658 }
659
660 pub fn git(&self) -> Command {
662 let mut git = self.context.git();
663
664 git.env("GIT_WORK_TREE", self.work_tree())
665 .env("GIT_INDEX_FILE", self.index());
666
667 git
668 }
669
670 fn submodule_conflict<P>(
672 &self,
673 path: P,
674 ours: &CommitId,
675 theirs: &CommitId,
676 ) -> GitResult<Conflict>
677 where
678 P: AsRef<Path>,
679 {
680 let path = path.as_ref().to_path_buf();
681
682 debug!(
683 target: "git.workarea",
684 "{} checking for a submodule conflict for {}",
685 self.dir.path().display(),
686 path.display(),
687 );
688
689 let branch_info = self
690 .submodule_config
691 .iter()
692 .find(|&(_, config)| {
693 config.get("path").map_or(false, |submod_path| {
694 submod_path.as_str() == path.to_string_lossy()
695 })
696 })
697 .map(|(name, config)| (name, config.get("branch").map(String::as_str)));
698
699 let (submodule_ctx, branch) = if let Some((name, branch_name)) = branch_info {
700 let submodule_ctx = GitContext::new(self.gitdir().join("modules").join(name));
701
702 let branch_name = if let Some(branch_name) = branch_name {
703 Cow::Borrowed(branch_name)
704 } else {
705 submodule_ctx
706 .default_branch()?
707 .map_or(Cow::Borrowed("master"), Into::into)
708 };
709
710 if branch_name == "." {
711 debug!(
713 target: "git.workarea",
714 "the `.` branch specifier for submodules is not supported for conflict \
715 resolution",
716 );
717
718 return Ok(Conflict::Path(path));
719 }
720
721 (submodule_ctx, branch_name)
722 } else {
723 debug!(
724 target: "git.workarea",
725 "no submodule configured for {}; cannot attempt smarter resolution",
726 path.display(),
727 );
728
729 return Ok(Conflict::Path(path));
730 };
731
732 let refs = submodule_ctx
734 .git()
735 .arg("rev-list")
736 .arg("--first-parent") .arg("--reverse") .arg(branch.as_ref())
739 .arg(format!("^{}", ours))
740 .arg(format!("^{}", theirs))
741 .output()
742 .map_err(|err| GitError::subcommand("rev-list new-submodule ^old-submodule", err))?;
743 if !refs.status.success() {
744 return Ok(Conflict::SubmoduleNotPresent(path));
745 }
746 let refs = String::from_utf8_lossy(&refs.stdout);
747
748 for hash in refs.lines() {
749 let ours_ancestor = submodule_ctx
750 .git()
751 .arg("merge-base")
752 .arg("--is-ancestor")
753 .arg(ours.as_str())
754 .arg(hash)
755 .status()
756 .map_err(|err| GitError::subcommand("merge-base --is-ancestor ours", err))?;
757 let theirs_ancestor = submodule_ctx
758 .git()
759 .arg("merge-base")
760 .arg("--is-ancestor")
761 .arg(theirs.as_str())
762 .arg(hash)
763 .status()
764 .map_err(|err| GitError::subcommand("merge-base --is-ancestor theirs", err))?;
765
766 if ours_ancestor.success() && theirs_ancestor.success() {
767 return Ok(Conflict::SubmoduleWithFix(path, CommitId::new(hash)));
768 }
769 }
770
771 Ok(Conflict::SubmoduleNotMerged(path))
772 }
773
774 fn conflict_information(&self) -> GitResult<Vec<Conflict>> {
776 let ls_files = self
777 .git()
778 .arg("ls-files")
779 .arg("--unmerged")
780 .output()
781 .map_err(|err| GitError::subcommand("ls-files --unmerged", err))?;
782 if !ls_files.status.success() {
783 return Err(GitError::git(format!(
784 "listing unmerged files: {}",
785 String::from_utf8_lossy(&ls_files.stderr),
786 )));
787 }
788 let conflicts = String::from_utf8_lossy(&ls_files.stdout);
789
790 let mut conflict_info = Vec::new();
791
792 let mut ours = CommitId::new(String::new());
794
795 for conflict in conflicts.lines() {
796 let info = conflict.split_whitespace().collect::<Vec<_>>();
797
798 assert!(
799 info.len() == 4,
800 "expected 4 entries for a conflict, received {}",
801 info.len(),
802 );
803
804 let permissions = info[0];
805 let hash = info[1];
806 let stage = info[2];
807 let path = info[3];
808
809 if permissions.starts_with("160000") {
810 if stage == "1" {
811 } else if stage == "2" {
815 ours = CommitId::new(hash);
816 } else if stage == "3" {
817 conflict_info.push(self.submodule_conflict(
818 path,
819 &ours,
820 &CommitId::new(hash),
821 )?);
822 }
823 } else {
824 conflict_info.push(Conflict::Path(Path::new(path).to_path_buf()));
825 }
826 }
827
828 Ok(conflict_info)
829 }
830
831 pub fn checkout<I, P>(&mut self, paths: I) -> GitResult<()>
839 where
840 I: IntoIterator<Item = P>,
841 P: AsRef<OsStr>,
842 {
843 checkout(self, paths)
844 }
845
846 pub fn setup_merge<'a>(
852 &'a self,
853 bases: &[CommitId],
854 base: &CommitId,
855 topic: &CommitId,
856 ) -> GitResult<MergeResult<'a>> {
857 let merge_recursive = self
858 .git()
859 .arg("merge-recursive")
860 .args(bases.iter().map(CommitId::as_str))
861 .arg("--")
862 .arg(base.as_str())
863 .arg(topic.as_str())
864 .output()
865 .map_err(|err| GitError::subcommand("merge-recursive", err))?;
866 if !merge_recursive.status.success() {
867 return Ok(MergeResult::Conflict(self.conflict_information()?));
868 }
869
870 self.setup_merge_impl(base, topic)
871 }
872
873 pub fn setup_update_merge<'a>(
879 &'a self,
880 base: &CommitId,
881 topic: &CommitId,
882 ) -> GitResult<MergeResult<'a>> {
883 self.setup_merge_impl(base, topic)
884 }
885
886 fn setup_merge_impl<'a>(
890 &'a self,
891 base: &CommitId,
892 topic: &CommitId,
893 ) -> GitResult<MergeResult<'a>> {
894 debug!(
895 target: "git.workarea",
896 "merging {} into {}",
897 topic,
898 base,
899 );
900
901 let write_tree = self
902 .git()
903 .arg("write-tree")
904 .output()
905 .map_err(|err| GitError::subcommand("write-tree", err))?;
906 if !write_tree.status.success() {
907 return Err(GitError::git(format!(
908 "writing the tree object: {}",
909 String::from_utf8_lossy(&write_tree.stderr),
910 )));
911 }
912 let merged_tree = String::from_utf8_lossy(&write_tree.stdout);
913 let merged_tree = merged_tree.trim();
914
915 let mut commit_tree = self.git();
916
917 commit_tree
918 .arg("commit-tree")
919 .arg(merged_tree)
920 .arg("-p")
921 .arg(base.as_str())
922 .arg("-p")
923 .arg(topic.as_str())
924 .stdin(Stdio::piped())
925 .stdout(Stdio::piped());
926
927 Ok(MergeResult::Ready(MergeCommand {
928 command: commit_tree,
929 _phantom: PhantomData,
930 }))
931 }
932
933 fn index(&self) -> PathBuf {
935 self.dir.path().join("index")
936 }
937
938 fn work_tree(&self) -> PathBuf {
940 self.dir.path().join("work")
941 }
942
943 pub fn cd_to_work_tree<'a>(&self, cmd: &'a mut Command) -> &'a mut Command {
945 cmd.current_dir(self.work_tree())
946 }
947
948 pub fn gitdir(&self) -> &Path {
950 self.context.gitdir()
951 }
952
953 pub fn submodule_config(&self) -> &SubmoduleConfig {
957 &self.submodule_config
958 }
959
960 #[cfg(test)]
964 pub fn __work_tree(&self) -> PathBuf {
965 self.work_tree()
966 }
967}
968
969impl WorkAreaGitContext for GitWorkArea {
970 fn cmd(&self) -> Command {
971 self.git()
972 }
973}