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::{Restored, 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(left) = verdict.reason {
131 conflicts.push(IcalMergeConflict {
132 left,
133 right: op.action.clone(),
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 = Restored::default();
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 pairs of actions that collided, one per side.
167 pub conflicts: Vec<IcalMergeConflict<'a>>,
168}
169
170/// Two actions that collided, one per side.
171///
172/// They are named `left` and `right` as vcard-rs names its own pair, the left
173/// one carrying with it why the right one did not simply apply.
174#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct IcalMergeConflict<'a> {
176 /// The left side's action, and why the right one did not simply apply.
177 pub left: IcalMergeReason<'a>,
178 /// The action the right side wanted.
179 pub right: IcalMergeAction<'a>,
180}
181
182/// A left-side action, and why the right one did not simply apply against it.
183#[derive(Clone, Debug, PartialEq, Eq)]
184pub enum IcalMergeReason<'a> {
185 /// Both sides changed the same field. The merged calendar holds the left
186 /// side's outcome, except where a removal met an update, in which case the
187 /// update was kept whichever side it came from.
188 Divergent(IcalMergeAction<'a>),
189 /// One side changed a series and the other changed one of its instances.
190 /// Both survive in the merged calendar; a rule that moved may have moved
191 /// the ground the override stood on, which is why this is said out loud.
192 Recurrence(IcalMergeAction<'a>),
193}
194
195/// One component's address: the steps from the calendar root down to it.
196#[derive(Clone, Debug, Default, PartialEq, Eq)]
197pub struct IcalComponentPath<'a>(pub Vec<IcalComponentStep<'a>>);
198
199/// One step of a component path: a name and the identity that tells it from
200/// its same-named siblings.
201#[derive(Clone, Debug, PartialEq, Eq)]
202pub struct IcalComponentStep<'a> {
203 /// The component name, uppercase.
204 pub name: Cow<'a, str>,
205 /// The `UID`, with the `RECURRENCE-ID` after a solidus when the component
206 /// overrides one instance; the position among same-named siblings when the
207 /// component carries no `UID`.
208 pub key: Cow<'a, str>,
209}
210
211/// One property's address: the component holding it, its name, and what tells
212/// it from the component's other properties of that name.
213#[derive(Clone, Debug, PartialEq, Eq)]
214pub struct IcalPropPath<'a> {
215 /// The component the property belongs to.
216 pub component: IcalComponentPath<'a>,
217 /// The property name as written.
218 pub name: Cow<'a, str>,
219 /// The position among the component's properties of that name, counted in
220 /// the calendar the action was read from.
221 pub index: usize,
222 /// The value that tells the property from its same-named siblings.
223 ///
224 /// Where iCalendar gives it one: the calendar user address of an
225 /// `ATTENDEE`, the URI or inline binary of an `ATTACH`, the `UID` a
226 /// `RELATED-TO` points at, the URI of a `CONFERENCE` or an `IMAGE`.
227 /// Lowercased, since matching normalises and writing is exact.
228 ///
229 /// `None` for every other property, whose position then tells it from its
230 /// siblings, and `None` too for a value a same-named sibling repeats,
231 /// which tells neither of them apart.
232 pub identity: Option<Cow<'a, str>>,
233}
234
235/// One change a side made relative to the base.
236#[derive(Clone, Debug, PartialEq, Eq)]
237pub enum IcalMergeAction<'a> {
238 /// A component the side added.
239 ComponentAdded {
240 /// Where it was added.
241 at: IcalComponentPath<'a>,
242 },
243 /// A component the side removed.
244 ComponentRemoved {
245 /// What it removed.
246 at: IcalComponentPath<'a>,
247 },
248 /// A property the side added.
249 PropAdded {
250 /// Where it was added.
251 at: IcalPropPath<'a>,
252 /// The added value.
253 value: IcalValue<'a>,
254 },
255 /// A property the side removed.
256 PropRemoved {
257 /// What it removed.
258 at: IcalPropPath<'a>,
259 /// The removed value.
260 value: IcalValue<'a>,
261 },
262 /// A matched property whose value changed.
263 ValueChanged {
264 /// The changed property.
265 at: IcalPropPath<'a>,
266 /// The base value.
267 old: IcalValue<'a>,
268 /// The changed value.
269 new: IcalValue<'a>,
270 },
271 /// One item joined a list value (`CATEGORIES`, `RDATE`, `EXDATE`).
272 ValueItemAdded {
273 /// The changed property.
274 at: IcalPropPath<'a>,
275 /// The added item.
276 item: Cow<'a, str>,
277 },
278 /// One item left a list value.
279 ValueItemRemoved {
280 /// The changed property.
281 at: IcalPropPath<'a>,
282 /// The removed item.
283 item: Cow<'a, str>,
284 },
285 /// A parameter the side added.
286 ParamAdded {
287 /// The changed property.
288 at: IcalPropPath<'a>,
289 /// The added parameter.
290 param: IcalParam<'a>,
291 },
292 /// A parameter the side removed.
293 ParamRemoved {
294 /// The changed property.
295 at: IcalPropPath<'a>,
296 /// The removed parameter.
297 param: IcalParam<'a>,
298 },
299 /// A parameter whose value changed.
300 ParamChanged {
301 /// The changed property.
302 at: IcalPropPath<'a>,
303 /// The base parameter.
304 old: IcalParam<'a>,
305 /// The changed parameter.
306 new: IcalParam<'a>,
307 },
308}
309
310impl<'a> IcalMergeAction<'a> {
311 /// Whether the action takes something away.
312 fn is_removal(&self) -> bool {
313 matches!(
314 self,
315 Self::ComponentRemoved { .. }
316 | Self::PropRemoved { .. }
317 | Self::ValueItemRemoved { .. }
318 | Self::ParamRemoved { .. }
319 )
320 }
321
322 /// The property the action lands on, for the actions that land on one.
323 fn prop_path(&self) -> Option<&IcalPropPath<'a>> {
324 match self {
325 Self::ComponentAdded { .. } | Self::ComponentRemoved { .. } => None,
326 Self::PropAdded { at, .. }
327 | Self::PropRemoved { at, .. }
328 | Self::ValueChanged { at, .. }
329 | Self::ValueItemAdded { at, .. }
330 | Self::ValueItemRemoved { at, .. }
331 | Self::ParamAdded { at, .. }
332 | Self::ParamRemoved { at, .. }
333 | Self::ParamChanged { at, .. } => Some(at),
334 }
335 }
336}