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
18use chrono::{DateTime, Utc};
19use lazy_static::lazy_static;
20use log::{debug, error};
21use regex::Regex;
22use tempfile::TempDir;
23use thiserror::Error;
24
25use crate::git::{CommitId, GitContext, GitError, GitResult, Identity};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum SubmoduleIntent {
31 CreateDirectory,
33 CreateGitFile,
35 WriteGitFile,
37}
38
39impl fmt::Display for SubmoduleIntent {
40 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
41 let intent = match self {
42 SubmoduleIntent::CreateDirectory => "create the directory structure",
43 SubmoduleIntent::CreateGitFile => "create the .git file",
44 SubmoduleIntent::WriteGitFile => "write the .git file",
45 };
46
47 write!(f, "{}", intent)
48 }
49}
50
51#[non_exhaustive]
53#[derive(Debug, Error)]
54pub enum WorkAreaError {
55 #[error("failed to create workarea's temporary directory")]
57 CreateTempDirectory {
58 #[source]
60 source: io::Error,
61 },
62 #[error("failed to create workarea's work tree directory")]
64 CreateWorkTree {
65 #[source]
67 source: io::Error,
68 },
69 #[error("failed to {} for the {} submodule", intent, submodule)]
71 SubmoduleSetup {
72 intent: SubmoduleIntent,
74 submodule: String,
76 #[source]
78 source: io::Error,
79 },
80 #[error("git error: {}", source)]
82 Git {
83 #[from]
85 source: GitError,
86 },
87}
88
89impl WorkAreaError {
90 pub(crate) fn temp_directory(source: io::Error) -> Self {
91 WorkAreaError::CreateTempDirectory {
92 source,
93 }
94 }
95
96 pub(crate) fn work_tree(source: io::Error) -> Self {
97 WorkAreaError::CreateWorkTree {
98 source,
99 }
100 }
101
102 pub(crate) fn submodule<S>(intent: SubmoduleIntent, submodule: S, source: io::Error) -> Self
103 where
104 S: Into<String>,
105 {
106 WorkAreaError::SubmoduleSetup {
107 intent,
108 submodule: submodule.into(),
109 source,
110 }
111 }
112}
113
114pub(crate) type WorkAreaResult<T> = Result<T, WorkAreaError>;
115
116#[derive(Debug)]
118pub enum Conflict {
119 Path(PathBuf),
121 SubmoduleNotMerged(PathBuf),
123 SubmoduleNotPresent(PathBuf),
125 SubmoduleWithFix(PathBuf, CommitId),
131}
132
133impl Conflict {
134 pub fn path(&self) -> &Path {
136 match *self {
137 Conflict::Path(ref p)
138 | Conflict::SubmoduleNotMerged(ref p)
139 | Conflict::SubmoduleNotPresent(ref p)
140 | Conflict::SubmoduleWithFix(ref p, _) => p,
141 }
142 }
143}
144
145impl PartialEq for Conflict {
146 fn eq(&self, rhs: &Self) -> bool {
147 self.path() == rhs.path()
148 }
149}
150
151pub struct MergeCommand<'a> {
153 command: Command,
155
156 _phantom: PhantomData<&'a str>,
159}
160
161impl<'a> MergeCommand<'a> {
162 pub fn committer(&mut self, committer: &Identity) -> &mut Self {
164 self.command
165 .env("GIT_COMMITTER_NAME", &committer.name)
166 .env("GIT_COMMITTER_EMAIL", &committer.email);
167 self
168 }
169
170 pub fn author(&mut self, author: &Identity) -> &mut Self {
172 self.command
173 .env("GIT_AUTHOR_NAME", &author.name)
174 .env("GIT_AUTHOR_EMAIL", &author.email);
175 self
176 }
177
178 pub fn author_date(&mut self, when: DateTime<Utc>) -> &mut Self {
180 self.command.env("GIT_AUTHOR_DATE", when.to_rfc2822());
181 self
182 }
183
184 pub fn commit<M>(self, message: M) -> GitResult<CommitId>
188 where
189 M: AsRef<str>,
190 {
191 self.commit_impl(message.as_ref())
192 }
193
194 fn commit_impl(mut self, message: &str) -> GitResult<CommitId> {
199 let mut commit_tree = self
200 .command
201 .spawn()
202 .map_err(|err| GitError::subcommand("commit-tree", err))?;
203
204 {
205 let commit_tree_stdin = commit_tree
206 .stdin
207 .as_mut()
208 .expect("expected commit-tree to have a stdin");
209 commit_tree_stdin
210 .write_all(message.as_bytes())
211 .map_err(|err| {
212 GitError::git_with_source(
213 "failed to write the commit message to commit-tree",
214 err,
215 )
216 })?;
217 }
218
219 let commit_tree = commit_tree
220 .wait_with_output()
221 .map_err(|err| GitError::subcommand("commit-tree", err))?;
222 if !commit_tree.status.success() {
223 return Err(GitError::git(format!(
224 "failed to commit the merged tree: {}",
225 String::from_utf8_lossy(&commit_tree.stderr),
226 )));
227 }
228
229 let merge_commit = String::from_utf8_lossy(&commit_tree.stdout);
230 Ok(CommitId::new(merge_commit.trim()))
231 }
232}
233
234impl<'a> Debug for MergeCommand<'a> {
235 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
236 f.debug_struct("MergeCommand").finish()
237 }
238}
239
240#[derive(Debug)]
242pub enum MergeResult<'a> {
243 Conflict(Vec<Conflict>),
245 Ready(MergeCommand<'a>),
249}
250
251pub type SubmoduleConfig = HashMap<String, HashMap<String, String>>;
253
254struct PreparingGitWorkArea {
256 context: GitContext,
258 dir: TempDir,
260}
261
262#[derive(Debug)]
265pub struct GitWorkArea {
266 context: GitContext,
268 dir: TempDir,
270 submodule_config: SubmoduleConfig,
272}
273
274lazy_static! {
275 static ref SUBMODULE_CONFIG_RE: Regex =
278 Regex::new(r"^submodule\.(?P<name>.*)\.(?P<key>[^=]*)=(?P<value>.*)$").unwrap();
279}
280
281trait WorkAreaGitContext {
283 fn cmd(&self) -> Command;
285}
286
287fn checkout<I, P>(ctx: &dyn WorkAreaGitContext, paths: I) -> GitResult<()>
289where
290 I: IntoIterator<Item = P>,
291 P: AsRef<OsStr>,
292{
293 let ls_files = ctx
294 .cmd()
295 .arg("ls-files")
296 .arg("--")
297 .args(paths.into_iter())
298 .output()
299 .map_err(|err| GitError::subcommand("ls-files", err))?;
300 if !ls_files.status.success() {
301 return Err(GitError::git(format!(
302 "listing paths in the index: {}",
303 String::from_utf8_lossy(&ls_files.stderr),
304 )));
305 }
306
307 checkout_files(ctx, &ls_files.stdout)
308}
309
310fn checkout_files(ctx: &dyn WorkAreaGitContext, files: &[u8]) -> GitResult<()> {
312 let mut checkout_index = ctx
313 .cmd()
314 .arg("checkout-index")
315 .arg("--force")
316 .arg("--quiet")
317 .arg("--stdin")
318 .stdin(Stdio::piped())
319 .stdout(Stdio::piped())
320 .stderr(Stdio::piped())
321 .spawn()
322 .map_err(|err| GitError::subcommand("checkout-index", err))?;
323 checkout_index
324 .stdin
325 .as_mut()
326 .expect("expected checkout-index to have a stdin")
327 .write_all(files)
328 .map_err(|err| GitError::git_with_source("writing to checkout-index", err))?;
329 let res = checkout_index
330 .wait()
331 .expect("expected checkout-index to execute successfully");
332 if !res.success() {
333 let mut stderr = Vec::new();
334 checkout_index
335 .stderr
336 .as_mut()
337 .expect("expected checkout-index to have a stderr")
338 .read_to_end(&mut stderr)
339 .map_err(|err| GitError::git_with_source("failed to read from checkout-index", err))?;
340 return Err(GitError::git(format!(
341 "running checkout-index: {}",
342 String::from_utf8_lossy(&stderr),
343 )));
344 }
345
346 let mut update_index = ctx
348 .cmd()
349 .arg("update-index")
350 .arg("--stdin")
351 .stdin(Stdio::piped())
352 .stdout(Stdio::piped())
353 .stderr(Stdio::piped())
354 .spawn()
355 .map_err(|err| GitError::subcommand("update-index", err))?;
356 update_index
357 .stdin
358 .as_mut()
359 .expect("expected update-index to have a stdin")
360 .write_all(files)
361 .map_err(|err| GitError::git_with_source("writing to update-index", err))?;
362 let res = update_index
363 .wait()
364 .expect("expected update-index to execute successfully");
365 if !res.success() {
366 let mut stderr = Vec::new();
367 update_index
368 .stderr
369 .as_mut()
370 .expect("expected update-index to have a stderr")
371 .read_to_end(&mut stderr)
372 .map_err(|err| GitError::git_with_source("failed to read from update-index", err))?;
373 return Err(GitError::git(format!(
374 "running update-index: {}",
375 String::from_utf8_lossy(&stderr),
376 )));
377 }
378
379 Ok(())
380}
381
382impl PreparingGitWorkArea {
383 fn new(context: GitContext, rev: &CommitId) -> Result<Self, WorkAreaError> {
385 let tempdir = TempDir::new_in(context.gitdir()).map_err(WorkAreaError::temp_directory)?;
386
387 let workarea = Self {
388 context,
389 dir: tempdir,
390 };
391
392 debug!(
393 target: "git.workarea",
394 "creating prepared workarea under {}",
395 workarea.dir.path().display(),
396 );
397
398 fs::create_dir_all(workarea.work_tree()).map_err(WorkAreaError::work_tree)?;
399 workarea.prepare(rev)?;
400
401 debug!(
402 target: "git.workarea",
403 "created prepared workarea under {}",
404 workarea.dir.path().display(),
405 );
406
407 Ok(workarea)
408 }
409
410 fn prepare(&self, rev: &CommitId) -> GitResult<()> {
414 let res = self
416 .git()
417 .arg("read-tree")
418 .arg("-i") .arg("-m") .arg(rev.as_str())
421 .output()
422 .map_err(|err| GitError::subcommand("read-tree", err))?;
423 if !res.status.success() {
424 return Err(GitError::git(format!(
425 "reading the tree from {}: {}",
426 rev,
427 String::from_utf8_lossy(&res.stderr),
428 )));
429 }
430
431 self.git()
433 .arg("update-index")
434 .arg("--refresh")
435 .arg("--ignore-missing")
436 .arg("--skip-worktree")
437 .stdout(Stdio::null())
438 .status()
439 .map_err(|err| GitError::subcommand("update-index", err))?;
440 checkout(self, iter::once(".gitmodules"))
444 }
445
446 fn git(&self) -> Command {
448 let mut git = self.context.git();
449
450 git.env("GIT_WORK_TREE", self.work_tree())
451 .env("GIT_INDEX_FILE", self.index());
452
453 git
454 }
455
456 fn query_submodules(&self) -> GitResult<SubmoduleConfig> {
458 let module_path = self.work_tree().join(".gitmodules");
459 if !module_path.exists() {
460 return Ok(SubmoduleConfig::new());
461 }
462
463 let config = self
464 .git()
465 .arg("config")
466 .arg("--file")
467 .arg(module_path)
468 .arg("--list")
469 .output()
470 .map_err(|err| GitError::subcommand("config --file .gitmodules", err))?;
471 if !config.status.success() {
472 return Err(GitError::git(format!(
473 "reading the submodule configuration: {}",
474 String::from_utf8_lossy(&config.stderr),
475 )));
476 }
477 let config = String::from_utf8_lossy(&config.stdout);
478
479 let mut submodule_config = SubmoduleConfig::new();
480
481 let captures = config
482 .lines()
483 .filter_map(|l| SUBMODULE_CONFIG_RE.captures(l));
484 for capture in captures {
485 submodule_config
486 .entry(
487 capture
488 .name("name")
489 .expect("the submodule regex should have a 'name' group")
490 .as_str()
491 .to_string(),
492 )
493 .or_default()
494 .insert(
495 capture
496 .name("key")
497 .expect("the submodule regex should have a 'key' group")
498 .as_str()
499 .to_string(),
500 capture
501 .name("value")
502 .expect("the submodule regex should have a 'value' group")
503 .as_str()
504 .to_string(),
505 );
506 }
507
508 let gitmoduledir = self.context.gitdir().join("modules");
509 Ok(submodule_config
510 .into_iter()
511 .filter(|(name, _)| gitmoduledir.join(name).exists())
512 .collect())
513 }
514
515 fn index(&self) -> PathBuf {
517 self.dir.path().join("index")
518 }
519
520 fn work_tree(&self) -> PathBuf {
522 self.dir.path().join("work")
523 }
524}
525
526impl WorkAreaGitContext for PreparingGitWorkArea {
527 fn cmd(&self) -> Command {
528 self.git()
529 }
530}
531
532impl GitWorkArea {
533 pub fn new(context: GitContext, rev: &CommitId) -> WorkAreaResult<Self> {
535 let intermediate = PreparingGitWorkArea::new(context, rev)?;
536
537 let workarea = Self {
538 submodule_config: intermediate.query_submodules()?,
539 context: intermediate.context,
540 dir: intermediate.dir,
541 };
542
543 debug!(
544 target: "git.workarea",
545 "creating prepared workarea with submodules under {}",
546 workarea.dir.path().display(),
547 );
548
549 workarea.prepare_submodules()?;
550
551 debug!(
552 target: "git.workarea",
553 "created prepared workarea with submodules under {}",
554 workarea.dir.path().display(),
555 );
556
557 Ok(workarea)
558 }
559
560 fn prepare_submodules(&self) -> WorkAreaResult<()> {
562 if self.submodule_config.is_empty() {
563 return Ok(());
564 }
565
566 debug!(
567 target: "git.workarea",
568 "preparing submodules for {}",
569 self.dir.path().display(),
570 );
571
572 for (name, config) in &self.submodule_config {
573 let gitdir = self.context.gitdir().join("modules").join(name);
574
575 if !gitdir.exists() {
576 error!(
577 target: "git.workarea",
578 "{}: submodule configuration for {} does not exist: {}",
579 self.dir.path().display(),
580 name,
581 gitdir.display(),
582 );
583
584 continue;
585 }
586
587 let path = match config.get("path") {
588 Some(path) => path,
589 None => {
590 error!(
591 target: "git.workarea",
592 "{}: submodule configuration for {}.path does not exist (skipping): {}",
593 self.dir.path().display(),
594 name,
595 gitdir.display(),
596 );
597 continue;
598 },
599 };
600 let gitfiledir = self.work_tree().join(path);
601 fs::create_dir_all(&gitfiledir).map_err(|err| {
602 WorkAreaError::submodule(SubmoduleIntent::CreateDirectory, name as &str, err)
603 })?;
604
605 let mut gitfile = File::create(gitfiledir.join(".git")).map_err(|err| {
606 WorkAreaError::submodule(SubmoduleIntent::CreateGitFile, name as &str, err)
607 })?;
608 writeln!(gitfile, "gitdir: {}", gitdir.display()).map_err(|err| {
609 WorkAreaError::submodule(SubmoduleIntent::WriteGitFile, name as &str, err)
610 })?;
611 }
612
613 Ok(())
614 }
615
616 pub fn git(&self) -> Command {
618 let mut git = self.context.git();
619
620 git.env("GIT_WORK_TREE", self.work_tree())
621 .env("GIT_INDEX_FILE", self.index());
622
623 git
624 }
625
626 fn submodule_conflict<P>(
628 &self,
629 path: P,
630 ours: &CommitId,
631 theirs: &CommitId,
632 ) -> GitResult<Conflict>
633 where
634 P: AsRef<Path>,
635 {
636 let path = path.as_ref().to_path_buf();
637
638 debug!(
639 target: "git.workarea",
640 "{} checking for a submodule conflict for {}",
641 self.dir.path().display(),
642 path.display(),
643 );
644
645 let branch_info = self
646 .submodule_config
647 .iter()
648 .find(|&(_, config)| {
649 config.get("path").map_or(false, |submod_path| {
650 submod_path.as_str() == path.to_string_lossy()
651 })
652 })
653 .map(|(name, config)| (name, config.get("branch").map(String::as_str)));
654
655 let (submodule_ctx, branch) = if let Some((name, branch_name)) = branch_info {
656 let submodule_ctx = GitContext::new(self.gitdir().join("modules").join(name));
657
658 let branch_name = if let Some(branch_name) = branch_name {
659 Cow::Borrowed(branch_name)
660 } else {
661 submodule_ctx
662 .default_branch()?
663 .map_or(Cow::Borrowed("master"), Into::into)
664 };
665
666 if branch_name == "." {
667 debug!(
669 target: "git.workarea",
670 "the `.` branch specifier for submodules is not supported for conflict \
671 resolution",
672 );
673
674 return Ok(Conflict::Path(path));
675 }
676
677 (submodule_ctx, branch_name)
678 } else {
679 debug!(
680 target: "git.workarea",
681 "no submodule configured for {}; cannot attempt smarter resolution",
682 path.display(),
683 );
684
685 return Ok(Conflict::Path(path));
686 };
687
688 let refs = submodule_ctx
690 .git()
691 .arg("rev-list")
692 .arg("--first-parent") .arg("--reverse") .arg(branch.as_ref())
695 .arg(format!("^{}", ours))
696 .arg(format!("^{}", theirs))
697 .output()
698 .map_err(|err| GitError::subcommand("rev-list new-submodule ^old-submodule", err))?;
699 if !refs.status.success() {
700 return Ok(Conflict::SubmoduleNotPresent(path));
701 }
702 let refs = String::from_utf8_lossy(&refs.stdout);
703
704 for hash in refs.lines() {
705 let ours_ancestor = submodule_ctx
706 .git()
707 .arg("merge-base")
708 .arg("--is-ancestor")
709 .arg(ours.as_str())
710 .arg(hash)
711 .status()
712 .map_err(|err| GitError::subcommand("merge-base --is-ancestor ours", err))?;
713 let theirs_ancestor = submodule_ctx
714 .git()
715 .arg("merge-base")
716 .arg("--is-ancestor")
717 .arg(theirs.as_str())
718 .arg(hash)
719 .status()
720 .map_err(|err| GitError::subcommand("merge-base --is-ancestor theirs", err))?;
721
722 if ours_ancestor.success() && theirs_ancestor.success() {
723 return Ok(Conflict::SubmoduleWithFix(path, CommitId::new(hash)));
724 }
725 }
726
727 Ok(Conflict::SubmoduleNotMerged(path))
728 }
729
730 fn conflict_information(&self) -> GitResult<Vec<Conflict>> {
732 let ls_files = self
733 .git()
734 .arg("ls-files")
735 .arg("--unmerged")
736 .output()
737 .map_err(|err| GitError::subcommand("ls-files --unmerged", err))?;
738 if !ls_files.status.success() {
739 return Err(GitError::git(format!(
740 "listing unmerged files: {}",
741 String::from_utf8_lossy(&ls_files.stderr),
742 )));
743 }
744 let conflicts = String::from_utf8_lossy(&ls_files.stdout);
745
746 let mut conflict_info = Vec::new();
747
748 let mut ours = CommitId::new(String::new());
750
751 for conflict in conflicts.lines() {
752 let info = conflict.split_whitespace().collect::<Vec<_>>();
753
754 assert!(
755 info.len() == 4,
756 "expected 4 entries for a conflict, received {}",
757 info.len(),
758 );
759
760 let permissions = info[0];
761 let hash = info[1];
762 let stage = info[2];
763 let path = info[3];
764
765 if permissions.starts_with("160000") {
766 if stage == "1" {
767 } else if stage == "2" {
771 ours = CommitId::new(hash);
772 } else if stage == "3" {
773 conflict_info.push(self.submodule_conflict(
774 path,
775 &ours,
776 &CommitId::new(hash),
777 )?);
778 }
779 } else {
780 conflict_info.push(Conflict::Path(Path::new(path).to_path_buf()));
781 }
782 }
783
784 Ok(conflict_info)
785 }
786
787 pub fn checkout<I, P>(&mut self, paths: I) -> GitResult<()>
795 where
796 I: IntoIterator<Item = P>,
797 P: AsRef<OsStr>,
798 {
799 checkout(self, paths)
800 }
801
802 pub fn setup_merge<'a>(
808 &'a self,
809 bases: &[CommitId],
810 base: &CommitId,
811 topic: &CommitId,
812 ) -> GitResult<MergeResult<'a>> {
813 let merge_recursive = self
814 .git()
815 .arg("merge-recursive")
816 .args(bases.iter().map(CommitId::as_str))
817 .arg("--")
818 .arg(base.as_str())
819 .arg(topic.as_str())
820 .output()
821 .map_err(|err| GitError::subcommand("merge-recursive", err))?;
822 if !merge_recursive.status.success() {
823 return Ok(MergeResult::Conflict(self.conflict_information()?));
824 }
825
826 self.setup_merge_impl(base, topic)
827 }
828
829 pub fn setup_update_merge<'a>(
835 &'a self,
836 base: &CommitId,
837 topic: &CommitId,
838 ) -> GitResult<MergeResult<'a>> {
839 self.setup_merge_impl(base, topic)
840 }
841
842 fn setup_merge_impl<'a>(
846 &'a self,
847 base: &CommitId,
848 topic: &CommitId,
849 ) -> GitResult<MergeResult<'a>> {
850 debug!(
851 target: "git.workarea",
852 "merging {} into {}",
853 topic,
854 base,
855 );
856
857 let write_tree = self
858 .git()
859 .arg("write-tree")
860 .output()
861 .map_err(|err| GitError::subcommand("write-tree", err))?;
862 if !write_tree.status.success() {
863 return Err(GitError::git(format!(
864 "writing the tree object: {}",
865 String::from_utf8_lossy(&write_tree.stderr),
866 )));
867 }
868 let merged_tree = String::from_utf8_lossy(&write_tree.stdout);
869 let merged_tree = merged_tree.trim();
870
871 let mut commit_tree = self.git();
872
873 commit_tree
874 .arg("commit-tree")
875 .arg(merged_tree)
876 .arg("-p")
877 .arg(base.as_str())
878 .arg("-p")
879 .arg(topic.as_str())
880 .stdin(Stdio::piped())
881 .stdout(Stdio::piped());
882
883 Ok(MergeResult::Ready(MergeCommand {
884 command: commit_tree,
885 _phantom: PhantomData,
886 }))
887 }
888
889 fn index(&self) -> PathBuf {
891 self.dir.path().join("index")
892 }
893
894 fn work_tree(&self) -> PathBuf {
896 self.dir.path().join("work")
897 }
898
899 pub fn cd_to_work_tree<'a>(&self, cmd: &'a mut Command) -> &'a mut Command {
901 cmd.current_dir(self.work_tree())
902 }
903
904 pub fn gitdir(&self) -> &Path {
906 self.context.gitdir()
907 }
908
909 pub fn submodule_config(&self) -> &SubmoduleConfig {
913 &self.submodule_config
914 }
915
916 #[cfg(test)]
920 pub fn __work_tree(&self) -> PathBuf {
921 self.work_tree()
922 }
923}
924
925impl WorkAreaGitContext for GitWorkArea {
926 fn cmd(&self) -> Command {
927 self.git()
928 }
929}