Skip to main content

gitoxide_core/repository/merge/
tree.rs

1use crate::OutputFormat;
2
3pub struct Options {
4    pub format: OutputFormat,
5    pub file_favor: Option<gix::merge::tree::FileFavor>,
6    pub tree_favor: Option<gix::merge::tree::TreeFavor>,
7    pub in_memory: bool,
8    pub debug: bool,
9    pub message: Option<String>,
10    pub update_head: bool,
11}
12
13pub(super) mod function {
14
15    use std::collections::BTreeSet;
16
17    use anyhow::{Context, anyhow, bail};
18    use gix::{
19        bstr::{BString, ByteSlice},
20        merge::tree::TreatAsUnresolved,
21        prelude::Write,
22    };
23
24    use super::Options;
25    use crate::OutputFormat;
26
27    #[allow(clippy::too_many_arguments)]
28    pub fn tree(
29        mut repo: gix::Repository,
30        out: &mut dyn std::io::Write,
31        err: &mut dyn std::io::Write,
32        base: BString,
33        ours: BString,
34        theirs: BString,
35        Options {
36            format,
37            file_favor,
38            tree_favor,
39            in_memory,
40            debug,
41            message,
42            update_head,
43        }: Options,
44    ) -> anyhow::Result<()> {
45        if format != OutputFormat::Human {
46            bail!("JSON output isn't implemented yet");
47        }
48        if update_head && in_memory {
49            bail!("`--update-head` cannot be used with `--in-memory` - cannot set head to nothing");
50        }
51        if update_head && message.is_none() {
52            bail!("`--update-head` requires `--message`");
53        }
54        repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
55        if in_memory || message.is_some() {
56            repo.objects.enable_object_memory();
57        }
58        let (base_ref, base_id) = refname_and_tree(&repo, base)?;
59        let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?;
60        let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?;
61
62        let options = repo
63            .tree_merge_options()?
64            .with_file_favor(file_favor)
65            .with_tree_favor(tree_favor);
66        let base_id_str = base_id.to_string();
67        let ours_id_str = ours_id.to_string();
68        let theirs_id_str = theirs_id.to_string();
69        let labels = gix::merge::blob::builtin_driver::text::Labels {
70            ancestor: base_ref
71                .as_ref()
72                .map_or(base_id_str.as_str().into(), |n| n.as_bstr())
73                .into(),
74            current: ours_ref
75                .as_ref()
76                .map_or(ours_id_str.as_str().into(), |n| n.as_bstr())
77                .into(),
78            other: theirs_ref
79                .as_ref()
80                .map_or(theirs_id_str.as_str().into(), |n| n.as_bstr())
81                .into(),
82        };
83        let res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
84        let has_conflicts = !res.conflicts.is_empty();
85        let has_unresolved_conflicts = res.has_unresolved_conflicts(TreatAsUnresolved::default());
86        if message.is_some() && has_unresolved_conflicts {
87            write_unresolved_conflict_paths(err, &res.conflicts)?;
88            if debug {
89                writeln!(err, "{:#?}", &res.conflicts)?;
90            }
91            bail!("Tree conflicted, refusing to write commit");
92        }
93
94        let tree_id = {
95            let _span = gix::trace::detail!("Writing merged tree");
96            let mut written = 0;
97            let tree_id = res
98                .tree
99                .detach()
100                .write(|tree| {
101                    written += 1;
102                    repo.write(tree)
103                })
104                .map_err(|err| anyhow!("{err}"))?;
105            writeln!(out, "{tree_id} (wrote {written} trees)")?;
106            tree_id
107        };
108
109        let conflicts = res.conflicts;
110        if message.is_some() && !in_memory {
111            persist_in_memory_objects(&mut repo)?;
112        }
113
114        if let Some(message) = message {
115            let head_id = repo.head_id()?;
116            let commit_id = if update_head {
117                let commit_id = repo.commit("HEAD", message, tree_id, Some(head_id))?;
118                let mut index = repo.index_from_tree(&tree_id)?;
119                index.write(Default::default())?;
120                commit_id
121            } else {
122                repo.new_commit(message, tree_id, Some(head_id))?.id()
123            };
124            writeln!(out, "{commit_id} (commit)")?;
125            return Ok(());
126        }
127
128        if debug {
129            writeln!(err, "{conflicts:#?}")?;
130        }
131        if has_conflicts {
132            writeln!(err, "{} possibly resolved conflicts", conflicts.len())?;
133        }
134        if has_unresolved_conflicts {
135            bail!("Tree conflicted")
136        }
137        Ok(())
138    }
139
140    fn persist_in_memory_objects(repo: &mut gix::Repository) -> anyhow::Result<()> {
141        let objects = repo.objects.take_object_memory().expect("always write in memory first");
142        for (_id, (kind, data)) in objects.iter() {
143            repo.write_buf(*kind, data).map_err(|err| anyhow!("{err}"))?;
144        }
145        Ok(())
146    }
147
148    fn write_unresolved_conflict_paths(
149        err: &mut dyn std::io::Write,
150        conflicts: &[gix::merge::tree::Conflict],
151    ) -> std::io::Result<()> {
152        let how = TreatAsUnresolved::default();
153        let mut paths = BTreeSet::new();
154        for conflict in conflicts.iter().filter(|conflict| conflict.is_unresolved(how)) {
155            let (ours, theirs) = conflict.changes_in_resolution();
156            for path in [
157                ours.source_location(),
158                ours.location(),
159                theirs.source_location(),
160                theirs.location(),
161            ] {
162                if !path.is_empty() {
163                    paths.insert(path);
164                }
165            }
166        }
167        for path in paths {
168            err.write_all(path.as_ref())?;
169            err.write_all(b"\n")?;
170        }
171        Ok(())
172    }
173
174    fn refname_and_tree(
175        repo: &gix::Repository,
176        revspec: BString,
177    ) -> anyhow::Result<(Option<BString>, gix::hash::ObjectId)> {
178        let spec = repo.rev_parse(revspec.as_bstr())?;
179        let tree_id = spec
180            .single()
181            .context("Expected revspec to expand to a single rev only")?
182            .object()?
183            .peel_to_tree()?
184            .id;
185        let refname = spec.first_reference().map(|r| r.name.shorten().as_bstr().to_owned());
186        Ok((refname, tree_id))
187    }
188}