gix_merge/commit/
virtual_merge_base.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/// The outcome produced by [`commit::merge_base()`](crate::commit::virtual_merge_base()).
pub struct Outcome {
    /// The commit ids of all the virtual merge bases we have produced in the process of recursively merging the merge-bases.
    /// As they have been written to the object database, they are still available until they are garbage collected.
    /// The last one is the most recently produced and the one returned as `commit_id`.
    /// This is never empty.
    pub virtual_merge_bases: Vec<gix_hash::ObjectId>,
    /// The id of the commit that was created to hold the merged tree.
    pub commit_id: gix_hash::ObjectId,
    /// The hash of the merged tree.
    pub tree_id: gix_hash::ObjectId,
}

/// The error returned by [`commit::merge_base()`](crate::commit::virtual_merge_base()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
    #[error(transparent)]
    MergeTree(#[from] crate::tree::Error),
    #[error("Failed to write tree for merged merge-base or virtual commit")]
    WriteObject(gix_object::write::Error),
    #[error(
        "Conflicts occurred when trying to resolve multiple merge-bases by merging them. This is most certainly a bug."
    )]
    VirtualMergeBaseConflict,
    #[error("Could not find commit to use as basis for a virtual commit")]
    FindCommit(#[from] gix_object::find::existing_object::Error),
}

pub(super) mod function {
    use super::Error;
    use crate::blob::builtin_driver;
    use crate::tree::TreatAsUnresolved;
    use gix_object::FindExt;

    /// Create a single virtual merge-base by merging `first_commit`, `second_commit` and `others` into one.
    /// Note that `first_commit` and `second_commit` are expected to have been popped off `others`, so `first_commit`
    /// was the last provided merge-base of function that provides multiple merge-bases for a pair of commits.
    ///
    /// The parameters `graph`, `diff_resource_cache`, `blob_merge`, `objects`, `abbreviate_hash` and `options` are passed
    /// directly to [`tree()`](crate::tree()) for merging the trees of two merge-bases at a time.
    /// Note that most of `options` are overwritten to match the requirements of a merge-base merge.
    #[allow(clippy::too_many_arguments)]
    pub fn virtual_merge_base<'objects>(
        first_commit: gix_hash::ObjectId,
        second_commit: gix_hash::ObjectId,
        mut others: Vec<gix_hash::ObjectId>,
        graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit<gix_revision::merge_base::Flags>>,
        diff_resource_cache: &mut gix_diff::blob::Platform,
        blob_merge: &mut crate::blob::Platform,
        objects: &'objects (impl gix_object::FindObjectOrHeader + gix_object::Write),
        abbreviate_hash: &mut dyn FnMut(&gix_hash::oid) -> String,
        mut options: crate::tree::Options,
    ) -> Result<super::Outcome, crate::commit::Error> {
        let mut merged_commit_id = first_commit;
        others.push(second_commit);

        options.allow_lossy_resolution = true;
        options.blob_merge.is_virtual_ancestor = true;
        options.blob_merge.text.conflict = builtin_driver::text::Conflict::ResolveWithOurs;
        let favor_ancestor = Some(builtin_driver::binary::ResolveWith::Ancestor);
        options.blob_merge.resolve_binary_with = favor_ancestor;
        options.symlink_conflicts = favor_ancestor;
        let labels = builtin_driver::text::Labels {
            current: Some("Temporary merge branch 1".into()),
            other: Some("Temporary merge branch 2".into()),
            ancestor: None,
        };
        let mut virtual_merge_bases = Vec::new();
        let mut tree_id = None;
        while let Some(next_commit_id) = others.pop() {
            options.marker_size_multiplier += 1;
            let mut out = crate::commit(
                merged_commit_id,
                next_commit_id,
                labels,
                graph,
                diff_resource_cache,
                blob_merge,
                objects,
                abbreviate_hash,
                crate::commit::Options {
                    allow_missing_merge_base: false,
                    tree_merge: options.clone(),
                    use_first_merge_base: false,
                },
            )?;
            // This shouldn't happen, but if for some buggy reason it does, we rather bail.
            if out
                .tree_merge
                .has_unresolved_conflicts(TreatAsUnresolved::ConflictMarkers)
            {
                return Err(Error::VirtualMergeBaseConflict.into());
            }
            let merged_tree_id = out
                .tree_merge
                .tree
                .write(|tree| objects.write(tree))
                .map_err(Error::WriteObject)?;

            tree_id = Some(merged_tree_id);
            merged_commit_id = create_virtual_commit(objects, merged_commit_id, next_commit_id, merged_tree_id)?;

            virtual_merge_bases.extend(out.virtual_merge_bases);
            virtual_merge_bases.push(merged_commit_id);
        }

        Ok(super::Outcome {
            virtual_merge_bases,
            commit_id: merged_commit_id,
            tree_id: tree_id.map_or_else(
                || {
                    let mut buf = Vec::new();
                    objects.find_commit(&merged_commit_id, &mut buf).map(|c| c.tree())
                },
                Ok,
            )?,
        })
    }

    fn create_virtual_commit(
        objects: &(impl gix_object::Find + gix_object::Write),
        parent_a: gix_hash::ObjectId,
        parent_b: gix_hash::ObjectId,
        tree_id: gix_hash::ObjectId,
    ) -> Result<gix_hash::ObjectId, Error> {
        let mut buf = Vec::new();
        let mut commit: gix_object::Commit = objects.find_commit(&parent_a, &mut buf)?.into();
        commit.parents = vec![parent_a, parent_b].into();
        commit.tree = tree_id;
        objects.write(&commit).map_err(Error::WriteObject)
    }
}