gitoxide_core/repository/merge/
tree.rs1use 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}