~whynothugo/vdirsyncer-rs

f805d3c79decb127b18c66a7edef04c007e169a7 — Hugo Osvaldo Barrera 3 months ago 8f3c0f7 auto-conflict-resolution
WIP: automated conflict resolution
3 files changed, 58 insertions(+), 5 deletions(-)

M vdirsyncer/src/config.rs
M vstorage/src/sync/declare.rs
M vstorage/src/sync/plan.rs
M vdirsyncer/src/config.rs => vdirsyncer/src/config.rs +22 -3
@@ 26,7 26,7 @@ use vstorage::{
    base::{IcsItem, Item, Storage, VcardItem},
    caldav::CalDavStorage,
    carddav::CardDavStorage,
    sync::declare::{CollectionDescription, DeclaredMapping, StoragePair},
    sync::declare::{CollectionDescription, ConflictAction, DeclaredMapping, StoragePair},
    vdir::VdirStorage,
    webcal::WebCalStorage,
    CollectionId,


@@ 165,10 165,21 @@ struct PairSection {
    collections: Collections,
    #[allow(dead_code)]
    metadata: Option<Vec<String>>,
    conflict_resolution: Option<VecDeque<String>>,
    conflict_resolution: Option<ConflictResolution>,
    // TODO: partial_sync
}

#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub(crate) enum ConflictResolution {
    #[serde(rename = "a wins")]
    AWins,
    #[serde(rename = "b wins")]
    BWins,
    #[serde(untagged)]
    Cmd(VecDeque<String>),
}

impl PairSection {
    fn try_into_named_pair<I: Item>(
        self,


@@ 205,7 216,7 @@ impl PairSection {
        }

        let conflict_resolution = match self.conflict_resolution {
            Some(args) => {
            Some(ConflictResolution::Cmd(args)) => {
                let mut args: VecDeque<_> = args.into_iter().map(OsString::from).collect();
                Some(RawCommand {
                    command: args


@@ 214,6 225,14 @@ impl PairSection {
                    args: args.into(),
                })
            }
            Some(ConflictResolution::AWins) => {
                pair = pair.with_conflict_action(ConflictAction::KeepA);
                None
            }
            Some(ConflictResolution::BWins) => {
                pair = pair.with_conflict_action(ConflictAction::KeepB);
                None
            }
            None => None,
        };


M vstorage/src/sync/declare.rs => vstorage/src/sync/declare.rs +16 -0
@@ 72,6 72,7 @@ pub struct StoragePair<I: Item> {
    pub(super) mappings: Vec<DeclaredMapping>,
    pub(super) all_from_a: bool,
    pub(super) all_from_b: bool,
    pub(super) conflict_action: ConflictAction,
}

impl<I: Item> StoragePair<I> {


@@ 87,6 88,7 @@ impl<I: Item> StoragePair<I> {
            mappings: Vec::new(),
            all_from_a: false,
            all_from_b: false,
            conflict_action: ConflictAction::Skip,
        }
    }



@@ 115,6 117,13 @@ impl<I: Item> StoragePair<I> {
        self
    }

    /// In case of conflict, apply the supplied action.
    #[must_use]
    pub fn with_conflict_action(mut self, action: ConflictAction) -> Self {
        self.conflict_action = action;
        self
    }

    /// Returns a reference to storage a.
    #[must_use]
    pub fn storage_a(&self) -> &dyn Storage<I> {


@@ 127,3 136,10 @@ impl<I: Item> StoragePair<I> {
        self.storage_b.as_ref()
    }
}

/// Action to automatically apply in case of conflict.
pub enum ConflictAction {
    Skip,
    KeepA,
    KeepB,
}

M vstorage/src/sync/plan.rs => vstorage/src/sync/plan.rs +20 -2
@@ 16,7 16,7 @@ use crate::disco::{DiscoveredCollection, Discovery};
use crate::{base::Item, sync::declare::StoragePair};
use crate::{CollectionId, ErrorKind, Etag, Href};

use super::declare::{CollectionDescription, DeclaredMapping};
use super::declare::{CollectionDescription, ConflictAction, DeclaredMapping};
use super::status::{ItemState, MappingUid, Side, StatusDatabase, StatusError, StatusForItem};

/// Error that occurs when creating a [`Plan`].


@@ 520,7 520,13 @@ impl CollectionPlan {
                    _ => None,
                };

                Ok(ItemAction::for_item(item_a, item_b, previous, uid))
                Ok(ItemAction::for_item(
                    item_a,
                    item_b,
                    previous,
                    uid,
                    &pair.conflict_action,
                ))
            })
            .filter_map(Result::transpose)
            .collect::<Result<Vec<_>, PlanError>>()?;


@@ 604,6 610,7 @@ impl ItemAction {
        current_b: Option<&ItemState>,
        previous: Option<StatusForItem>,
        uid: &str,
        on_conflict: &ConflictAction,
    ) -> Option<ItemAction> {
        match (current_a, current_b, previous) {
            (None, None, None) => unreachable!("no action for item that doesn't exist anywhere"),


@@ 695,6 702,17 @@ impl ItemAction {
            }
        }
    }

    #[must_use]
    fn for_conflicting() -> ItemAction {
        // TODO: When I overwrite a conflicting item in B, I also need to udpate the state in A.
        // This is a unique, new action...???
        ItemAction::Conflict {
            a: a.clone(),
            b: b.clone(),
            is_new: true,
        }
    }
}

impl std::fmt::Display for ItemAction {