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
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/// Steps which are involved in the submodule preparation process.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum SubmoduleIntent {
31    /// Creating the directory for the submodule.
32    CreateDirectory,
33    /// Creating a `.git` file for the submodule.
34    CreateGitFile,
35    /// Writing a `.git` file for the submodule.
36    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/// Errors which may occur when using a workarea.
52#[non_exhaustive]
53#[derive(Debug, Error)]
54pub enum WorkAreaError {
55    /// Failed to create a temporary directory for the workarea.
56    #[error("failed to create workarea's temporary directory")]
57    CreateTempDirectory {
58        /// The cause of the failure.
59        #[source]
60        source: io::Error,
61    },
62    /// Failed to create a directory to hold the work tree.
63    #[error("failed to create workarea's work tree directory")]
64    CreateWorkTree {
65        /// The cause of the failure.
66        #[source]
67        source: io::Error,
68    },
69    /// Failure to set up submodules in the workarea.
70    #[error("failed to {} for the {} submodule", intent, submodule)]
71    SubmoduleSetup {
72        /// The action that failed.
73        intent: SubmoduleIntent,
74        /// The submodule that failed.
75        submodule: String,
76        /// The cause of the failure.
77        #[source]
78        source: io::Error,
79    },
80    /// A git operation failed.
81    #[error("git error: {}", source)]
82    Git {
83        /// The cause of the failure.
84        #[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/// Representation of merge conflict possibilities.
117#[derive(Debug)]
118pub enum Conflict {
119    /// A regular blob has conflicted.
120    Path(PathBuf),
121    /// A submodule points to a commit not merged into the target branch.
122    SubmoduleNotMerged(PathBuf),
123    /// The submodule points to a commit not present in the main repository.
124    SubmoduleNotPresent(PathBuf),
125    /// The submodule conflicts, but a resolution is available.
126    ///
127    /// This occurs when the submodule points to a commit not on the first-parent history of the
128    /// target branch on both sides of the merge. The suggested commit is the oldest commit on the
129    /// main branch which contains both branches.
130    SubmoduleWithFix(PathBuf, CommitId),
131}
132
133impl Conflict {
134    /// The path to the blob that for the conflict.
135    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
151/// A command which has been prepared to create a merge commit.
152pub struct MergeCommand<'a> {
153    /// The merge command.
154    command: Command,
155
156    /// Phantom entry which is used to tie a merge command's lifetime to the `GitWorkArea` to which
157    /// it applies.
158    _phantom: PhantomData<&'a str>,
159}
160
161impl<'a> MergeCommand<'a> {
162    /// Set the committer of the merge.
163    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    /// Set the authorship of the merge.
171    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    /// Set the authorship date of the merge.
179    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    /// Commit the merge with the given commit message.
185    ///
186    /// Returns the ID of the merge commit itself.
187    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    /// The implementation of the commit.
195    ///
196    /// This spawns the commit command, feeds the message in over its standard input, runs it and
197    /// returns the new commit object's ID.
198    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/// The result of an attempted merge.
241#[derive(Debug)]
242pub enum MergeResult<'a> {
243    /// A merge conflict occurred.
244    Conflict(Vec<Conflict>),
245    /// The merge is ready to be committed.
246    ///
247    /// The command may be executed in order to create the commit from the merged tree.
248    Ready(MergeCommand<'a>),
249}
250
251/// The configuration for submodules within the tree.
252pub type SubmoduleConfig = HashMap<String, HashMap<String, String>>;
253
254/// Intermediate type for setting up the workarea. Does not include submodules.
255struct PreparingGitWorkArea {
256    /// The context to work with.
257    context: GitContext,
258    /// The directory the workarea lives under.
259    dir: TempDir,
260}
261
262/// A representation of an empty workarea where actions which require a work tree and an index may
263/// be preformed.
264#[derive(Debug)]
265pub struct GitWorkArea {
266    /// The context to work with.
267    context: GitContext,
268    /// The directory the workarea lives under.
269    dir: TempDir,
270    /// The submodule configuration for the workarea.
271    submodule_config: SubmoduleConfig,
272}
273
274lazy_static! {
275    // When reading `.gitmodules`, we need to extract configuration values. This regex matches it
276    // and extracts the relevant parts.
277    static ref SUBMODULE_CONFIG_RE: Regex =
278        Regex::new(r"^submodule\.(?P<name>.*)\.(?P<key>[^=]*)=(?P<value>.*)$").unwrap();
279}
280
281/// A trait to abstract over both `GitWorkArea` and `PreparingGitWorkArea`.
282trait WorkAreaGitContext {
283    /// The `git` command for the workarea.
284    fn cmd(&self) -> Command;
285}
286
287/// Checkout a set of paths into a workarea.
288fn 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
310/// Checkout a set of files (in `git ls-files` format) into a workarea.
311fn 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    // Update the index for the files we put into the context
347    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    /// Create an area for performing actions which require a work tree.
384    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    /// Set up the index file such that it things everything is OK, but no files are actually on
411    /// the filesystem. Also sets up `.gitmodules` since it needs to be on disk for further
412    /// preparations.
413    fn prepare(&self, rev: &CommitId) -> GitResult<()> {
414        // Read the base into the temporary index
415        let res = self
416            .git()
417            .arg("read-tree")
418            .arg("-i") // ignore the working tree
419            .arg("-m") // perform a merge
420            .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        // Make the index believe the working tree is fine.
432        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        // Explicitly do not check the return code; it is a failure.
441
442        // Checkout .gitmodules so that submodules work.
443        checkout(self, iter::once(".gitmodules"))
444    }
445
446    /// Run a git command in the workarea.
447    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    /// Create a `SubmoduleConfig` for the repository.
457    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    /// The path to the index file for the work tree.
516    fn index(&self) -> PathBuf {
517        self.dir.path().join("index")
518    }
519
520    /// The path to the directory for the work tree.
521    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    /// Create an area for performing actions which require a work tree.
534    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    /// Prepare requested submodules for use.
561    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    /// Run a git command in the workarea.
617    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    /// Figure out if there's a possible resolution for the submodule.
627    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                // TODO(#6): Pass the branch name we are working on down to here.
668                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        // NOTE: The submodule is assumed to be kept up-to-date externally.
689        let refs = submodule_ctx
690            .git()
691            .arg("rev-list")
692            .arg("--first-parent") // only look at first-parent history
693            .arg("--reverse") // start with oldest commits
694            .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    /// Extract conflict information from the repository.
731    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        // Submodule conflict info scratch space
749        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                    // Nothing to do; we don't need to know the hash of the submodule at the
768                    // mergebase of the two branches.
769                    // old = hash.to_owned();
770                } 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    /// Checkout paths from the index to the filesystem.
788    ///
789    /// Normally, files are not placed into the worktree, so checks which use other tools to
790    /// inspect file contents do not work. This method checks out files to the working directory
791    /// and fixes up Git's knowledge that they are there.
792    ///
793    /// All paths supported by Git's globbing and searching mechanisms are supported.
794    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    /// Prepare a command to create a merge commit.
803    ///
804    /// The merge is performed, but only as a tree object. In order to create the actual commit
805    /// object, a successful merge returns a command which should be executed to create the commit
806    /// object. That commit object should then be stored in a reference using `git update-ref`.
807    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    /// Prepare a command to create a merge commit.
830    ///
831    /// The merge is performed, but only as a tree object. In order to create the actual commit
832    /// object, a successful merge returns a command which should be executed to create the commit
833    /// object. That commit object should then be stored in a reference using `git update-ref`.
834    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    /// Prepare a command to create a merge commit.
843    ///
844    /// This supports choosing the merge strategy.
845    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    /// The path to the index file for the workarea.
890    fn index(&self) -> PathBuf {
891        self.dir.path().join("index")
892    }
893
894    /// The path to the working directory for the workarea.
895    fn work_tree(&self) -> PathBuf {
896        self.dir.path().join("work")
897    }
898
899    /// Run a command from the work tree root.
900    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    /// The path to the git repository.
905    pub fn gitdir(&self) -> &Path {
906        self.context.gitdir()
907    }
908
909    /// The submodule configuration for the repository.
910    ///
911    /// This is read from the `.gitmodules` file in the commit (if it exists).
912    pub fn submodule_config(&self) -> &SubmoduleConfig {
913        &self.submodule_config
914    }
915
916    /// The path to the working directory for the workarea.
917    ///
918    /// Only exported for testing purposes.
919    #[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}