git_workarea/
prepare.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use 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/// Steps which are involved in the submodule preparation process.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum SubmoduleIntent {
34    /// Creating the directory for the submodule.
35    CreateDirectory,
36    /// Creating a `.git` file for the submodule.
37    CreateGitFile,
38    /// Writing a `.git` file for the submodule.
39    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/// Errors which may occur when using a workarea.
55#[derive(Debug, Error)]
56#[non_exhaustive]
57pub enum WorkAreaError {
58    /// Failed to create a temporary directory for the workarea.
59    #[error("failed to create workarea's temporary directory")]
60    #[deprecated(since = "4.3.0", note = "Use `CreateTempDirectoryPath` instead")]
61    CreateTempDirectory {
62        /// The cause of the failure.
63        #[source]
64        source: io::Error,
65    },
66    /// Failed to create a directory to hold the work tree.
67    #[error("failed to create workarea's work tree directory")]
68    CreateWorkTree {
69        /// The cause of the failure.
70        #[source]
71        source: io::Error,
72    },
73    /// Failure to set up submodules in the workarea.
74    #[error("failed to {} for the {} submodule", intent, submodule)]
75    SubmoduleSetup {
76        /// The action that failed.
77        intent: SubmoduleIntent,
78        /// The submodule that failed.
79        submodule: String,
80        /// The cause of the failure.
81        #[source]
82        source: io::Error,
83    },
84    /// A git operation failed.
85    #[error("git error: {}", source)]
86    Git {
87        /// The cause of the failure.
88        #[from]
89        source: GitError,
90    },
91    /// Failed to create a temporary directory for the workarea.
92    #[error("failed to create workarea's temporary directory under {}", directory.display())]
93    CreateTempDirectoryPath {
94        /// The directory that should have contained the temporary directory.
95        directory: PathBuf,
96        /// The cause of the failure.
97        #[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/// Representation of merge conflict possibilities.
131#[derive(Debug)]
132pub enum Conflict {
133    /// A regular blob has conflicted.
134    Path(PathBuf),
135    /// A submodule points to a commit not merged into the target branch.
136    SubmoduleNotMerged(PathBuf),
137    /// The submodule points to a commit not present in the main repository.
138    SubmoduleNotPresent(PathBuf),
139    /// The submodule conflicts, but a resolution is available.
140    ///
141    /// This occurs when the submodule points to a commit not on the first-parent history of the
142    /// target branch on both sides of the merge. The suggested commit is the oldest commit on the
143    /// main branch which contains both branches.
144    SubmoduleWithFix(PathBuf, CommitId),
145}
146
147impl Conflict {
148    /// The path to the blob that for the conflict.
149    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
165/// A command which has been prepared to create a merge commit.
166pub struct MergeCommand<'a> {
167    /// The merge command.
168    command: Command,
169
170    /// Phantom entry which is used to tie a merge command's lifetime to the `GitWorkArea` to which
171    /// it applies.
172    _phantom: PhantomData<&'a str>,
173}
174
175impl MergeCommand<'_> {
176    /// Set the committer of the merge.
177    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    /// Set the authorship of the merge.
185    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    /// Set the authorship date of the merge.
193    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    /// Commit the merge with the given commit message.
199    ///
200    /// Returns the ID of the merge commit itself.
201    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    /// The implementation of the commit.
209    ///
210    /// This spawns the commit command, feeds the message in over its standard input, runs it and
211    /// returns the new commit object's ID.
212    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/// The result of an attempted merge.
255#[derive(Debug)]
256pub enum MergeResult<'a> {
257    /// A merge conflict occurred.
258    Conflict(Vec<Conflict>),
259    /// The merge is ready to be committed.
260    ///
261    /// The command may be executed in order to create the commit from the merged tree.
262    Ready(MergeCommand<'a>),
263}
264
265/// The configuration for submodules within the tree.
266pub type SubmoduleConfig = HashMap<String, HashMap<String, String>>;
267
268/// Intermediate type for setting up the workarea. Does not include submodules.
269struct PreparingGitWorkArea {
270    /// The context to work with.
271    context: GitContext,
272    /// The directory the workarea lives under.
273    dir: TempDir,
274}
275
276/// A representation of an empty workarea where actions which require a work tree and an index may
277/// be preformed.
278#[derive(Debug)]
279pub struct GitWorkArea {
280    /// The context to work with.
281    context: GitContext,
282    /// The directory the workarea lives under.
283    dir: TempDir,
284    /// The submodule configuration for the workarea.
285    submodule_config: SubmoduleConfig,
286}
287
288lazy_static! {
289    // When reading `.gitmodules`, we need to extract configuration values. This regex matches it
290    // and extracts the relevant parts.
291    static ref SUBMODULE_CONFIG_RE: Regex =
292        Regex::new(r"^submodule\.(?P<name>.*)\.(?P<key>[^=]*)=(?P<value>.*)$").unwrap();
293}
294
295/// A trait to abstract over both `GitWorkArea` and `PreparingGitWorkArea`.
296trait WorkAreaGitContext {
297    /// The `git` command for the workarea.
298    fn cmd(&self) -> Command;
299}
300
301/// Checkout a set of paths into a workarea.
302fn 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
324/// Checkout a set of files (in `git ls-files` format) into a workarea.
325fn 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    // Update the index for the files we put into the context
361    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    /// Create an area for performing actions which require a work tree.
398    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    /// Set up the index file such that it things everything is OK, but no files are actually on
455    /// the filesystem. Also sets up `.gitmodules` since it needs to be on disk for further
456    /// preparations.
457    fn prepare(&self, rev: &CommitId) -> GitResult<()> {
458        // Read the base into the temporary index
459        let res = self
460            .git()
461            .arg("read-tree")
462            .arg("-i") // ignore the working tree
463            .arg("-m") // perform a merge
464            .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        // Make the index believe the working tree is fine.
476        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        // Explicitly do not check the return code; it is a failure.
485
486        // Checkout .gitmodules so that submodules work.
487        checkout(self, iter::once(".gitmodules"))
488    }
489
490    /// Run a git command in the workarea.
491    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    /// Create a `SubmoduleConfig` for the repository.
501    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    /// The path to the index file for the work tree.
560    fn index(&self) -> PathBuf {
561        self.dir.path().join("index")
562    }
563
564    /// The path to the directory for the work tree.
565    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    /// Create an area for performing actions which require a work tree.
578    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    /// Prepare requested submodules for use.
605    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    /// Run a git command in the workarea.
661    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    /// Figure out if there's a possible resolution for the submodule.
671    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                // TODO(#6): Pass the branch name we are working on down to here.
712                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        // NOTE: The submodule is assumed to be kept up-to-date externally.
733        let refs = submodule_ctx
734            .git()
735            .arg("rev-list")
736            .arg("--first-parent") // only look at first-parent history
737            .arg("--reverse") // start with oldest commits
738            .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    /// Extract conflict information from the repository.
775    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        // Submodule conflict info scratch space
793        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                    // Nothing to do; we don't need to know the hash of the submodule at the
812                    // mergebase of the two branches.
813                    // old = hash.to_owned();
814                } 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    /// Checkout paths from the index to the filesystem.
832    ///
833    /// Normally, files are not placed into the worktree, so checks which use other tools to
834    /// inspect file contents do not work. This method checks out files to the working directory
835    /// and fixes up Git's knowledge that they are there.
836    ///
837    /// All paths supported by Git's globbing and searching mechanisms are supported.
838    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    /// Prepare a command to create a merge commit.
847    ///
848    /// The merge is performed, but only as a tree object. In order to create the actual commit
849    /// object, a successful merge returns a command which should be executed to create the commit
850    /// object. That commit object should then be stored in a reference using `git update-ref`.
851    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    /// Prepare a command to create a merge commit.
874    ///
875    /// The merge is performed, but only as a tree object. In order to create the actual commit
876    /// object, a successful merge returns a command which should be executed to create the commit
877    /// object. That commit object should then be stored in a reference using `git update-ref`.
878    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    /// Prepare a command to create a merge commit.
887    ///
888    /// This supports choosing the merge strategy.
889    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    /// The path to the index file for the workarea.
934    fn index(&self) -> PathBuf {
935        self.dir.path().join("index")
936    }
937
938    /// The path to the working directory for the workarea.
939    fn work_tree(&self) -> PathBuf {
940        self.dir.path().join("work")
941    }
942
943    /// Run a command from the work tree root.
944    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    /// The path to the git repository.
949    pub fn gitdir(&self) -> &Path {
950        self.context.gitdir()
951    }
952
953    /// The submodule configuration for the repository.
954    ///
955    /// This is read from the `.gitmodules` file in the commit (if it exists).
956    pub fn submodule_config(&self) -> &SubmoduleConfig {
957        &self.submodule_config
958    }
959
960    /// The path to the working directory for the workarea.
961    ///
962    /// Only exported for testing purposes.
963    #[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}