gitoxide_core/repository/merge/
file.rs

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