gitoxide_core/repository/merge/
file.rs

1use 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    // TODO: there should be a way to normalize paths without going through patterns, at least in this case maybe?
38    //       `Search` actually sorts patterns by excluding or not, all that can lead to strange results.
39    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}