Skip to main content

ical/tree/
merge.rs

1//! # Three-way merge
2//!
3//! Reconcile two divergent edits of a calendar against their common base.
4//!
5//! [`IcalMerge::merge`] is the unit a synchronisation engine needs: given a
6//! base calendar and two calendars derived from it, it reports what each side
7//! changed relative to the base and builds one merged calendar.
8//!
9//! Never last-writer-wins: a field only one side touched is taken from that
10//! side, and a field both sides touched is a conflict, reported so a caller
11//! can resolve it differently.
12//!
13//! The merged calendar starts as a clone of the left one, so the left side's
14//! bytes are there exactly as they were, folds included; the right side's
15//! actions are then replayed line by line, so every line the right side did
16//! not touch keeps its bytes too.
17//!
18//! ## Ours and theirs
19//!
20//! The left side is git's `ours` and the right side is git's `theirs`. The
21//! left side supplies the baseline, so its folding, its parameter casing and
22//! its property order come out untouched, and it keeps its own value where
23//! both sides wrote one into a single field.
24//!
25//! One side answers both questions on purpose. A caller reaches for a merge
26//! holding a version it is merging into, and that version is the one it would
27//! rather not churn and the one it means to keep.
28//!
29//! Every collision is reported either way, so a caller wanting the other value
30//! puts it to somebody rather than asking the merge to guess.
31//!
32//! ## The four steps
33//!
34//! `node` addresses a calendar: it walks it into components, each carrying the
35//! `UID` and `RECURRENCE-ID` path that names it, and finds again in one
36//! calendar what was read in another.
37//!
38//! `diff` matches a side against the base and reports one change per field,
39//! down to a single list item or parameter. `compare` says when two sides
40//! performed one act, which is only where they wrote the same bytes.
41//!
42//! `judge` decides whether the right side's act lands and what to report about
43//! it, and `replay` puts the ones that land onto the left side's bytes.
44//!
45//! ## The two ways a merge can conflict
46//!
47//! **Divergence.** Both sides changed the same field. The left side's outcome
48//! is kept, except where a removal meets an update: there the update wins
49//! whichever side it came from, because keeping data beats losing it silently.
50//!
51//! **Recurrence.** One side changed what defines the series (its `DTSTART`,
52//! `DTEND`, `DURATION`, `RRULE`, `RDATE` or `EXDATE`, or the series component
53//! itself) while the other changed one instance of it.
54//!
55//! Neither is wrong and both survive, but a rule that moved may have moved the
56//! ground the override stood on, so it is reported. A change to anything else
57//! the series carries cannot have moved an occurrence and is not reported
58//! against one.
59
60mod compare;
61mod diff;
62mod judge;
63mod node;
64mod op;
65mod replay;
66
67use alloc::{borrow::Cow, vec::Vec};
68
69use crate::{
70    param::IcalParam,
71    tree::{
72        cst::IcalCst,
73        merge::{
74            diff::Diff,
75            op::{Op, Slot},
76            replay::Shift,
77        },
78    },
79    value::IcalValue,
80};
81
82/// A three-way merge waiting to run.
83///
84/// See the module documentation for the matching, granularity and conflict
85/// rules.
86pub struct IcalMerge<'m, 'a> {
87    /// The common ancestor both sides were derived from.
88    pub base: &'m IcalCst<'a>,
89    /// The side being merged into, git's `ours`. The merged calendar is built
90    /// from its bytes, and a collision neither side settles keeps its value.
91    pub left: &'m IcalCst<'a>,
92    /// The side being merged in, git's `theirs`. Its changes are replayed onto
93    /// the left's bytes.
94    pub right: &'m IcalCst<'a>,
95}
96
97impl<'a> IcalMerge<'_, 'a> {
98    /// Run the merge.
99    pub fn merge(self) -> IcalMergeReport<'a> {
100        let version = self.base.version();
101
102        let base = self.base.nodes();
103        let left = self.left.nodes();
104        let right = self.right.nodes();
105
106        let left_ops = Diff {
107            base: &base,
108            side: &left,
109            version,
110        }
111        .run();
112        let right_ops = Diff {
113            base: &base,
114            side: &right,
115            version,
116        }
117        .run();
118
119        let mut merged = self.left.clone();
120        let mut conflicts = Vec::new();
121        let mut applicable = Vec::new();
122
123        for op in &right_ops {
124            let verdict = self.judge(op, &left_ops, &right_ops);
125
126            if verdict.applies {
127                applicable.push(op);
128            }
129
130            if let Some(reason) = verdict.reason {
131                conflicts.push(IcalMergeConflict {
132                    right: op.action.clone(),
133                    reason,
134                });
135            }
136        }
137
138        applicable.sort_by_key(|op| op.replay_order());
139
140        let shift = Shift::of(&left_ops);
141        let mut restored = Vec::new();
142
143        for op in applicable {
144            self.apply(&mut merged, op, &shift, &mut restored);
145        }
146
147        IcalMergeReport {
148            merged,
149            left: left_ops.into_iter().map(|op| op.action).collect(),
150            right: right_ops.into_iter().map(|op| op.action).collect(),
151            conflicts,
152        }
153    }
154}
155
156/// The outcome of a three-way merge.
157#[derive(Clone, Debug)]
158pub struct IcalMergeReport<'a> {
159    /// The merged calendar: the left one with the right side's applicable
160    /// actions replayed onto it, line by line.
161    pub merged: IcalCst<'a>,
162    /// What the left calendar changed relative to the base.
163    pub left: Vec<IcalMergeAction<'a>>,
164    /// What the right calendar changed relative to the base.
165    pub right: Vec<IcalMergeAction<'a>>,
166    /// The right-side actions that did not simply apply, and why.
167    pub conflicts: Vec<IcalMergeConflict<'a>>,
168}
169
170/// A right-side action that did not simply apply.
171#[derive(Clone, Debug, PartialEq, Eq)]
172pub struct IcalMergeConflict<'a> {
173    /// The action the right side wanted.
174    pub right: IcalMergeAction<'a>,
175    /// Why it did not simply apply.
176    pub reason: IcalMergeReason<'a>,
177}
178
179/// Why a right-side action did not simply apply.
180#[derive(Clone, Debug, PartialEq, Eq)]
181pub enum IcalMergeReason<'a> {
182    /// Both sides changed the same field. The merged calendar holds the left
183    /// side's outcome, except where a removal met an update, in which case the
184    /// update was kept whichever side it came from. The action carried here is
185    /// the left side's, beside the right side's on the conflict itself.
186    Divergent(IcalMergeAction<'a>),
187    /// One side changed a series and the other changed one of its instances.
188    /// Both survive in the merged calendar; a rule that moved may have moved
189    /// the ground the override stood on, which is why this is said out loud.
190    Recurrence(IcalMergeAction<'a>),
191}
192
193/// One component's address: the steps from the calendar root down to it.
194#[derive(Clone, Debug, Default, PartialEq, Eq)]
195pub struct IcalComponentPath<'a>(pub Vec<IcalComponentStep<'a>>);
196
197/// One step of a component path: a name and the identity that tells it from
198/// its same-named siblings.
199#[derive(Clone, Debug, PartialEq, Eq)]
200pub struct IcalComponentStep<'a> {
201    /// The component name, uppercase.
202    pub name: Cow<'a, str>,
203    /// The `UID`, with the `RECURRENCE-ID` after a solidus when the component
204    /// overrides one instance; the position among same-named siblings when the
205    /// component carries no `UID`.
206    pub key: Cow<'a, str>,
207}
208
209/// One property's address: the component holding it, its name, and what tells
210/// it from the component's other properties of that name.
211#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct IcalPropPath<'a> {
213    /// The component the property belongs to.
214    pub component: IcalComponentPath<'a>,
215    /// The property name as written.
216    pub name: Cow<'a, str>,
217    /// The position among the component's properties of that name, counted in
218    /// the calendar the action was read from.
219    pub index: usize,
220    /// The value that tells the property from its same-named siblings.
221    ///
222    /// Where iCalendar gives it one: the calendar user address of an
223    /// `ATTENDEE`, the URI or inline binary of an `ATTACH`, the `UID` a
224    /// `RELATED-TO` points at, the URI of a `CONFERENCE` or an `IMAGE`.
225    /// Lowercased, since matching normalises and writing is exact.
226    ///
227    /// `None` for every other property, whose position then tells it from its
228    /// siblings, and `None` too for a value a same-named sibling repeats,
229    /// which tells neither of them apart.
230    pub identity: Option<Cow<'a, str>>,
231}
232
233/// One change a side made relative to the base.
234#[derive(Clone, Debug, PartialEq, Eq)]
235pub enum IcalMergeAction<'a> {
236    /// A component the side added.
237    ComponentAdded {
238        /// Where it was added.
239        at: IcalComponentPath<'a>,
240    },
241    /// A component the side removed.
242    ComponentRemoved {
243        /// What it removed.
244        at: IcalComponentPath<'a>,
245    },
246    /// A property the side added.
247    PropAdded {
248        /// Where it was added.
249        at: IcalPropPath<'a>,
250        /// The added value.
251        value: IcalValue<'a>,
252    },
253    /// A property the side removed.
254    PropRemoved {
255        /// What it removed.
256        at: IcalPropPath<'a>,
257        /// The removed value.
258        value: IcalValue<'a>,
259    },
260    /// A matched property whose value changed.
261    ValueChanged {
262        /// The changed property.
263        at: IcalPropPath<'a>,
264        /// The base value.
265        old: IcalValue<'a>,
266        /// The changed value.
267        new: IcalValue<'a>,
268    },
269    /// One item joined a list value (`CATEGORIES`, `RDATE`, `EXDATE`).
270    ValueItemAdded {
271        /// The changed property.
272        at: IcalPropPath<'a>,
273        /// The added item.
274        item: Cow<'a, str>,
275    },
276    /// One item left a list value.
277    ValueItemRemoved {
278        /// The changed property.
279        at: IcalPropPath<'a>,
280        /// The removed item.
281        item: Cow<'a, str>,
282    },
283    /// A parameter the side added.
284    ParamAdded {
285        /// The changed property.
286        at: IcalPropPath<'a>,
287        /// The added parameter.
288        param: IcalParam<'a>,
289    },
290    /// A parameter the side removed.
291    ParamRemoved {
292        /// The changed property.
293        at: IcalPropPath<'a>,
294        /// The removed parameter.
295        param: IcalParam<'a>,
296    },
297    /// A parameter whose value changed.
298    ParamChanged {
299        /// The changed property.
300        at: IcalPropPath<'a>,
301        /// The base parameter.
302        old: IcalParam<'a>,
303        /// The changed parameter.
304        new: IcalParam<'a>,
305    },
306}
307
308impl<'a> IcalMergeAction<'a> {
309    /// Whether the action takes something away.
310    fn is_removal(&self) -> bool {
311        matches!(
312            self,
313            Self::ComponentRemoved { .. }
314                | Self::PropRemoved { .. }
315                | Self::ValueItemRemoved { .. }
316                | Self::ParamRemoved { .. }
317        )
318    }
319
320    /// The property the action lands on, for the actions that land on one.
321    fn prop_path(&self) -> Option<&IcalPropPath<'a>> {
322        match self {
323            Self::ComponentAdded { .. } | Self::ComponentRemoved { .. } => None,
324            Self::PropAdded { at, .. }
325            | Self::PropRemoved { at, .. }
326            | Self::ValueChanged { at, .. }
327            | Self::ValueItemAdded { at, .. }
328            | Self::ValueItemRemoved { at, .. }
329            | Self::ParamAdded { at, .. }
330            | Self::ParamRemoved { at, .. }
331            | Self::ParamChanged { at, .. } => Some(at),
332        }
333    }
334}