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}