gitoxide_core/repository/merge/
file.rs1use std::path::Path;
2
3use anyhow::{anyhow, bail, Context};
4use gix::{
5 bstr::{BString, ByteSlice},
6 merge::blob::{
7 builtin_driver::{binary, text::Conflict},
8 pipeline::WorktreeRoots,
9 Resolution, ResourceKind,
10 },
11 object::tree::EntryKind,
12 Id,
13};
14
15use crate::OutputFormat;
16
17pub fn file(
18 repo: gix::Repository,
19 out: &mut dyn std::io::Write,
20 format: OutputFormat,
21 conflict: Option<gix::merge::blob::builtin_driver::text::Conflict>,
22 base: BString,
23 ours: BString,
24 theirs: BString,
25) -> anyhow::Result<()> {
26 if format != OutputFormat::Human {
27 bail!("JSON output isn't implemented yet");
28 }
29 let index = &repo.index_or_load_from_head()?;
30 let specs = repo.pathspec(
31 false,
32 [base, ours, theirs],
33 true,
34 index,
35 gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping.adjust_for_bare(repo.is_bare()),
36 )?;
37 let mut patterns = specs.search().patterns().map(|p| p.path().to_owned());
40 let base = patterns.next().unwrap();
41 let ours = patterns.next().unwrap();
42 let theirs = patterns.next().unwrap();
43
44 let base_id = repo.rev_parse_single(base.as_bstr()).ok();
45 let ours_id = repo.rev_parse_single(ours.as_bstr()).ok();
46 let theirs_id = repo.rev_parse_single(theirs.as_bstr()).ok();
47 let roots = worktree_roots(base_id, ours_id, theirs_id, repo.workdir())?;
48
49 let mut cache = repo.merge_resource_cache(roots)?;
50 let null = repo.object_hash().null();
51 cache.set_resource(
52 base_id.map_or(null, Id::detach),
53 EntryKind::Blob,
54 base.as_bstr(),
55 ResourceKind::CommonAncestorOrBase,
56 &repo.objects,
57 )?;
58 cache.set_resource(
59 ours_id.map_or(null, Id::detach),
60 EntryKind::Blob,
61 ours.as_bstr(),
62 ResourceKind::CurrentOrOurs,
63 &repo.objects,
64 )?;
65 cache.set_resource(
66 theirs_id.map_or(null, Id::detach),
67 EntryKind::Blob,
68 theirs.as_bstr(),
69 ResourceKind::OtherOrTheirs,
70 &repo.objects,
71 )?;
72
73 let mut options = repo.blob_merge_options()?;
74 if let Some(conflict) = conflict {
75 options.text.conflict = conflict;
76 options.resolve_binary_with = match conflict {
77 Conflict::Keep { .. } => None,
78 Conflict::ResolveWithOurs => Some(binary::ResolveWith::Ours),
79 Conflict::ResolveWithTheirs => Some(binary::ResolveWith::Theirs),
80 Conflict::ResolveWithUnion => None,
81 };
82 }
83 let platform = cache.prepare_merge(&repo.objects, options)?;
84 let labels = gix::merge::blob::builtin_driver::text::Labels {
85 ancestor: Some(base.as_bstr()),
86 current: Some(ours.as_bstr()),
87 other: Some(theirs.as_bstr()),
88 };
89 let mut buf = repo.empty_reusable_buffer();
90 let (pick, resolution) = platform.merge(&mut buf, labels, &repo.command_context()?)?;
91 let buf = platform
92 .buffer_by_pick(pick)
93 .map_err(|_| anyhow!("Participating object was too large"))?
94 .unwrap_or(&buf);
95 out.write_all(buf)?;
96
97 if resolution == Resolution::Conflict {
98 bail!("File conflicted")
99 }
100 Ok(())
101}
102
103fn worktree_roots(
104 base: Option<gix::Id<'_>>,
105 ours: Option<gix::Id<'_>>,
106 theirs: Option<gix::Id<'_>>,
107 workdir: Option<&Path>,
108) -> anyhow::Result<gix::merge::blob::pipeline::WorktreeRoots> {
109 let roots = if base.is_none() || ours.is_none() || theirs.is_none() {
110 let workdir = workdir.context("A workdir is required if one of the bases are provided as path.")?;
111 gix::merge::blob::pipeline::WorktreeRoots {
112 current_root: ours.is_none().then(|| workdir.to_owned()),
113 other_root: theirs.is_none().then(|| workdir.to_owned()),
114 common_ancestor_root: base.is_none().then(|| workdir.to_owned()),
115 }
116 } else {
117 WorktreeRoots::default()
118 };
119 Ok(roots)
120}