Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle: use diff options in Patch
Fintan Halpenny committed 10 months ago
commit caa917406caf1b9c8c949f3f5e4c424103b32488
parent 78fe4ee9087267b0cf0472a2891f127c83ce416f
6 files changed +145 -3757
modified crates/radicle-cli/examples/rad-cob-show.md
@@ -72,7 +72,7 @@ We can show the patch COB too.

```
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object d1f7f869fde9fac19c1779c4c2e77e8361333f91
-
{"title":"Start drafting peace treaty","author":{"id":"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"},"state":{"status":"open"},"target":"delegates","labels":[],"merges":{},"revisions":{"d1f7f869fde9fac19c1779c4c2e77e8361333f91":{"id":"d1f7f869fde9fac19c1779c4c2e77e8361333f91","author":{"id":"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"},"description":[{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","timestamp":1671125284000,"body":"See details.","embeds":[]}],"base":"f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354","oid":"575ed68c716d6aae81ea6b718fd9ac66a8eae532","discussion":{"comments":{},"timeline":[]},"reviews":{},"timestamp":1671125284000,"resolves":[],"reactions":[]}},"assignees":[],"timeline":["d1f7f869fde9fac19c1779c4c2e77e8361333f91"],"reviews":{}}
+
{"title":"Start drafting peace treaty","author":{"id":"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"},"state":{"status":"open"},"target":"delegates","labels":[],"merges":{},"revisions":{"d1f7f869fde9fac19c1779c4c2e77e8361333f91":{"id":"d1f7f869fde9fac19c1779c4c2e77e8361333f91","author":{"id":"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"},"description":[{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","timestamp":1671125284000,"body":"See details.","embeds":[]}],"base":"f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354","oid":"575ed68c716d6aae81ea6b718fd9ac66a8eae532","discussion":{"comments":{},"timeline":[]},"reviews":{},"timestamp":1671125284000,"resolves":[],"reactions":[]}},"assignees":[],"timeline":["d1f7f869fde9fac19c1779c4c2e77e8361333f91"],"reviews":{},"diffOptions":{"algorithm":"Histogram","skipBinary":false,"contextLines":3,"interhunkLines":0,"find":{"exact_match":false,"renames":{"limit":200,"rename_threshold":50}}}}
```

Finally let's update the issue and see the output of `rad cob show` also changes.
modified crates/radicle-remote-helper/src/push.rs
@@ -505,6 +505,7 @@ where
            patch::MergeTarget::default(),
            base,
            head,
+
            None,
            &[],
            signer,
        )
@@ -515,6 +516,7 @@ where
            patch::MergeTarget::default(),
            base,
            head,
+
            None,
            &[],
            signer,
        )
modified crates/radicle/src/cob/patch.rs
@@ -234,6 +234,11 @@ pub enum Action {
        /// Review comments resolved by this revision.
        #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
        resolves: BTreeSet<(EntryId, CommentId)>,
+
        /// The diff options for the patch.
+
        ///
+
        /// **N.B**: Only relevant for the initial revision creation.
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        diff_options: Option<diff::Options>,
    },
    #[serde(rename = "revision.edit")]
    RevisionEdit {
@@ -428,11 +433,64 @@ pub struct Patch {
    pub(super) timeline: Vec<EntryId>,
    /// Reviews index. Keeps track of reviews for better performance.
    pub(super) reviews: BTreeMap<ReviewId, Option<(RevisionId, ActorId)>>,
+
    /// The options used to provide the diff of the [`Patch`].
+
    #[serde(default)]
+
    pub(super) diff_options: diff::Options,
+
}
+

+
/// Data to create a new [`Patch`].
+
///
+
/// A new request can be constructed with [`CreatePatchRequest::new`]. The
+
/// [`MergeTarget`] and [`diff::Options`] can be set for the request by using
+
/// [`CreatePatchRequest::with_target`] and
+
/// [`CreatePatchRequest::with_diff_options`], respectively.
+
///
+
/// To use the [`CreatePatchRequest`], see [`Patch::new`].
+
pub struct CreatePatchRequest {
+
    title: String,
+
    target: MergeTarget,
+
    id: RevisionId,
+
    revision: Revision,
+
    diff_options: diff::Options,
+
}
+

+
impl CreatePatchRequest {
+
    /// Construct a new `CreatePatch` using default for `target`and
+
    /// `diff_options`.
+
    pub fn new(title: String, id: RevisionId, revision: Revision) -> Self {
+
        Self {
+
            title,
+
            target: MergeTarget::default(),
+
            id,
+
            revision,
+
            diff_options: diff::Options::default(),
+
        }
+
    }
+

+
    /// Set the `target`.
+
    pub fn with_target(self, target: MergeTarget) -> Self {
+
        Self { target, ..self }
+
    }
+

+
    /// Set the `diff_options`.
+
    pub fn with_diff_options(self, diff_options: diff::Options) -> Self {
+
        Self {
+
            diff_options,
+
            ..self
+
        }
+
    }
}

impl Patch {
-
    /// Construct a new patch object from a revision.
-
    pub fn new(title: String, target: MergeTarget, (id, revision): (RevisionId, Revision)) -> Self {
+
    /// Construct a new patch object from a [`CreatePatchRequest`].
+
    pub fn new(create: CreatePatchRequest) -> Self {
+
        let CreatePatchRequest {
+
            title,
+
            target,
+
            id,
+
            revision,
+
            diff_options,
+
        } = create;
        Self {
            title,
            author: revision.author.clone(),
@@ -444,6 +502,7 @@ impl Patch {
            assignees: BTreeSet::default(),
            timeline: vec![id.into_inner()],
            reviews: BTreeMap::default(),
+
            diff_options,
        }
    }

@@ -845,6 +904,8 @@ impl Patch {
                base,
                oid,
                resolves,
+
                // ignored for new revisions
+
                diff_options: _,
            } => {
                debug_assert!(!self.revisions.contains_key(&entry));
                let id = RevisionId(entry);
@@ -1136,6 +1197,7 @@ impl store::Cob for Patch {
            base,
            oid,
            resolves,
+
            diff_options,
        }) = actions.next()
        else {
            return Err(Error::Init("the first action must be of type `revision`"));
@@ -1152,7 +1214,11 @@ impl store::Cob for Patch {
            op.timestamp,
            resolves,
        );
-
        let mut patch = Patch::new(title, target, (RevisionId(op.id), revision));
+

+
        let create = CreatePatchRequest::new(title, RevisionId(op.id), revision)
+
            .with_target(target)
+
            .with_diff_options(diff_options.unwrap_or_default());
+
        let mut patch = Patch::new(create);

        for action in actions {
            match patch.authorization(&action, &op.author, &doc)? {
@@ -1914,6 +1980,22 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
        self.push(Action::Merge { revision, commit })
    }

+
    fn initial_revision(
+
        &mut self,
+
        description: impl ToString,
+
        base: impl Into<git::Oid>,
+
        oid: impl Into<git::Oid>,
+
        opts: Option<diff::Options>,
+
    ) -> Result<(), store::Error> {
+
        self.push(Action::Revision {
+
            description: description.to_string(),
+
            base: base.into(),
+
            oid: oid.into(),
+
            resolves: BTreeSet::new(),
+
            diff_options: opts,
+
        })
+
    }
+

    /// Update a patch with a new revision.
    pub fn revision(
        &mut self,
@@ -1926,6 +2008,7 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
            base: base.into(),
            oid: oid.into(),
            resolves: BTreeSet::new(),
+
            diff_options: None,
        })
    }

@@ -2599,6 +2682,7 @@ where
        target: MergeTarget,
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
+
        opts: Option<diff::Options>,
        labels: &[Label],
        cache: &'g mut C,
        signer: &Device<G>,
@@ -2613,6 +2697,7 @@ where
            target,
            base,
            oid,
+
            opts,
            labels,
            Lifecycle::default(),
            cache,
@@ -2628,6 +2713,7 @@ where
        target: MergeTarget,
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
+
        opts: Option<diff::Options>,
        labels: &[Label],
        cache: &'g mut C,
        signer: &Device<G>,
@@ -2642,6 +2728,7 @@ where
            target,
            base,
            oid,
+
            opts,
            labels,
            Lifecycle::Draft,
            cache,
@@ -2676,6 +2763,7 @@ where
        target: MergeTarget,
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
+
        opts: Option<diff::Options>,
        labels: &[Label],
        state: Lifecycle,
        cache: &'g mut C,
@@ -2686,7 +2774,7 @@ where
        G: crypto::signature::Signer<crypto::Signature>,
    {
        let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx, _| {
-
            tx.revision(description, base, oid)?;
+
            tx.initial_revision(description, base, oid, opts)?;
            tx.edit(title, target)?;

            if !labels.is_empty() {
@@ -2862,6 +2950,7 @@ mod test {
                target,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -2902,6 +2991,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -2935,6 +3025,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -2966,6 +3057,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -3018,6 +3110,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -3058,6 +3151,7 @@ mod test {
                base,
                oid,
                resolves: Default::default(),
+
                diff_options: None,
            },
            Action::Edit {
                title: String::from("My patch"),
@@ -3069,6 +3163,7 @@ mod test {
            base,
            oid,
            resolves: Default::default(),
+
            diff_options: None,
        }]);
        let a3 = alice.op::<Patch>([Action::RevisionRedact {
            revision: RevisionId(a2.id()),
@@ -3109,6 +3204,7 @@ mod test {
                    base,
                    oid,
                    resolves: Default::default(),
+
                    diff_options: None,
                },
                Action::Edit {
                    title: String::from("Some patch"),
@@ -3124,6 +3220,7 @@ mod test {
                base,
                oid,
                resolves: Default::default(),
+
                diff_options: None,
            },
            &alice,
        );
@@ -3169,6 +3266,7 @@ mod test {
                base,
                oid,
                resolves: Default::default(),
+
                diff_options: None,
            },
            Action::Edit {
                title: String::from("My patch"),
@@ -3206,6 +3304,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -3251,6 +3350,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -3282,6 +3382,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -3333,6 +3434,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -3380,6 +3482,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -3435,6 +3538,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
@@ -3484,6 +3588,7 @@ mod test {
                MergeTarget::Delegates,
                branch.base,
                branch.oid,
+
                None,
                &[],
                &alice.signer,
            )
modified crates/radicle/src/cob/patch/cache.rs
@@ -11,11 +11,12 @@ use crate::cob::store;
use crate::cob::{Label, ObjectId, TypeName};
use crate::git;
use crate::node::device::Device;
+
use crate::node::NodeId;
use crate::prelude::RepoId;
use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};

use super::{
-
    ByRevision, MergeTarget, NodeId, Patch, PatchCounts, PatchId, PatchMut, Revision, RevisionId,
+
    diff, ByRevision, MergeTarget, Patch, PatchCounts, PatchId, PatchMut, Revision, RevisionId,
    State, Status,
};

@@ -115,6 +116,7 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
        target: MergeTarget,
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
+
        opts: Option<diff::Options>,
        labels: &[Label],
        signer: &Device<G>,
    ) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
@@ -129,6 +131,7 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
            target,
            base,
            oid,
+
            opts,
            labels,
            &mut self.cache,
            signer,
@@ -145,6 +148,7 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
        target: MergeTarget,
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
+
        opts: Option<diff::Options>,
        labels: &[Label],
        signer: &Device<G>,
    ) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
@@ -159,6 +163,7 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
            target,
            base,
            oid,
+
            opts,
            labels,
            &mut self.cache,
            signer,
@@ -714,7 +719,8 @@ mod tests {
    use crate::cob::thread::{Comment, Thread};
    use crate::cob::Author;
    use crate::patch::{
-
        ByRevision, MergeTarget, Patch, PatchCounts, PatchId, Revision, RevisionId, State, Status,
+
        ByRevision, CreatePatchRequest, Patch, PatchCounts, PatchId, Revision, RevisionId, State,
+
        Status,
    };
    use crate::prelude::Did;
    use crate::profile::env;
@@ -767,13 +773,15 @@ mod tests {
        let mut cache = memory(repo);
        assert!(cache.is_empty().unwrap());

-
        let patch = Patch::new("Patch #1".to_string(), MergeTarget::Delegates, revision());
+
        let (id, rev) = revision();
+
        let patch = Patch::new(CreatePatchRequest::new("Patch #1".to_string(), id, rev));
        let id = ObjectId::from_str("47799cbab2eca047b6520b9fce805da42b49ecab").unwrap();
        cache.update(&cache.rid(), &id, &patch).unwrap();

+
        let (id, rev) = revision();
        let patch = Patch {
            state: State::Archived,
-
            ..Patch::new("Patch #2".to_string(), MergeTarget::Delegates, revision())
+
            ..Patch::new(CreatePatchRequest::new("Patch #2".to_string(), id, rev))
        };
        let id = ObjectId::from_str("ae981ded6ed2ed2cdba34c8603714782667f18a3").unwrap();
        cache.update(&cache.rid(), &id, &patch).unwrap();
@@ -803,16 +811,18 @@ mod tests {
            .collect::<BTreeSet<PatchId>>();

        for id in open_ids.iter() {
-
            let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
+
            let (rev_id, rev) = revision();
+
            let patch = Patch::new(CreatePatchRequest::new(id.to_string(), rev_id, rev));
            cache
                .update(&cache.rid(), &PatchId::from(*id), &patch)
                .unwrap();
        }

        for id in draft_ids.iter() {
+
            let (rev_id, rev) = revision();
            let patch = Patch {
                state: State::Draft,
-
                ..Patch::new(id.to_string(), MergeTarget::Delegates, revision())
+
                ..Patch::new(CreatePatchRequest::new(id.to_string(), rev_id, rev))
            };
            cache
                .update(&cache.rid(), &PatchId::from(*id), &patch)
@@ -820,9 +830,10 @@ mod tests {
        }

        for id in archived_ids.iter() {
+
            let (rev_id, rev) = revision();
            let patch = Patch {
                state: State::Archived,
-
                ..Patch::new(id.to_string(), MergeTarget::Delegates, revision())
+
                ..Patch::new(CreatePatchRequest::new(id.to_string(), rev_id, rev))
            };
            cache
                .update(&cache.rid(), &PatchId::from(*id), &patch)
@@ -830,12 +841,13 @@ mod tests {
        }

        for id in merged_ids.iter() {
+
            let (rev_id, rev) = revision();
            let patch = Patch {
                state: State::Merged {
                    revision: arbitrary::oid().into(),
                    commit: arbitrary::oid(),
                },
-
                ..Patch::new(id.to_string(), MergeTarget::Delegates, revision())
+
                ..Patch::new(CreatePatchRequest::new(id.to_string(), rev_id, rev))
            };
            cache
                .update(&cache.rid(), &PatchId::from(*id), &patch)
@@ -869,7 +881,8 @@ mod tests {
        let mut patches = Vec::with_capacity(ids.len());

        for id in ids.iter() {
-
            let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
+
            let (rev_id, rev) = revision();
+
            let patch = Patch::new(CreatePatchRequest::new(id.to_string(), rev_id, rev));
            cache
                .update(&cache.rid(), &PatchId::from(*id), &patch)
                .unwrap();
@@ -897,11 +910,11 @@ mod tests {
            .iter()
            .next()
            .expect("at least one revision should have been created");
-
        let mut patch = Patch::new(
+
        let mut patch = Patch::new(CreatePatchRequest::new(
            patch_id.to_string(),
-
            MergeTarget::Delegates,
-
            (*rev_id, rev.clone()),
-
        );
+
            *rev_id,
+
            rev.clone(),
+
        ));
        let timeline = revisions.keys().copied().collect::<Vec<_>>();
        patch
            .timeline
@@ -937,7 +950,8 @@ mod tests {
        let mut patches = Vec::with_capacity(ids.len());

        for id in ids.iter() {
-
            let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
+
            let (rev_id, rev) = revision();
+
            let patch = Patch::new(CreatePatchRequest::new(id.to_string(), rev_id, rev));
            cache
                .update(&cache.rid(), &PatchId::from(*id), &patch)
                .unwrap();
@@ -964,7 +978,8 @@ mod tests {
        let mut patches = Vec::with_capacity(ids.len());

        for id in ids.iter() {
-
            let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
+
            let (rev_id, rev) = revision();
+
            let patch = Patch::new(CreatePatchRequest::new(id.to_string(), rev_id, rev));
            cache
                .update(&cache.rid(), &PatchId::from(*id), &patch)
                .unwrap();
@@ -990,7 +1005,8 @@ mod tests {
            .collect::<BTreeSet<PatchId>>();

        for id in ids.iter() {
-
            let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
+
            let (rev_id, rev) = revision();
+
            let patch = Patch::new(CreatePatchRequest::new(id.to_string(), rev_id, rev));
            cache
                .update(&cache.rid(), &PatchId::from(*id), &patch)
                .unwrap();
modified crates/radicle/src/cob/test.rs
@@ -237,6 +237,7 @@ impl<G: Signer> Actor<G> {
                    base,
                    oid,
                    resolves: Default::default(),
+
                    diff_options: None,
                },
                patch::Action::Edit {
                    title: title.to_string(),
deleted radicle/src/cob/patch.rs
@@ -1,3736 +0,0 @@
-
<<<<<<< Conflict 1 of 1
-
%%%%%%% Changes from base to side #2
-
 pub mod cache;
-
 pub mod diff;
-
 
-
 use std::collections::btree_map;
-
 use std::collections::{BTreeMap, BTreeSet, HashMap};
-
 use std::fmt;
-
 use std::ops::Deref;
-
 use std::str::FromStr;
-
 
-
 use amplify::Wrapper;
-
 use nonempty::NonEmpty;
-
 use once_cell::sync::Lazy;
-
 use serde::{Deserialize, Serialize};
-
 use storage::{HasRepoId, RepositoryError};
-
 use thiserror::Error;
-
 
-
 use crate::cob;
-
-use crate::cob::common::{Author, Authorization, CodeLocation, Label, Reaction, Timestamp};
-
+use crate::cob::common::{
-
+    Author, Authorization, DiffLocation, Label, PartialLocation, Reaction, Timestamp,
-
+};
-
 use crate::cob::store::Transaction;
-
 use crate::cob::store::{Cob, CobAction};
-
 use crate::cob::thread;
-
 use crate::cob::thread::Thread;
-
 use crate::cob::thread::{Comment, CommentId, Edit, Reactions};
-
 use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName, Uri};
-
 use crate::crypto::{PublicKey, Signer};
-
 use crate::git;
-
 use crate::identity::doc::{DocAt, DocError};
-
 use crate::identity::PayloadError;
-
 use crate::prelude::*;
-
 use crate::storage;
-
 
-
 pub use cache::Cache;
-
 
-
 /// Type name of a patch.
-
 pub static TYPENAME: Lazy<TypeName> =
-
     Lazy::new(|| FromStr::from_str("xyz.radicle.patch").expect("type name is valid"));
-
 
-
 /// Patch operation.
-
 pub type Op = cob::Op<Action>;
-
 
-
 /// Identifier for a patch.
-
 pub type PatchId = ObjectId;
-
 
-
 /// Unique identifier for a patch revision.
-
 #[derive(
-
     Wrapper,
-
     Debug,
-
     Clone,
-
     Copy,
-
     Serialize,
-
     Deserialize,
-
     PartialEq,
-
     Eq,
-
     PartialOrd,
-
     Ord,
-
     Hash,
-
     From,
-
     Display,
-
 )]
-
 #[display(inner)]
-
 #[wrap(Deref)]
-
 pub struct RevisionId(EntryId);
-
 
-
 /// Unique identifier for a patch review.
-
 #[derive(
-
     Wrapper,
-
     Debug,
-
     Clone,
-
     Copy,
-
     Serialize,
-
     Deserialize,
-
     PartialEq,
-
     Eq,
-
     PartialOrd,
-
     Ord,
-
     Hash,
-
     From,
-
     Display,
-
 )]
-
 #[display(inner)]
-
 #[wrapper(Deref)]
-
 pub struct ReviewId(EntryId);
-
 
-
 /// Index of a revision in the revisions list.
-
 pub type RevisionIx = usize;
-
 
-
 /// Error applying an operation onto a state.
-
 #[derive(Debug, Error)]
-
 pub enum Error {
-
     /// Causal dependency missing.
-
     ///
-
     /// This error indicates that the operations are not being applied
-
     /// in causal order, which is a requirement for this CRDT.
-
     ///
-
     /// For example, this can occur if an operation references another operation
-
     /// that hasn't happened yet.
-
     #[error("causal dependency {0:?} missing")]
-
     Missing(EntryId),
-
     /// Error applying an op to the patch thread.
-
     #[error("thread apply failed: {0}")]
-
     Thread(#[from] thread::Error),
-
     /// Error loading the identity document committed to by an operation.
-
     #[error("identity doc failed to load: {0}")]
-
     Doc(#[from] DocError),
-
     /// Identity document is missing.
-
     #[error("missing identity document")]
-
     MissingIdentity,
-
     /// Review is empty.
-
     #[error("empty review; verdict or summary not provided")]
-
     EmptyReview,
-
     /// Duplicate review.
-
     #[error("review {0} of {1} already exists by author {2}")]
-
     DuplicateReview(ReviewId, RevisionId, NodeId),
-
     /// Error loading the document payload.
-
     #[error("payload failed to load: {0}")]
-
     Payload(#[from] PayloadError),
-
     /// Git error.
-
     #[error("git: {0}")]
-
     Git(#[from] git::ext::Error),
-
     /// Store error.
-
     #[error("store: {0}")]
-
     Store(#[from] store::Error),
-
     #[error("op decoding failed: {0}")]
-
     Op(#[from] op::OpEncodingError),
-
     /// Action not authorized by the author
-
     #[error("{0} not authorized to apply {1:?}")]
-
     NotAuthorized(ActorId, Action),
-
     /// An illegal action.
-
     #[error("action is not allowed: {0}")]
-
     NotAllowed(EntryId),
-
     /// Revision not found.
-
     #[error("revision not found: {0}")]
-
     RevisionNotFound(RevisionId),
-
     /// Initialization failed.
-
     #[error("initialization failed: {0}")]
-
     Init(&'static str),
-
     #[error("failed to update patch {id} in cache: {err}")]
-
     CacheUpdate {
-
         id: PatchId,
-
         #[source]
-
         err: Box<dyn std::error::Error + Send + Sync + 'static>,
-
     },
-
     #[error("failed to remove patch {id} from cache: {err}")]
-
     CacheRemove {
-
         id: PatchId,
-
         #[source]
-
         err: Box<dyn std::error::Error + Send + Sync + 'static>,
-
     },
-
     #[error("failed to remove patches from cache: {err}")]
-
     CacheRemoveAll {
-
         #[source]
-
         err: Box<dyn std::error::Error + Send + Sync + 'static>,
-
     },
-
 }
-
 
-
 /// Patch operation.
-
 #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-
 #[serde(tag = "type", rename_all = "camelCase")]
-
 pub enum Action {
-
     //
-
     // Actions on patch.
-
     //
-
     #[serde(rename = "edit")]
-
     Edit { title: String, target: MergeTarget },
-
     #[serde(rename = "label")]
-
     Label { labels: BTreeSet<Label> },
-
     #[serde(rename = "lifecycle")]
-
     Lifecycle { state: Lifecycle },
-
     #[serde(rename = "assign")]
-
     Assign { assignees: BTreeSet<Did> },
-
     #[serde(rename = "merge")]
-
     Merge {
-
         revision: RevisionId,
-
         commit: git::Oid,
-
     },
-
 
-
     //
-
     // Review actions
-
     //
-
     #[serde(rename = "review")]
-
     Review {
-
         revision: RevisionId,
-
         #[serde(default, skip_serializing_if = "Option::is_none")]
-
         summary: Option<String>,
-
         #[serde(default, skip_serializing_if = "Option::is_none")]
-
         verdict: Option<Verdict>,
-
         #[serde(default, skip_serializing_if = "Vec::is_empty")]
-
         labels: Vec<Label>,
-
     },
-
     #[serde(rename = "review.edit")]
-
     ReviewEdit {
-
         review: ReviewId,
-
         #[serde(default, skip_serializing_if = "Option::is_none")]
-
         summary: Option<String>,
-
         #[serde(default, skip_serializing_if = "Option::is_none")]
-
         verdict: Option<Verdict>,
-
         #[serde(default, skip_serializing_if = "Vec::is_empty")]
-
         labels: Vec<Label>,
-
     },
-
     #[serde(rename = "review.redact")]
-
     ReviewRedact { review: ReviewId },
-
+    /// **DEPRECATED**
-
+    ///
-
+    /// We do not construct this variant anymore. Use [`Action::ReviewComment`]
-
+    /// instead.
-
     #[serde(rename = "review.comment")]
-
+    ReviewCommentV1(ReviewComment),
-
+    #[serde(rename = "review.comment.v2")]
-
     ReviewComment {
-
         review: ReviewId,
-
         body: String,
-
         #[serde(default, skip_serializing_if = "Option::is_none")]
-
-        location: Option<CodeLocation>,
-
+        location: Option<DiffLocation>,
-
         /// Comment this is a reply to.
-
         /// Should be [`None`] if it's the first comment.
-
         /// Should be [`Some`] otherwise.
-
         #[serde(default, skip_serializing_if = "Option::is_none")]
-
         reply_to: Option<CommentId>,
-
         /// Embeded content.
-
         #[serde(default, skip_serializing_if = "Vec::is_empty")]
-
         embeds: Vec<Embed<Uri>>,
-
     },
-
     #[serde(rename = "review.comment.edit")]
-
     ReviewCommentEdit {
-
         review: ReviewId,
-
         comment: EntryId,
-
         body: String,
-
         embeds: Vec<Embed<Uri>>,
-
     },
-
     #[serde(rename = "review.comment.redact")]
-
     ReviewCommentRedact { review: ReviewId, comment: EntryId },
-
     #[serde(rename = "review.comment.react")]
-
     ReviewCommentReact {
-
         review: ReviewId,
-
         comment: EntryId,
-
         reaction: Reaction,
-
         active: bool,
-
     },
-
     #[serde(rename = "review.comment.resolve")]
-
     ReviewCommentResolve { review: ReviewId, comment: EntryId },
-
     #[serde(rename = "review.comment.unresolve")]
-
     ReviewCommentUnresolve { review: ReviewId, comment: EntryId },
-
 
-
     //
-
     // Revision actions
-
     //
-
     #[serde(rename = "revision")]
-
     Revision {
-
         description: String,
-
         base: git::Oid,
-
         oid: git::Oid,
-
         /// Review comments resolved by this revision.
-
         #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
-
         resolves: BTreeSet<(EntryId, CommentId)>,
-
     },
-
     #[serde(rename = "revision.edit")]
-
     RevisionEdit {
-
         revision: RevisionId,
-
         description: String,
-
         /// Embeded content.
-
         #[serde(default, skip_serializing_if = "Vec::is_empty")]
-
         embeds: Vec<Embed<Uri>>,
-
     },
-
+    /// **DEPRECATED**
-
+    ///
-
+    /// We do not construct this variant anymore. Use
-
+    /// [`Action::RevisionReact`] instead.
-
+    #[serde(rename = "revision.react")]
-
+    RevisionReactV1(RevisionReact),
-
     /// React to the revision.
-
-    #[serde(rename = "revision.react")]
-
+    #[serde(rename = "revision.react.v2")]
-
     RevisionReact {
-
         revision: RevisionId,
-
         #[serde(default, skip_serializing_if = "Option::is_none")]
-
-        location: Option<CodeLocation>,
-
+        location: Option<DiffLocation>,
-
         reaction: Reaction,
-
         active: bool,
-
     },
-
     #[serde(rename = "revision.redact")]
-
     RevisionRedact { revision: RevisionId },
-
-    #[serde(rename_all = "camelCase")]
-
+    /// **DEPRECATED**
-
+    ///
-
+    /// We do not construct this variant anymore. Use
-
+    /// [`Action::RevisionComment`] instead.
-
     #[serde(rename = "revision.comment")]
-
+    RevisionCommentV1(RevisionComment),
-
+    #[serde(rename = "revision.comment.v2")]
-
     RevisionComment {
-
         /// The revision to comment on.
-
         revision: RevisionId,
-
         /// For comments on the revision code.
-
         #[serde(default, skip_serializing_if = "Option::is_none")]
-
-        location: Option<CodeLocation>,
-
+        location: Option<DiffLocation>,
-
         /// Comment body.
-
         body: String,
-
         /// Comment this is a reply to.
-
         /// Should be [`None`] if it's the top-level comment.
-
         /// Should be the root [`CommentId`] if it's a top-level comment.
-
         #[serde(default, skip_serializing_if = "Option::is_none")]
-
         reply_to: Option<CommentId>,
-
         /// Embeded content.
-
         #[serde(default, skip_serializing_if = "Vec::is_empty")]
-
         embeds: Vec<Embed<Uri>>,
-
     },
-
     /// Edit a revision comment.
-
     #[serde(rename = "revision.comment.edit")]
-
     RevisionCommentEdit {
-
         revision: RevisionId,
-
         comment: CommentId,
-
         body: String,
-
         embeds: Vec<Embed<Uri>>,
-
     },
-
     /// Redact a revision comment.
-
     #[serde(rename = "revision.comment.redact")]
-
     RevisionCommentRedact {
-
         revision: RevisionId,
-
         comment: CommentId,
-
     },
-
     /// React to a revision comment.
-
     #[serde(rename = "revision.comment.react")]
-
     RevisionCommentReact {
-
         revision: RevisionId,
-
         comment: CommentId,
-
         reaction: Reaction,
-
         active: bool,
-
     },
-
 }
-
 
-
+/// Deliberately cannot be constructed outside of this module, so that it is no
-
+/// longer used.
-

-
+
-
+/// Deliberately cannot be constructed outside of this module, so that it is no
-
+/// longer used.
-
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-
+#[serde(rename_all = "camelCase")]
-
+pub struct RevisionComment {
-
+    /// The revision to comment on.
-
+    revision: RevisionId,
-
+    /// For comments on the revision code.
-
+    #[serde(default, skip_serializing_if = "Option::is_none")]
-
+    location: Option<PartialLocation>,
-
+    /// Comment body.
-
+    body: String,
-
+    /// Comment this is a reply to.
-
+    /// Should be [`None`] if it's the top-level comment.
-
+    /// Should be the root [`CommentId`] if it's a top-level comment.
-
+    #[serde(default, skip_serializing_if = "Option::is_none")]
-
+    reply_to: Option<CommentId>,
-
+    /// Embeded content.
-
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
-
+    embeds: Vec<Embed<Uri>>,
-
+}
-
+
-
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-
+#[serde(rename_all = "camelCase")]
-
+pub struct RevisionReact {
-
+    revision: RevisionId,
-
+    #[serde(default, skip_serializing_if = "Option::is_none")]
-
+    location: Option<PartialLocation>,
-
+    reaction: Reaction,
-
+    active: bool,
-
+}
-
+
-
 impl CobAction for Action {
-
     fn parents(&self) -> Vec<git::Oid> {
-
         match self {
-
             Self::Revision { base, oid, .. } => {
-
                 vec![*base, *oid]
-
             }
-
             Self::Merge { commit, .. } => {
-
                 vec![*commit]
-
             }
-
             _ => vec![],
-
         }
-
     }
-
 
-
     fn produces_identifier(&self) -> bool {
-
         matches!(
-
             self,
-
             Self::Revision { .. }
-
                 | Self::RevisionComment { .. }
-
                 | Self::Review { .. }
-
                 | Self::ReviewComment { .. }
-
         )
-
     }
-
 }
-
 
-
 /// Output of a merge.
-
 #[derive(Debug)]
-
 #[must_use]
-
 pub struct Merged<'a, R> {
-
     pub patch: PatchId,
-
     pub entry: EntryId,
-
 
-
     stored: &'a R,
-
 }
-
 
-
 impl<R: WriteRepository> Merged<'_, R> {
-
     /// Cleanup after merging a patch.
-
     ///
-
     /// This removes Git refs relating to the patch, both in the working copy,
-
     /// and the stored copy; and updates `rad/sigrefs`.
-
     pub fn cleanup<G: Signer>(
-
         self,
-
         working: &git::raw::Repository,
-
         signer: &G,
-
     ) -> Result<(), storage::RepositoryError> {
-
         let nid = signer.public_key();
-
         let stored_ref = git::refs::patch(&self.patch).with_namespace(nid.into());
-
         let working_ref = git::refs::workdir::patch_upstream(&self.patch);
-
 
-
         working
-
             .find_reference(&working_ref)
-
             .and_then(|mut r| r.delete())
-
             .ok();
-
 
-
         self.stored
-
             .raw()
-
             .find_reference(&stored_ref)
-
             .and_then(|mut r| r.delete())
-
             .ok();
-
         self.stored.sign_refs(signer)?;
-
 
-
         Ok(())
-
     }
-
 }
-
 
-
 /// Where a patch is intended to be merged.
-
 #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-
 #[serde(rename_all = "camelCase")]
-
 pub enum MergeTarget {
-
     /// Intended for the default branch of the project delegates.
-
     /// Note that if the delegations change while the patch is open,
-
     /// this will always mean whatever the "current" delegation set is.
-
     /// If it were otherwise, patches could become un-mergeable.
-
     #[default]
-
     Delegates,
-
 }
-
 
-
 impl MergeTarget {
-
     /// Get the head of the target branch.
-
     pub fn head<R: ReadRepository>(&self, repo: &R) -> Result<git::Oid, RepositoryError> {
-
         match self {
-
             MergeTarget::Delegates => {
-
                 let (_, target) = repo.head()?;
-
                 Ok(target)
-
             }
-
         }
-
     }
-
 }
-
 
-
 /// Patch state.
-
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
 #[serde(rename_all = "camelCase")]
-
 pub struct Patch {
-
     /// Title of the patch.
-
     pub(super) title: String,
-
     /// Patch author.
-
     pub(super) author: Author,
-
     /// Current state of the patch.
-
     pub(super) state: State,
-
     /// Target this patch is meant to be merged in.
-
     pub(super) target: MergeTarget,
-
     /// Associated labels.
-
     /// Labels can be added and removed at will.
-
     pub(super) labels: BTreeSet<Label>,
-
     /// Patch merges.
-
     ///
-
     /// Only one merge is allowed per user.
-
     ///
-
     /// Merges can be removed and replaced, but not modified. Generally, once a revision is merged,
-
     /// it stays that way. Being able to remove merges may be useful in case of force updates
-
     /// on the target branch.
-
     pub(super) merges: BTreeMap<ActorId, Merge>,
-
     /// List of patch revisions. The initial changeset is part of the
-
     /// first revision.
-
     ///
-
     /// Revisions can be redacted, but are otherwise immutable.
-
     pub(super) revisions: BTreeMap<RevisionId, Option<Revision>>,
-
     /// Users assigned to review this patch.
-
     pub(super) assignees: BTreeSet<ActorId>,
-
     /// Timeline of operations.
-
     pub(super) timeline: Vec<EntryId>,
-
     /// Reviews index. Keeps track of reviews for better performance.
-
     pub(super) reviews: BTreeMap<ReviewId, Option<(RevisionId, ActorId)>>,
-
 }
-
 
-
 impl Patch {
-
     /// Construct a new patch object from a revision.
-
     pub fn new(title: String, target: MergeTarget, (id, revision): (RevisionId, Revision)) -> Self {
-
         Self {
-
             title,
-
             author: revision.author.clone(),
-
             state: State::default(),
-
             target,
-
             labels: BTreeSet::default(),
-
             merges: BTreeMap::default(),
-
             revisions: BTreeMap::from_iter([(id, Some(revision))]),
-
             assignees: BTreeSet::default(),
-
             timeline: vec![id.into_inner()],
-
             reviews: BTreeMap::default(),
-
         }
-
     }
-
 
-
     /// Title of the patch.
-
     pub fn title(&self) -> &str {
-
         self.title.as_str()
-
     }
-
 
-
     /// Current state of the patch.
-
     pub fn state(&self) -> &State {
-
         &self.state
-
     }
-
 
-
     /// Target this patch is meant to be merged in.
-
     pub fn target(&self) -> MergeTarget {
-
         self.target
-
     }
-
 
-
     /// Timestamp of the first revision of the patch.
-
     pub fn timestamp(&self) -> Timestamp {
-
         self.updates()
-
             .next()
-
             .map(|(_, r)| r)
-
             .expect("Patch::timestamp: at least one revision is present")
-
             .timestamp
-
     }
-
 
-
     /// Associated labels.
-
     pub fn labels(&self) -> impl Iterator<Item = &Label> {
-
         self.labels.iter()
-
     }
-
 
-
     /// Patch description.
-
     pub fn description(&self) -> &str {
-
         let (_, r) = self.root();
-
         r.description()
-
     }
-
 
-
     /// Patch embeds.
-
     pub fn embeds(&self) -> &[Embed<Uri>] {
-
         let (_, r) = self.root();
-
         r.embeds()
-
     }
-
 
-
     /// Author of the first revision of the patch.
-
     pub fn author(&self) -> &Author {
-
         &self.author
-
     }
-
 
-
     /// All revision authors.
-
     pub fn authors(&self) -> BTreeSet<&Author> {
-
         self.revisions
-
             .values()
-
             .filter_map(|r| r.as_ref())
-
             .map(|r| &r.author)
-
             .collect()
-
     }
-
 
-
     /// Get the `Revision` by its `RevisionId`.
-
     ///
-
     /// None is returned if the `Revision` has been redacted (deleted).
-
     pub fn revision(&self, id: &RevisionId) -> Option<&Revision> {
-
         self.revisions.get(id).and_then(|o| o.as_ref())
-
     }
-
 
-
     /// List of patch revisions by the patch author. The initial changeset is part of the
-
     /// first revision.
-
     pub fn updates(&self) -> impl DoubleEndedIterator<Item = (RevisionId, &Revision)> {
-
         self.revisions_by(self.author().public_key())
-
     }
-
 
-
     /// List of all patch revisions by all authors.
-
     pub fn revisions(&self) -> impl DoubleEndedIterator<Item = (RevisionId, &Revision)> {
-
         self.timeline.iter().filter_map(move |id| {
-
             self.revisions
-
                 .get(id)
-
                 .and_then(|o| o.as_ref())
-
                 .map(|rev| (RevisionId(*id), rev))
-
         })
-
     }
-
 
-
     /// List of patch revisions by the given author.
-
     pub fn revisions_by<'a>(
-
         &'a self,
-
         author: &'a PublicKey,
-
     ) -> impl DoubleEndedIterator<Item = (RevisionId, &'a Revision)> {
-
         self.revisions()
-
             .filter(move |(_, r)| (r.author.public_key() == author))
-
     }
-
 
-
     /// List of patch reviews of the given revision.
-
     pub fn reviews_of(&self, rev: RevisionId) -> impl Iterator<Item = (&ReviewId, &Review)> {
-
         self.reviews.iter().filter_map(move |(review_id, t)| {
-
             t.and_then(|(rev_id, pk)| {
-
                 if rev == rev_id {
-
                     self.revision(&rev_id)
-
                         .and_then(|r| r.review_by(&pk))
-
                         .map(|r| (review_id, r))
-
                 } else {
-
                     None
-
                 }
-
             })
-
         })
-
     }
-
 
-
     /// List of patch assignees.
-
     pub fn assignees(&self) -> impl Iterator<Item = Did> + '_ {
-
         self.assignees.iter().map(Did::from)
-
     }
-
 
-
     /// Get the merges.
-
     pub fn merges(&self) -> impl Iterator<Item = (&ActorId, &Merge)> {
-
         self.merges.iter()
-
     }
-
 
-
     /// Reference to the Git object containing the code on the latest revision.
-
     pub fn head(&self) -> &git::Oid {
-
         &self.latest().1.oid
-
     }
-
 
-
     /// Get the commit of the target branch on which this patch is based.
-
     /// This can change via a patch update.
-
     pub fn base(&self) -> &git::Oid {
-
         &self.latest().1.base
-
     }
-
 
-
     /// Get the merge base of this patch.
-
     pub fn merge_base<R: ReadRepository>(&self, repo: &R) -> Result<git::Oid, git::ext::Error> {
-
         repo.merge_base(self.base(), self.head())
-
     }
-
 
-
     /// Get the commit range of this patch.
-
     pub fn range(&self) -> Result<(git::Oid, git::Oid), git::ext::Error> {
-
         Ok((*self.base(), *self.head()))
-
     }
-
 
-
     /// Index of latest revision in the revisions list.
-
     pub fn version(&self) -> RevisionIx {
-
         self.revisions
-
             .len()
-
             .checked_sub(1)
-
             .expect("Patch::version: at least one revision is present")
-
     }
-
 
-
     /// Root revision.
-
     ///
-
     /// This is the revision that was created with the patch.
-
     pub fn root(&self) -> (RevisionId, &Revision) {
-
         self.updates()
-
             .next()
-
             .expect("Patch::root: there is always a root revision")
-
     }
-
 
-
     /// Latest revision by the patch author.
-
     pub fn latest(&self) -> (RevisionId, &Revision) {
-
         self.latest_by(self.author().public_key())
-
             .expect("Patch::latest: there is always at least one revision")
-
     }
-
 
-
     /// Latest revision by the given author.
-
     pub fn latest_by<'a>(&'a self, author: &'a PublicKey) -> Option<(RevisionId, &'a Revision)> {
-
         self.revisions_by(author).next_back()
-
     }
-
 
-
     /// Time of last update.
-
     pub fn updated_at(&self) -> Timestamp {
-
         self.latest().1.timestamp()
-
     }
-
 
-
     /// Check if the patch is merged.
-
     pub fn is_merged(&self) -> bool {
-
         matches!(self.state(), State::Merged { .. })
-
     }
-
 
-
     /// Check if the patch is open.
-
     pub fn is_open(&self) -> bool {
-
         matches!(self.state(), State::Open { .. })
-
     }
-
 
-
     /// Check if the patch is archived.
-
     pub fn is_archived(&self) -> bool {
-
         matches!(self.state(), State::Archived)
-
     }
-
 
-
     /// Check if the patch is a draft.
-
     pub fn is_draft(&self) -> bool {
-
         matches!(self.state(), State::Draft)
-
     }
-
 
-
     /// Apply authorization rules on patch actions.
-
     pub fn authorization(
-
         &self,
-
         action: &Action,
-
         actor: &ActorId,
-
         doc: &Doc,
-
     ) -> Result<Authorization, Error> {
-
         if doc.is_delegate(&actor.into()) {
-
             // A delegate is authorized to do all actions.
-
             return Ok(Authorization::Allow);
-
         }
-
         let author = self.author().id().as_key();
-
         let outcome = match action {
-
             // The patch author can edit the patch and change its state.
-
             Action::Edit { .. } => Authorization::from(actor == author),
-
             Action::Lifecycle { state } => Authorization::from(match state {
-
                 Lifecycle::Open { .. } => actor == author,
-
                 Lifecycle::Draft { .. } => actor == author,
-
                 Lifecycle::Archived { .. } => actor == author,
-
             }),
-
             // Only delegates can carry out these actions.
-
             Action::Label { labels } => {
-
                 if labels == &self.labels {
-
                     // No-op is allowed for backwards compatibility.
-
                     Authorization::Allow
-
                 } else {
-
                     Authorization::Deny
-
                 }
-
             }
-
             Action::Assign { .. } => Authorization::Deny,
-
             Action::Merge { .. } => match self.target() {
-
                 MergeTarget::Delegates => Authorization::Deny,
-
             },
-
             // Anyone can submit a review.
-
             Action::Review { .. } => Authorization::Allow,
-
             Action::ReviewRedact { review, .. } | Action::ReviewEdit { review, .. } => {
-
                 if let Some((_, review)) = lookup::review(self, review)? {
-
                     Authorization::from(actor == review.author.public_key())
-
                 } else {
-
                     // Redacted.
-
                     Authorization::Unknown
-
                 }
-
             }
-
             // Anyone can comment on a review.
-
+            Action::ReviewCommentV1 { .. } => Authorization::Allow,
-
             Action::ReviewComment { .. } => Authorization::Allow,
-
             // The comment author can edit and redact their own comment.
-
             Action::ReviewCommentEdit {
-
                 review, comment, ..
-
             }
-
             | Action::ReviewCommentRedact { review, comment } => {
-
                 if let Some((_, review)) = lookup::review(self, review)? {
-
                     if let Some(comment) = review.comments.comment(comment) {
-
                         return Ok(Authorization::from(*actor == comment.author()));
-
                     }
-
                 }
-
                 // Redacted.
-
                 Authorization::Unknown
-
             }
-
             // Anyone can react to a review comment.
-
             Action::ReviewCommentReact { .. } => Authorization::Allow,
-
             // The reviewer, commenter or revision author can resolve and unresolve review comments.
-
             Action::ReviewCommentResolve { review, comment }
-
             | Action::ReviewCommentUnresolve { review, comment } => {
-
                 if let Some((revision, review)) = lookup::review(self, review)? {
-
                     if let Some(comment) = review.comments.comment(comment) {
-
                         return Ok(Authorization::from(
-
                             actor == &comment.author()
-
                                 || actor == review.author.public_key()
-
                                 || actor == revision.author.public_key(),
-
                         ));
-
                     }
-
                 }
-
                 // Redacted.
-
                 Authorization::Unknown
-
             }
-
             // Anyone can propose revisions.
-
             Action::Revision { .. } => Authorization::Allow,
-
             // Only the revision author can edit or redact their revision.
-
             Action::RevisionEdit { revision, .. } | Action::RevisionRedact { revision, .. } => {
-
                 if let Some(revision) = lookup::revision(self, revision)? {
-
                     Authorization::from(actor == revision.author.public_key())
-
                 } else {
-
                     // Redacted.
-
                     Authorization::Unknown
-
                 }
-
             }
-
             // Anyone can react to or comment on a revision.
-
+            Action::RevisionReactV1 { .. } => Authorization::Allow,
-
             Action::RevisionReact { .. } => Authorization::Allow,
-
+            Action::RevisionCommentV1 { .. } => Authorization::Allow,
-
             Action::RevisionComment { .. } => Authorization::Allow,
-
             // Only the comment author can edit or redact their comment.
-
             Action::RevisionCommentEdit {
-
                 revision, comment, ..
-
             }
-
             | Action::RevisionCommentRedact {
-
                 revision, comment, ..
-
             } => {
-
                 if let Some(revision) = lookup::revision(self, revision)? {
-
                     if let Some(comment) = revision.discussion.comment(comment) {
-
                         return Ok(Authorization::from(actor == &comment.author()));
-
                     }
-
                 }
-
                 // Redacted.
-
                 Authorization::Unknown
-
             }
-
             // Anyone can react to a revision.
-
             Action::RevisionCommentReact { .. } => Authorization::Allow,
-
         };
-
         Ok(outcome)
-
     }
-
 }
-
 
-
 impl Patch {
-
     /// Apply an action after checking if it's authorized.
-
     fn op_action<R: ReadRepository>(
-
         &mut self,
-
         action: Action,
-
         id: EntryId,
-
         author: ActorId,
-
         timestamp: Timestamp,
-
         concurrent: &[&cob::Entry],
-
         doc: &DocAt,
-
         repo: &R,
-
     ) -> Result<(), Error> {
-
         match self.authorization(&action, &author, doc)? {
-
             Authorization::Allow => {
-
                 self.action(action, id, author, timestamp, concurrent, doc, repo)
-
             }
-
             Authorization::Deny => Err(Error::NotAuthorized(author, action)),
-
             Authorization::Unknown => {
-
                 // In this case, since there is not enough information to determine
-
                 // whether the action is authorized or not, we simply ignore it.
-
                 // It's likely that the target object was redacted, and we can't
-
                 // verify whether the action would have been allowed or not.
-
                 Ok(())
-
             }
-
         }
-
     }
-
 
-
     /// Apply a single action to the patch.
-
     fn action<R: ReadRepository>(
-
         &mut self,
-
         action: Action,
-
         entry: EntryId,
-
         author: ActorId,
-
         timestamp: Timestamp,
-
         _concurrent: &[&cob::Entry],
-
         identity: &Doc,
-
         repo: &R,
-
     ) -> Result<(), Error> {
-
         match action {
-
             Action::Edit { title, target } => {
-
                 self.title = title;
-
                 self.target = target;
-
             }
-
             Action::Lifecycle { state } => {
-
                 let valid = self.state == State::Draft
-
                     || self.state == State::Archived
-
                     || self.state == State::Open { conflicts: vec![] };
-
 
-
                 if valid {
-
                     match state {
-
                         Lifecycle::Open => {
-
                             self.state = State::Open { conflicts: vec![] };
-
                         }
-
                         Lifecycle::Draft => {
-
                             self.state = State::Draft;
-
                         }
-
                         Lifecycle::Archived => {
-
                             self.state = State::Archived;
-
                         }
-
                     }
-
                 }
-
             }
-
             Action::Label { labels } => {
-
                 self.labels = BTreeSet::from_iter(labels);
-
             }
-
             Action::Assign { assignees } => {
-
                 self.assignees = BTreeSet::from_iter(assignees.into_iter().map(ActorId::from));
-
             }
-
             Action::RevisionEdit {
-
                 revision,
-
                 description,
-
                 embeds,
-
             } => {
-
                 if let Some(redactable) = self.revisions.get_mut(&revision) {
-
                     // If the revision was redacted concurrently, there's nothing to do.
-
                     if let Some(revision) = redactable {
-
                         revision.description.push(Edit::new(
-
                             author,
-
                             description,
-
                             timestamp,
-
                             embeds,
-
                         ));
-
                     }
-
                 } else {
-
                     return Err(Error::Missing(revision.into_inner()));
-
                 }
-
             }
-
             Action::Revision {
-
                 description,
-
                 base,
-
                 oid,
-
                 resolves,
-
             } => {
-
                 debug_assert!(!self.revisions.contains_key(&entry));
-
                 let id = RevisionId(entry);
-
 
-
                 self.revisions.insert(
-
                     id,
-
                     Some(Revision::new(
-
                         id,
-
                         author.into(),
-
                         description,
-
                         base,
-
                         oid,
-
                         timestamp,
-
                         resolves,
-
                     )),
-
                 );
-
             }
-
+            Action::RevisionReactV1(RevisionReact {
-
+                revision,
-
+                reaction,
-
+                active,
-
+                location,
-
+            }) => {
-
+                if let Some(revision) = lookup::revision_mut(self, &revision)? {
-
+                    let key = (author, reaction);
-
+                    let location = location.map(CodeLocation::from);
-
+                    let reactions = revision.reactions.entry(location).or_default();
-
+
-
+                    if active {
-
+                        reactions.insert(key);
-
+                    } else {
-
+                        reactions.remove(&key);
-
+                    }
-
+                }
-
+            }
-
             Action::RevisionReact {
-
                 revision,
-
                 reaction,
-
                 active,
-
                 location,
-
             } => {
-
                 if let Some(revision) = lookup::revision_mut(self, &revision)? {
-
                     let key = (author, reaction);
-
+                    let location = location.map(CodeLocation::from);
-
                     let reactions = revision.reactions.entry(location).or_default();
-
 
-
                     if active {
-
                         reactions.insert(key);
-
                     } else {
-
                         reactions.remove(&key);
-
                     }
-
                 }
-
             }
-
             Action::RevisionRedact { revision } => {
-
                 // Not allowed to delete the root revision.
-
                 let (root, _) = self.root();
-
                 if revision == root {
-
                     return Err(Error::NotAllowed(entry));
-
                 }
-
                 // Redactions must have observed a revision to be valid.
-
                 if let Some(r) = self.revisions.get_mut(&revision) {
-
                     // If the revision has already been merged, ignore the redaction. We
-
                     // don't want to redact merged revisions.
-
                     if self.merges.values().any(|m| m.revision == revision) {
-
                         return Ok(());
-
                     }
-
                     *r = None;
-
                 } else {
-
                     return Err(Error::Missing(revision.into_inner()));
-
                 }
-
             }
-
             Action::Review {
-
                 revision,
-
                 ref summary,
-
                 verdict,
-
                 labels,
-
             } => {
-
                 let Some(rev) = self.revisions.get_mut(&revision) else {
-
                     // If the revision was redacted concurrently, there's nothing to do.
-
                     return Ok(());
-
                 };
-
                 if let Some(rev) = rev {
-
                     // Insert a review if there isn't already one. Otherwise we just ignore
-
                     // this operation
-
                     if let btree_map::Entry::Vacant(e) = rev.reviews.entry(author) {
-
                         let id = ReviewId(entry);
-
 
-
                         e.insert(Review::new(
-
                             id,
-
                             Author::new(author),
-
                             verdict,
-
                             summary.to_owned(),
-
                             labels,
-
                             timestamp,
-
                         ));
-
                         // Update reviews index.
-
                         self.reviews.insert(id, Some((revision, author)));
-
                     } else {
-
                         log::error!(
-
                             target: "patch",
-
                             "Review by {author} for {revision} already exists, ignoring action.."
-
                         );
-
                     }
-
                 }
-
             }
-
             Action::ReviewEdit {
-
                 review,
-
                 summary,
-
                 verdict,
-
                 labels,
-
             } => {
-
                 if summary.is_none() && verdict.is_none() {
-
                     return Err(Error::EmptyReview);
-
                 }
-
                 let Some(review) = lookup::review_mut(self, &review)? else {
-
                     return Ok(());
-
                 };
-
                 review.verdict = verdict;
-
                 review.summary = summary;
-
                 review.labels = labels;
-
             }
-
             Action::ReviewCommentReact {
-
                 review,
-
                 comment,
-
                 reaction,
-
                 active,
-
             } => {
-
                 if let Some(review) = lookup::review_mut(self, &review)? {
-
                     thread::react(
-
                         &mut review.comments,
-
                         entry,
-
                         author,
-
                         comment,
-
                         reaction,
-
                         active,
-
                     )?;
-
                 }
-
             }
-
             Action::ReviewCommentRedact { review, comment } => {
-
                 if let Some(review) = lookup::review_mut(self, &review)? {
-
                     thread::redact(&mut review.comments, entry, comment)?;
-
                 }
-
             }
-
             Action::ReviewCommentEdit {
-
                 review,
-
                 comment,
-
                 body,
-
                 embeds,
-
             } => {
-
                 if let Some(review) = lookup::review_mut(self, &review)? {
-
                     thread::edit(
-
                         &mut review.comments,
-
                         entry,
-
                         author,
-
                         comment,
-
                         timestamp,
-
                         body,
-
                         embeds,
-
                     )?;
-
                 }
-
             }
-
             Action::ReviewCommentResolve { review, comment } => {
-
                 if let Some(review) = lookup::review_mut(self, &review)? {
-
                     thread::resolve(&mut review.comments, entry, comment)?;
-
                 }
-
             }
-
             Action::ReviewCommentUnresolve { review, comment } => {
-
                 if let Some(review) = lookup::review_mut(self, &review)? {
-
                     thread::unresolve(&mut review.comments, entry, comment)?;
-
                 }
-
             }
-
+            Action::ReviewCommentV1(ReviewComment {
-
+                review,
-
+                body,
-
+                location,
-
+                reply_to,
-
+                embeds,
-
+            }) => {
-
+                if let Some(review) = lookup::review_mut(self, &review)? {
-
+                    thread::comment(
-
+                        &mut review.comments,
-
+                        entry,
-
+                        author,
-
+                        timestamp,
-
+                        body,
-
+                        reply_to,
-
+                        location.map(|loc| loc.into()),
-
+                        embeds,
-
+                    )?;
-
+                }
-
+            }
-
             Action::ReviewComment {
-
                 review,
-
                 body,
-
                 location,
-
                 reply_to,
-
                 embeds,
-
             } => {
-
                 if let Some(review) = lookup::review_mut(self, &review)? {
-
                     thread::comment(
-
                         &mut review.comments,
-
                         entry,
-
                         author,
-
                         timestamp,
-
                         body,
-
                         reply_to,
-
-                        location,
-
+                        location.map(|loc| loc.into()),
-
                         embeds,
-
                     )?;
-
                 }
-
             }
-
             Action::ReviewRedact { review } => {
-
                 // Redactions must have observed a review to be valid.
-
                 let Some(locator) = self.reviews.get_mut(&review) else {
-
                     return Err(Error::Missing(review.into_inner()));
-
                 };
-
                 // If the review is already redacted, do nothing.
-
                 let Some((revision, reviewer)) = locator else {
-
                     return Ok(());
-
                 };
-
                 // The revision must have existed at some point.
-
                 let Some(redactable) = self.revisions.get_mut(revision) else {
-
                     return Err(Error::Missing(revision.into_inner()));
-
                 };
-
                 // But it could be redacted.
-
                 let Some(revision) = redactable else {
-
                     return Ok(());
-
                 };
-
                 // Remove review for this author.
-
                 if let Some(r) = revision.reviews.remove(reviewer) {
-
                     debug_assert_eq!(r.id, review);
-
                 } else {
-
                     log::error!(
-
                         target: "patch", "Review {review} not found in revision {}", revision.id
-
                     );
-
                 }
-
                 // Set the review locator in the review index to redacted.
-
                 *locator = None;
-
             }
-
             Action::Merge { revision, commit } => {
-
                 // If the revision was redacted before the merge, ignore the merge.
-
                 if lookup::revision_mut(self, &revision)?.is_none() {
-
                     return Ok(());
-
                 };
-
                 match self.target() {
-
                     MergeTarget::Delegates => {
-
                         let proj = identity.project()?;
-
                         let branch = git::refs::branch(proj.default_branch());
-
 
-
                         // Nb. We don't return an error in case the merge commit is not an
-
                         // ancestor of the default branch. The default branch can change
-
                         // *after* the merge action is created, which is out of the control
-
                         // of the merge author. We simply skip it, which allows archiving in
-
                         // case of a rebase off the master branch, or a redaction of the
-
                         // merge.
-
                         let Ok(head) = repo.reference_oid(&author, &branch) else {
-
                             return Ok(());
-
                         };
-
                         if commit != head && !repo.is_ancestor_of(commit, head)? {
-
                             return Ok(());
-
                         }
-
                     }
-
                 }
-
                 self.merges.insert(
-
                     author,
-
                     Merge {
-
                         revision,
-
                         commit,
-
                         timestamp,
-
                     },
-
                 );
-
 
-
                 let mut merges = self.merges.iter().fold(
-
                     HashMap::<(RevisionId, git::Oid), usize>::new(),
-
                     |mut acc, (_, merge)| {
-
                         *acc.entry((merge.revision, merge.commit)).or_default() += 1;
-
                         acc
-
                     },
-
                 );
-
                 // Discard revisions that weren't merged by a threshold of delegates.
-
                 merges.retain(|_, count| *count >= identity.threshold());
-
 
-
                 match merges.into_keys().collect::<Vec<_>>().as_slice() {
-
                     [] => {
-
                         // None of the revisions met the quorum.
-
                     }
-
                     [(revision, commit)] => {
-
                         // Patch is merged.
-
                         self.state = State::Merged {
-
                             revision: *revision,
-
                             commit: *commit,
-
                         };
-
                     }
-
                     revisions => {
-
                         // More than one revision met the quorum.
-
                         self.state = State::Open {
-
                             conflicts: revisions.to_vec(),
-
                         };
-
                     }
-
                 }
-
             }
-
 
-
+            Action::RevisionCommentV1(RevisionComment {
-
+                revision,
-
+                body,
-
+                reply_to,
-
+                embeds,
-
+                location,
-
+            }) => {
-
+                if let Some(revision) = lookup::revision_mut(self, &revision)? {
-
+                    thread::comment(
-
+                        &mut revision.discussion,
-
+                        entry,
-
+                        author,
-
+                        timestamp,
-
+                        body,
-
+                        reply_to,
-
+                        location.map(|loc| loc.into()),
-
+                        embeds,
-
+                    )?;
-
+                }
-
+            }
-
             Action::RevisionComment {
-
                 revision,
-
                 body,
-
                 reply_to,
-
                 embeds,
-
                 location,
-
             } => {
-
                 if let Some(revision) = lookup::revision_mut(self, &revision)? {
-
                     thread::comment(
-
                         &mut revision.discussion,
-
                         entry,
-
                         author,
-
                         timestamp,
-
                         body,
-
                         reply_to,
-
-                        location,
-
+                        location.map(|loc| loc.into()),
-
                         embeds,
-
                     )?;
-
                 }
-
             }
-
             Action::RevisionCommentEdit {
-
                 revision,
-
                 comment,
-
                 body,
-
                 embeds,
-
             } => {
-
                 if let Some(revision) = lookup::revision_mut(self, &revision)? {
-
                     thread::edit(
-
                         &mut revision.discussion,
-
                         entry,
-
                         author,
-
                         comment,
-
                         timestamp,
-
                         body,
-
                         embeds,
-
                     )?;
-
                 }
-
             }
-
             Action::RevisionCommentRedact { revision, comment } => {
-
                 if let Some(revision) = lookup::revision_mut(self, &revision)? {
-
                     thread::redact(&mut revision.discussion, entry, comment)?;
-
                 }
-
             }
-
             Action::RevisionCommentReact {
-
                 revision,
-
                 comment,
-
                 reaction,
-
                 active,
-
             } => {
-
                 if let Some(revision) = lookup::revision_mut(self, &revision)? {
-
                     thread::react(
-
                         &mut revision.discussion,
-
                         entry,
-
                         author,
-
                         comment,
-
                         reaction,
-
                         active,
-
                     )?;
-
                 }
-
             }
-
         }
-
         Ok(())
-
     }
-
 }
-
 
-
 impl cob::store::CobWithType for Patch {
-
     fn type_name() -> &'static TypeName {
-
         &TYPENAME
-
     }
-
 }
-
 
-
 impl store::Cob for Patch {
-
     type Action = Action;
-
     type Error = Error;
-
 
-
     fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
-
         let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
-
         let mut actions = op.actions.into_iter();
-
         let Some(Action::Revision {
-
             description,
-
             base,
-
             oid,
-
             resolves,
-
         }) = actions.next()
-
         else {
-
             return Err(Error::Init("the first action must be of type `revision`"));
-
         };
-
         let Some(Action::Edit { title, target }) = actions.next() else {
-
             return Err(Error::Init("the second action must be of type `edit`"));
-
         };
-
         let revision = Revision::new(
-
             RevisionId(op.id),
-
             op.author.into(),
-
             description,
-
             base,
-
             oid,
-
             op.timestamp,
-
             resolves,
-
         );
-
         let mut patch = Patch::new(title, target, (RevisionId(op.id), revision));
-
 
-
         for action in actions {
-
             match patch.authorization(&action, &op.author, &doc)? {
-
                 Authorization::Allow => {
-
                     patch.action(action, op.id, op.author, op.timestamp, &[], &doc, repo)?;
-
                 }
-
                 Authorization::Deny => {
-
                     return Err(Error::NotAuthorized(op.author, action));
-
                 }
-
                 Authorization::Unknown => {
-
                     // Note that this shouldn't really happen since there's no concurrency in the
-
                     // root operation.
-
                     continue;
-
                 }
-
             }
-
         }
-
         Ok(patch)
-
     }
-
 
-
     fn op<'a, R: ReadRepository, I: IntoIterator<Item = &'a cob::Entry>>(
-
         &mut self,
-
         op: Op,
-
         concurrent: I,
-
         repo: &R,
-
     ) -> Result<(), Error> {
-
         debug_assert!(!self.timeline.contains(&op.id));
-
         self.timeline.push(op.id);
-
 
-
         let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
-
         let concurrent = concurrent.into_iter().collect::<Vec<_>>();
-
 
-
         for action in op.actions {
-
             log::trace!(target: "patch", "Applying {} {action:?}", op.id);
-
 
-
             if let Err(e) = self.op_action(
-
                 action,
-
                 op.id,
-
                 op.author,
-
                 op.timestamp,
-
                 &concurrent,
-
                 &doc,
-
                 repo,
-
             ) {
-
                 log::error!(target: "patch", "Error applying {}: {e}", op.id);
-
                 return Err(e);
-
             }
-
         }
-
         Ok(())
-
     }
-
 }
-
 
-
 impl<R: ReadRepository> cob::Evaluate<R> for Patch {
-
     type Error = Error;
-
 
-
     fn init(entry: &cob::Entry, repo: &R) -> Result<Self, Self::Error> {
-
         let op = Op::try_from(entry)?;
-
         let object = Patch::from_root(op, repo)?;
-
 
-
         Ok(object)
-
     }
-
 
-
     fn apply<'a, I: Iterator<Item = (&'a EntryId, &'a cob::Entry)>>(
-
         &mut self,
-
         entry: &cob::Entry,
-
         concurrent: I,
-
         repo: &R,
-
     ) -> Result<(), Self::Error> {
-
         let op = Op::try_from(entry)?;
-
 
-
         self.op(op, concurrent.map(|(_, e)| e), repo)
-
     }
-
 }
-
 
-
 mod lookup {
-
     use super::*;
-
 
-
     pub fn revision<'a>(
-
         patch: &'a Patch,
-
         revision: &RevisionId,
-
     ) -> Result<Option<&'a Revision>, Error> {
-
         match patch.revisions.get(revision) {
-
             Some(Some(revision)) => Ok(Some(revision)),
-
             // Redacted.
-
             Some(None) => Ok(None),
-
             // Missing. Causal error.
-
             None => Err(Error::Missing(revision.into_inner())),
-
         }
-
     }
-
 
-
     pub fn revision_mut<'a>(
-
         patch: &'a mut Patch,
-
         revision: &RevisionId,
-
     ) -> Result<Option<&'a mut Revision>, Error> {
-
         match patch.revisions.get_mut(revision) {
-
             Some(Some(revision)) => Ok(Some(revision)),
-
             // Redacted.
-
             Some(None) => Ok(None),
-
             // Missing. Causal error.
-
             None => Err(Error::Missing(revision.into_inner())),
-
         }
-
     }
-
 
-
     pub fn review<'a>(
-
         patch: &'a Patch,
-
         review: &ReviewId,
-
     ) -> Result<Option<(&'a Revision, &'a Review)>, Error> {
-
         match patch.reviews.get(review) {
-
             Some(Some((revision, author))) => {
-
                 match patch.revisions.get(revision) {
-
                     Some(Some(rev)) => {
-
                         let r = rev
-
                             .reviews
-
                             .get(author)
-
                             .ok_or_else(|| Error::Missing(review.into_inner()))?;
-
                         debug_assert_eq!(&r.id, review);
-
 
-
                         Ok(Some((rev, r)))
-
                     }
-
                     Some(None) => {
-
                         // If the revision was redacted concurrently, there's nothing to do.
-
                         // Likewise, if the review was redacted concurrently, there's nothing to do.
-
                         Ok(None)
-
                     }
-
                     None => Err(Error::Missing(revision.into_inner())),
-
                 }
-
             }
-
             Some(None) => {
-
                 // Redacted.
-
                 Ok(None)
-
             }
-
             None => Err(Error::Missing(review.into_inner())),
-
         }
-
     }
-
 
-
     pub fn review_mut<'a>(
-
         patch: &'a mut Patch,
-
         review: &ReviewId,
-
     ) -> Result<Option<&'a mut Review>, Error> {
-
         match patch.reviews.get(review) {
-
             Some(Some((revision, author))) => {
-
                 match patch.revisions.get_mut(revision) {
-
                     Some(Some(rev)) => {
-
                         let r = rev
-
                             .reviews
-
                             .get_mut(author)
-
                             .ok_or_else(|| Error::Missing(review.into_inner()))?;
-
                         debug_assert_eq!(&r.id, review);
-
 
-
                         Ok(Some(r))
-
                     }
-
                     Some(None) => {
-
                         // If the revision was redacted concurrently, there's nothing to do.
-
                         // Likewise, if the review was redacted concurrently, there's nothing to do.
-
                         Ok(None)
-
                     }
-
                     None => Err(Error::Missing(revision.into_inner())),
-
                 }
-
             }
-
             Some(None) => {
-
                 // Redacted.
-
                 Ok(None)
-
             }
-
             None => Err(Error::Missing(review.into_inner())),
-
         }
-
     }
-
 }
-
 
-
+/// A `CodeLocation` enumerates the different locations that refer to code.
-
+///
-
+/// [`CodeLocation::Partial`] is the variant that ensures we are
-
+/// backwards-compatible.
-
+///
-
+/// [`CodeLocation::Diff`] is the variant that is the location that is used in
-
+/// the public API.
-
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-
+#[serde(rename_all = "camelCase", tag = "type")]
-
+pub enum CodeLocation {
-
+    Partial(PartialLocation),
-
+    Diff(DiffLocation),
-
+}
-
+
-
+impl From<PartialLocation> for CodeLocation {
-
+    fn from(loc: PartialLocation) -> Self {
-
+        Self::Partial(loc)
-
+    }
-
+}
-
+
-
+impl From<DiffLocation> for CodeLocation {
-
+    fn from(loc: DiffLocation) -> Self {
-
+        Self::Diff(loc)
-
+    }
-
+}
-
+
-
 /// A patch revision.
-
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
 #[serde(rename_all = "camelCase")]
-
 pub struct Revision {
-
     /// Revision identifier.
-
     pub(super) id: RevisionId,
-
     /// Author of the revision.
-
     pub(super) author: Author,
-
     /// Revision description.
-
     pub(super) description: NonEmpty<Edit>,
-
     /// Base branch commit, used as a merge base.
-
     pub(super) base: git::Oid,
-
     /// Reference to the Git object containing the code (revision head).
-
     pub(super) oid: git::Oid,
-
     /// Discussion around this revision.
-
     pub(super) discussion: Thread<Comment<CodeLocation>>,
-
     /// Reviews of this revision's changes (all review edits are kept).
-
     pub(super) reviews: BTreeMap<ActorId, Review>,
-
     /// When this revision was created.
-
     pub(super) timestamp: Timestamp,
-
     /// Review comments resolved by this revision.
-
     pub(super) resolves: BTreeSet<(EntryId, CommentId)>,
-
     /// Reactions on code locations and revision itself
-
     #[serde(
-
         serialize_with = "ser::serialize_reactions",
-
         deserialize_with = "ser::deserialize_reactions"
-
     )]
-
     pub(super) reactions: BTreeMap<Option<CodeLocation>, Reactions>,
-
 }
-
 
-
 impl Revision {
-
     pub fn new(
-
         id: RevisionId,
-
         author: Author,
-
         description: String,
-
         base: git::Oid,
-
         oid: git::Oid,
-
         timestamp: Timestamp,
-
         resolves: BTreeSet<(EntryId, CommentId)>,
-
     ) -> Self {
-
         let description = Edit::new(*author.public_key(), description, timestamp, Vec::default());
-
 
-
         Self {
-
             id,
-
             author,
-
             description: NonEmpty::new(description),
-
             base,
-
             oid,
-
             discussion: Thread::default(),
-
             reviews: BTreeMap::default(),
-
             timestamp,
-
             resolves,
-
             reactions: Default::default(),
-
         }
-
     }
-
 
-
     pub fn id(&self) -> RevisionId {
-
         self.id
-
     }
-
 
-
     pub fn description(&self) -> &str {
-
         self.description.last().body.as_str()
-
     }
-
 
-
     pub fn edits(&self) -> impl Iterator<Item = &Edit> {
-
         self.description.iter()
-
     }
-
 
-
     pub fn embeds(&self) -> &[Embed<Uri>] {
-
         &self.description.last().embeds
-
     }
-
 
-
     pub fn reactions(&self) -> &BTreeMap<Option<CodeLocation>, BTreeSet<(PublicKey, Reaction)>> {
-
         &self.reactions
-
     }
-
 
-
     /// Author of the revision.
-
     pub fn author(&self) -> &Author {
-
         &self.author
-
     }
-
 
-
     /// Base branch commit, used as a merge base.
-
     pub fn base(&self) -> &git::Oid {
-
         &self.base
-
     }
-
 
-
     /// Reference to the Git object containing the code (revision head).
-
     pub fn head(&self) -> git::Oid {
-
         self.oid
-
     }
-
 
-
     /// Get the commit range of this revision.
-
     pub fn range(&self) -> (git::Oid, git::Oid) {
-
         (self.base, self.oid)
-
     }
-
 
-
     /// When this revision was created.
-
     pub fn timestamp(&self) -> Timestamp {
-
         self.timestamp
-
     }
-
 
-
     /// Discussion around this revision.
-
     pub fn discussion(&self) -> &Thread<Comment<CodeLocation>> {
-
         &self.discussion
-
     }
-
 
-
     /// Review comments resolved by this revision.
-
     pub fn resolves(&self) -> &BTreeSet<(EntryId, CommentId)> {
-
         &self.resolves
-
     }
-
 
-
     /// Iterate over all top-level replies.
-
     pub fn replies(&self) -> impl Iterator<Item = (&CommentId, &thread::Comment<CodeLocation>)> {
-
         self.discussion.comments()
-
     }
-
 
-
     /// Reviews of this revision's changes (one per actor).
-
     pub fn reviews(&self) -> impl DoubleEndedIterator<Item = (&PublicKey, &Review)> {
-
         self.reviews.iter()
-
     }
-
 
-
     /// Get a review by author.
-
     pub fn review_by(&self, author: &ActorId) -> Option<&Review> {
-
         self.reviews.get(author)
-
     }
-
 }
-
 
-
 /// Patch state.
-
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-
 #[serde(rename_all = "camelCase", tag = "status")]
-
 pub enum State {
-
     Draft,
-
     Open {
-
         /// Revisions that were merged and are conflicting.
-
         #[serde(skip_serializing_if = "Vec::is_empty")]
-
         #[serde(default)]
-
         conflicts: Vec<(RevisionId, git::Oid)>,
-
     },
-
     Archived,
-
     Merged {
-
         /// The revision that was merged.
-
         revision: RevisionId,
-
         /// The commit in the target branch that contains the changes.
-
         commit: git::Oid,
-
     },
-
 }
-
 
-
 impl Default for State {
-
     fn default() -> Self {
-
         Self::Open { conflicts: vec![] }
-
     }
-
 }
-
 
-
 impl fmt::Display for State {
-
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
         match self {
-
             Self::Archived => write!(f, "archived"),
-
             Self::Draft => write!(f, "draft"),
-
             Self::Open { .. } => write!(f, "open"),
-
             Self::Merged { .. } => write!(f, "merged"),
-
         }
-
     }
-
 }
-
 
-
 impl From<&State> for Status {
-
     fn from(value: &State) -> Self {
-
         match value {
-
             State::Draft => Self::Draft,
-
             State::Open { .. } => Self::Open,
-
             State::Archived => Self::Archived,
-
             State::Merged { .. } => Self::Merged,
-
         }
-
     }
-
 }
-
 
-
 /// A simplified enumeration of a [`State`] that can be used for
-
 /// filtering purposes.
-
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
-
 pub enum Status {
-
     Draft,
-
     #[default]
-
     Open,
-
     Archived,
-
     Merged,
-
 }
-
 
-
 impl fmt::Display for Status {
-
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
         match self {
-
             Self::Archived => write!(f, "archived"),
-
             Self::Draft => write!(f, "draft"),
-
             Self::Open => write!(f, "open"),
-
             Self::Merged => write!(f, "merged"),
-
         }
-
     }
-
 }
-
 
-
 /// A lifecycle operation, resulting in a new state.
-
 #[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-
 #[serde(rename_all = "camelCase", tag = "status")]
-
 pub enum Lifecycle {
-
     #[default]
-
     Open,
-
     Draft,
-
     Archived,
-
 }
-
 
-
 /// A merged patch revision.
-
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
-
 #[serde(rename_all = "camelCase")]
-
 pub struct Merge {
-
     /// Revision that was merged.
-
     pub revision: RevisionId,
-
     /// Base branch commit that contains the revision.
-
     pub commit: git::Oid,
-
     /// When this merge was performed.
-
     pub timestamp: Timestamp,
-
 }
-
 
-
 /// A patch review verdict.
-
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-
 #[serde(rename_all = "camelCase")]
-
 pub enum Verdict {
-
     /// Accept patch.
-
     Accept,
-
     /// Reject patch.
-
     Reject,
-
 }
-
 
-
 impl fmt::Display for Verdict {
-
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
         match self {
-
             Self::Accept => write!(f, "accept"),
-
             Self::Reject => write!(f, "reject"),
-
         }
-
     }
-
 }
-
 
-
 /// A patch review on a revision.
-
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
 #[serde(rename_all = "camelCase")]
-
 pub struct Review {
-
     /// Review identifier.
-
     pub(super) id: ReviewId,
-
     /// Review author.
-
     pub(super) author: Author,
-
     /// Review verdict.
-
     ///
-
     /// The verdict cannot be changed, since revisions are immutable.
-
     pub(super) verdict: Option<Verdict>,
-
     /// Review summary.
-
     ///
-
     /// Can be edited or set to `None`.
-
     pub(super) summary: Option<String>,
-
     /// Review comments.
-
     pub(super) comments: Thread<Comment<CodeLocation>>,
-
     /// Labels qualifying the review. For example if this review only looks at the
-
     /// concept or intention of the patch, it could have a "concept" label.
-
     pub(super) labels: Vec<Label>,
-
     /// Review timestamp.
-
     pub(super) timestamp: Timestamp,
-
 }
-
 
-
 impl Review {
-
     pub fn new(
-
         id: ReviewId,
-
         author: Author,
-
         verdict: Option<Verdict>,
-
         summary: Option<String>,
-
         labels: Vec<Label>,
-
         timestamp: Timestamp,
-
     ) -> Self {
-
         Self {
-
             id,
-
             author,
-
             verdict,
-
             summary,
-
             comments: Thread::default(),
-
             labels,
-
             timestamp,
-
         }
-
     }
-
 
-
     /// Review identifier.
-
     pub fn id(&self) -> ReviewId {
-
         self.id
-
     }
-
 
-
     /// Review author.
-
     pub fn author(&self) -> &Author {
-
         &self.author
-
     }
-
 
-
     /// Review verdict.
-
     pub fn verdict(&self) -> Option<Verdict> {
-
         self.verdict
-
     }
-
 
-
     /// Review inline code comments.
-
     pub fn comments(&self) -> impl DoubleEndedIterator<Item = (&EntryId, &Comment<CodeLocation>)> {
-
         self.comments.comments()
-
     }
-
 
-
     /// Review labels.
-
     pub fn labels(&self) -> impl Iterator<Item = &Label> {
-
         self.labels.iter()
-
     }
-
 
-
     /// Review general comment.
-
     pub fn summary(&self) -> Option<&str> {
-
         self.summary.as_deref()
-
     }
-
 
-
     /// Review timestamp.
-
     pub fn timestamp(&self) -> Timestamp {
-
         self.timestamp
-
     }
-
 }
-
 
-
 impl<R: ReadRepository> store::Transaction<Patch, R> {
-
     pub fn edit(&mut self, title: impl ToString, target: MergeTarget) -> Result<(), store::Error> {
-
         self.push(Action::Edit {
-
             title: title.to_string(),
-
             target,
-
         })
-
     }
-
 
-
     pub fn edit_revision(
-
         &mut self,
-
         revision: RevisionId,
-
         description: impl ToString,
-
         embeds: Vec<Embed<Uri>>,
-
     ) -> Result<(), store::Error> {
-
         self.embed(embeds.clone())?;
-
         self.push(Action::RevisionEdit {
-
             revision,
-
             description: description.to_string(),
-
             embeds,
-
         })
-
     }
-
 
-
     /// Redact the revision.
-
     pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
-
         self.push(Action::RevisionRedact { revision })
-
     }
-
 
-
     /// Start a patch revision discussion.
-
     pub fn thread<S: ToString>(
-
         &mut self,
-
         revision: RevisionId,
-
         body: S,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::RevisionComment {
-
             revision,
-
             body: body.to_string(),
-
             reply_to: None,
-
             location: None,
-
             embeds: vec![],
-
         })
-
     }
-
 
-
     /// React on a patch revision.
-
     pub fn react(
-
         &mut self,
-
         revision: RevisionId,
-
         reaction: Reaction,
-
-        location: Option<CodeLocation>,
-
+        location: Option<DiffLocation>,
-
         active: bool,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::RevisionReact {
-
             revision,
-
             reaction,
-
             location,
-
             active,
-
         })
-
     }
-
 
-
     /// Comment on a patch revision.
-
     pub fn comment<S: ToString>(
-
         &mut self,
-
         revision: RevisionId,
-
         body: S,
-
         reply_to: Option<CommentId>,
-
-        location: Option<CodeLocation>,
-
+        location: Option<DiffLocation>,
-
         embeds: Vec<Embed<Uri>>,
-
     ) -> Result<(), store::Error> {
-
         self.embed(embeds.clone())?;
-
         self.push(Action::RevisionComment {
-
             revision,
-
             body: body.to_string(),
-
             reply_to,
-
             location,
-
             embeds,
-
         })
-
     }
-
 
-
     /// Edit a comment on a patch revision.
-
     pub fn comment_edit<S: ToString>(
-
         &mut self,
-
         revision: RevisionId,
-
         comment: CommentId,
-
         body: S,
-
         embeds: Vec<Embed<Uri>>,
-
     ) -> Result<(), store::Error> {
-
         self.embed(embeds.clone())?;
-
         self.push(Action::RevisionCommentEdit {
-
             revision,
-
             comment,
-
             body: body.to_string(),
-
             embeds,
-
         })
-
     }
-
 
-
     /// React a comment on a patch revision.
-
     pub fn comment_react(
-
         &mut self,
-
         revision: RevisionId,
-
         comment: CommentId,
-
         reaction: Reaction,
-
         active: bool,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::RevisionCommentReact {
-
             revision,
-
             comment,
-
             reaction,
-
             active,
-
         })
-
     }
-
 
-
     /// Redact a comment on a patch revision.
-
     pub fn comment_redact(
-
         &mut self,
-
         revision: RevisionId,
-
         comment: CommentId,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::RevisionCommentRedact { revision, comment })
-
     }
-
 
-
-    /// Comment on a review.
-
+    /// Comment on a review for a diff location.
-
     pub fn review_comment<S: ToString>(
-
         &mut self,
-
         review: ReviewId,
-
         body: S,
-
-        location: Option<CodeLocation>,
-
+        location: Option<DiffLocation>,
-
         reply_to: Option<CommentId>,
-
         embeds: Vec<Embed<Uri>>,
-
     ) -> Result<(), store::Error> {
-
         self.embed(embeds.clone())?;
-
         self.push(Action::ReviewComment {
-
             review,
-
             body: body.to_string(),
-
             location,
-
             reply_to,
-
             embeds,
-
         })
-
     }
-
 
-
     /// Resolve a review comment.
-
     pub fn review_comment_resolve(
-
         &mut self,
-
         review: ReviewId,
-
         comment: CommentId,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::ReviewCommentResolve { review, comment })
-
     }
-
 
-
     /// Unresolve a review comment.
-
     pub fn review_comment_unresolve(
-
         &mut self,
-
         review: ReviewId,
-
         comment: CommentId,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::ReviewCommentUnresolve { review, comment })
-
     }
-
 
-
     /// Edit review comment.
-
     pub fn edit_review_comment<S: ToString>(
-
         &mut self,
-
         review: ReviewId,
-
         comment: EntryId,
-
         body: S,
-
         embeds: Vec<Embed<Uri>>,
-
     ) -> Result<(), store::Error> {
-
         self.embed(embeds.clone())?;
-
         self.push(Action::ReviewCommentEdit {
-
             review,
-
             comment,
-
             body: body.to_string(),
-
             embeds,
-
         })
-
     }
-
 
-
     /// React to a review comment.
-
     pub fn react_review_comment(
-
         &mut self,
-
         review: ReviewId,
-
         comment: EntryId,
-
         reaction: Reaction,
-
         active: bool,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::ReviewCommentReact {
-
             review,
-
             comment,
-
             reaction,
-
             active,
-
         })
-
     }
-
 
-
     /// Redact a review comment.
-
     pub fn redact_review_comment(
-
         &mut self,
-
         review: ReviewId,
-
         comment: EntryId,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::ReviewCommentRedact { review, comment })
-
     }
-
 
-
     /// Review a patch revision.
-
     /// Does nothing if a review for that revision already exists by the author.
-
     pub fn review(
-
         &mut self,
-
         revision: RevisionId,
-
         verdict: Option<Verdict>,
-
         summary: Option<String>,
-
         labels: Vec<Label>,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::Review {
-
             revision,
-
             summary,
-
             verdict,
-
             labels,
-
         })
-
     }
-
 
-
     /// Edit a review.
-
     pub fn review_edit(
-
         &mut self,
-
         review: ReviewId,
-
         verdict: Option<Verdict>,
-
         summary: Option<String>,
-
         labels: Vec<Label>,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::ReviewEdit {
-
             review,
-
             summary,
-
             verdict,
-
             labels,
-
         })
-
     }
-
 
-
     /// Redact a patch review.
-
     pub fn redact_review(&mut self, review: ReviewId) -> Result<(), store::Error> {
-
         self.push(Action::ReviewRedact { review })
-
     }
-
 
-
     /// Merge a patch revision.
-
     pub fn merge(&mut self, revision: RevisionId, commit: git::Oid) -> Result<(), store::Error> {
-
         self.push(Action::Merge { revision, commit })
-
     }
-
 
-
     /// Update a patch with a new revision.
-
     pub fn revision(
-
         &mut self,
-
         description: impl ToString,
-
         base: impl Into<git::Oid>,
-
         oid: impl Into<git::Oid>,
-
     ) -> Result<(), store::Error> {
-
         self.push(Action::Revision {
-
             description: description.to_string(),
-
             base: base.into(),
-
             oid: oid.into(),
-
             resolves: BTreeSet::new(),
-
         })
-
     }
-
 
-
     /// Lifecycle a patch.
-
     pub fn lifecycle(&mut self, state: Lifecycle) -> Result<(), store::Error> {
-
         self.push(Action::Lifecycle { state })
-
     }
-
 
-
     /// Assign a patch.
-
     pub fn assign(&mut self, assignees: BTreeSet<Did>) -> Result<(), store::Error> {
-
         self.push(Action::Assign { assignees })
-
     }
-
 
-
     /// Label a patch.
-
     pub fn label(&mut self, labels: impl IntoIterator<Item = Label>) -> Result<(), store::Error> {
-
         self.push(Action::Label {
-
             labels: labels.into_iter().collect(),
-
         })
-
     }
-
 }
-
 
-
 pub struct PatchMut<'a, 'g, R, C> {
-
     pub id: ObjectId,
-
 
-
     patch: Patch,
-
     store: &'g mut Patches<'a, R>,
-
     cache: &'g mut C,
-
 }
-
 
-
 impl<'a, 'g, R, C> PatchMut<'a, 'g, R, C>
-
 where
-
     C: cob::cache::Update<Patch>,
-
     R: ReadRepository + SignRepository + cob::Store,
-
 {
-
     pub fn new(id: ObjectId, patch: Patch, cache: &'g mut Cache<Patches<'a, R>, C>) -> Self {
-
         Self {
-
             id,
-
             patch,
-
             store: &mut cache.store,
-
             cache: &mut cache.cache,
-
         }
-
     }
-
 
-
     pub fn id(&self) -> &ObjectId {
-
         &self.id
-
     }
-
 
-
     /// Reload the patch data from storage.
-
     pub fn reload(&mut self) -> Result<(), store::Error> {
-
         self.patch = self
-
             .store
-
             .get(&self.id)?
-
             .ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;
-
 
-
         Ok(())
-
     }
-
 
-
     pub fn transaction<G, F>(
-
         &mut self,
-
         message: &str,
-
         signer: &G,
-
         operations: F,
-
     ) -> Result<EntryId, Error>
-
     where
-
         G: Signer,
-
         F: FnOnce(&mut Transaction<Patch, R>) -> Result<(), store::Error>,
-
     {
-
         let mut tx = Transaction::default();
-
         operations(&mut tx)?;
-
 
-
         let (patch, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
-
         self.cache
-
             .update(&self.store.as_ref().id(), &self.id, &patch)
-
             .map_err(|e| Error::CacheUpdate {
-
                 id: self.id,
-
                 err: e.into(),
-
             })?;
-
         self.patch = patch;
-
 
-
         Ok(commit)
-
     }
-
 
-
     /// Edit patch metadata.
-
     pub fn edit<G: Signer>(
-
         &mut self,
-
         title: String,
-
         target: MergeTarget,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Edit", signer, |tx| tx.edit(title, target))
-
     }
-
 
-
     /// Edit revision metadata.
-
     pub fn edit_revision<G: Signer>(
-
         &mut self,
-
         revision: RevisionId,
-
         description: String,
-
         embeds: impl IntoIterator<Item = Embed<Uri>>,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Edit revision", signer, |tx| {
-
             tx.edit_revision(revision, description, embeds.into_iter().collect())
-
         })
-
     }
-
 
-
     /// Redact a revision.
-
     pub fn redact<G: Signer>(
-
         &mut self,
-
         revision: RevisionId,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Redact revision", signer, |tx| tx.redact(revision))
-
     }
-
 
-
     /// Create a thread on a patch revision.
-
     pub fn thread<G: Signer, S: ToString>(
-
         &mut self,
-
         revision: RevisionId,
-
         body: S,
-
         signer: &G,
-
     ) -> Result<CommentId, Error> {
-
         self.transaction("Create thread", signer, |tx| tx.thread(revision, body))
-
     }
-
 
-
     /// Comment on a patch revision.
-
     pub fn comment<G: Signer, S: ToString>(
-
         &mut self,
-
         revision: RevisionId,
-
         body: S,
-
         reply_to: Option<CommentId>,
-
-        location: Option<CodeLocation>,
-
+        location: Option<DiffLocation>,
-
         embeds: impl IntoIterator<Item = Embed<Uri>>,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Comment", signer, |tx| {
-
             tx.comment(
-
                 revision,
-
                 body,
-
                 reply_to,
-
                 location,
-
                 embeds.into_iter().collect(),
-
             )
-
         })
-
     }
-
 
-
     /// React on a patch revision.
-
     pub fn react<G: Signer>(
-
         &mut self,
-
         revision: RevisionId,
-
         reaction: Reaction,
-
-        location: Option<CodeLocation>,
-
+        location: Option<DiffLocation>,
-
         active: bool,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("React", signer, |tx| {
-
             tx.react(revision, reaction, location, active)
-
         })
-
     }
-
 
-
     /// Edit a comment on a patch revision.
-
     pub fn comment_edit<G: Signer, S: ToString>(
-
         &mut self,
-
         revision: RevisionId,
-
         comment: CommentId,
-
         body: S,
-
         embeds: impl IntoIterator<Item = Embed<Uri>>,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Edit comment", signer, |tx| {
-
             tx.comment_edit(revision, comment, body, embeds.into_iter().collect())
-
         })
-
     }
-
 
-
     /// React to a comment on a patch revision.
-
     pub fn comment_react<G: Signer>(
-
         &mut self,
-
         revision: RevisionId,
-
         comment: CommentId,
-
         reaction: Reaction,
-
         active: bool,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("React comment", signer, |tx| {
-
             tx.comment_react(revision, comment, reaction, active)
-
         })
-
     }
-
 
-
     /// Redact a comment on a patch revision.
-
     pub fn comment_redact<G: Signer>(
-
         &mut self,
-
         revision: RevisionId,
-
         comment: CommentId,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Redact comment", signer, |tx| {
-
             tx.comment_redact(revision, comment)
-
         })
-
     }
-
 
-
     /// Comment on a line of code as part of a review.
-
     pub fn review_comment<G: Signer, S: ToString>(
-
         &mut self,
-
         review: ReviewId,
-
         body: S,
-
-        location: Option<CodeLocation>,
-
+        location: Option<DiffLocation>,
-
         reply_to: Option<CommentId>,
-
         embeds: impl IntoIterator<Item = Embed<Uri>>,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Review comment", signer, |tx| {
-
             tx.review_comment(
-
                 review,
-
                 body,
-
                 location,
-
                 reply_to,
-
                 embeds.into_iter().collect(),
-
             )
-
         })
-
     }
-
 
-
     /// Edit review comment.
-
     pub fn edit_review_comment<G: Signer, S: ToString>(
-
         &mut self,
-
         review: ReviewId,
-
         comment: EntryId,
-
         body: S,
-
         embeds: impl IntoIterator<Item = Embed<Uri>>,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Edit review comment", signer, |tx| {
-
             tx.edit_review_comment(review, comment, body, embeds.into_iter().collect())
-
         })
-
     }
-
 
-
     /// React to a review comment.
-
     pub fn react_review_comment<G: Signer>(
-
         &mut self,
-
         review: ReviewId,
-
         comment: EntryId,
-
         reaction: Reaction,
-
         active: bool,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("React to review comment", signer, |tx| {
-
             tx.react_review_comment(review, comment, reaction, active)
-
         })
-
     }
-
 
-
     /// React to a review comment.
-
     pub fn redact_review_comment<G: Signer>(
-
         &mut self,
-
         review: ReviewId,
-
         comment: EntryId,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Redact review comment", signer, |tx| {
-
             tx.redact_review_comment(review, comment)
-
         })
-
     }
-
 
-
     /// Review a patch revision.
-
     pub fn review<G: Signer>(
-
         &mut self,
-
         revision: RevisionId,
-
         verdict: Option<Verdict>,
-
         summary: Option<String>,
-
         labels: Vec<Label>,
-
         signer: &G,
-
     ) -> Result<ReviewId, Error> {
-
         if verdict.is_none() && summary.is_none() {
-
             return Err(Error::EmptyReview);
-
         }
-
         self.transaction("Review", signer, |tx| {
-
             tx.review(revision, verdict, summary, labels)
-
         })
-
         .map(ReviewId)
-
     }
-
 
-
     /// Edit a review.
-
     pub fn review_edit<G: Signer>(
-
         &mut self,
-
         review: ReviewId,
-
         verdict: Option<Verdict>,
-
         summary: Option<String>,
-
         labels: Vec<Label>,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Edit review", signer, |tx| {
-
             tx.review_edit(review, verdict, summary, labels)
-
         })
-
     }
-
 
-
     /// Redact a patch review.
-
     pub fn redact_review<G: Signer>(
-
         &mut self,
-
         review: ReviewId,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Redact review", signer, |tx| tx.redact_review(review))
-
     }
-
 
-
     /// Resolve a patch review comment.
-
     pub fn resolve_review_comment<G: Signer>(
-
         &mut self,
-
         review: ReviewId,
-
         comment: CommentId,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Resolve review comment", signer, |tx| {
-
             tx.review_comment_resolve(review, comment)
-
         })
-
     }
-
 
-
     /// Unresolve a patch review comment.
-
     pub fn unresolve_review_comment<G: Signer>(
-
         &mut self,
-
         review: ReviewId,
-
         comment: CommentId,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Unresolve review comment", signer, |tx| {
-
             tx.review_comment_unresolve(review, comment)
-
         })
-
     }
-
 
-
     /// Merge a patch revision.
-
     pub fn merge<G: Signer>(
-
         &mut self,
-
         revision: RevisionId,
-
         commit: git::Oid,
-
         signer: &G,
-
     ) -> Result<Merged<R>, Error> {
-
         // TODO: Don't allow merging the same revision twice?
-
         let entry = self.transaction("Merge revision", signer, |tx| tx.merge(revision, commit))?;
-
 
-
         Ok(Merged {
-
             entry,
-
             patch: self.id,
-
             stored: self.store.as_ref(),
-
         })
-
     }
-
 
-
     /// Update a patch with a new revision.
-
     pub fn update<G: Signer>(
-
         &mut self,
-
         description: impl ToString,
-
         base: impl Into<git::Oid>,
-
         oid: impl Into<git::Oid>,
-
         signer: &G,
-
     ) -> Result<RevisionId, Error> {
-
         self.transaction("Add revision", signer, |tx| {
-
             tx.revision(description, base, oid)
-
         })
-
         .map(RevisionId)
-
     }
-
 
-
     /// Lifecycle a patch.
-
     pub fn lifecycle<G: Signer>(&mut self, state: Lifecycle, signer: &G) -> Result<EntryId, Error> {
-
         self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state))
-
     }
-
 
-
     /// Assign a patch.
-
     pub fn assign<G: Signer>(
-
         &mut self,
-
         assignees: BTreeSet<Did>,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Assign", signer, |tx| tx.assign(assignees))
-
     }
-
 
-
     /// Archive a patch.
-
     pub fn archive<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
-
         self.lifecycle(Lifecycle::Archived, signer)?;
-
 
-
         Ok(true)
-
     }
-
 
-
     /// Mark an archived patch as ready to be reviewed again.
-
     /// Returns `false` if the patch was not archived.
-
     pub fn unarchive<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
-
         if !self.is_archived() {
-
             return Ok(false);
-
         }
-
         self.lifecycle(Lifecycle::Open, signer)?;
-
 
-
         Ok(true)
-
     }
-
 
-
     /// Mark a patch as ready to be reviewed.
-
     /// Returns `false` if the patch was not a draft.
-
     pub fn ready<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
-
         if !self.is_draft() {
-
             return Ok(false);
-
         }
-
         self.lifecycle(Lifecycle::Open, signer)?;
-
 
-
         Ok(true)
-
     }
-
 
-
     /// Mark an open patch as a draft.
-
     /// Returns `false` if the patch was not open and free of merges.
-
     pub fn unready<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
-
         if !matches!(self.state(), State::Open { conflicts } if conflicts.is_empty()) {
-
             return Ok(false);
-
         }
-
         self.lifecycle(Lifecycle::Draft, signer)?;
-
 
-
         Ok(true)
-
     }
-
 
-
     /// Label a patch.
-
     pub fn label<G: Signer>(
-
         &mut self,
-
         labels: impl IntoIterator<Item = Label>,
-
         signer: &G,
-
     ) -> Result<EntryId, Error> {
-
         self.transaction("Label", signer, |tx| tx.label(labels))
-
     }
-
 }
-
 
-
 impl<R, C> Deref for PatchMut<'_, '_, R, C> {
-
     type Target = Patch;
-
 
-
     fn deref(&self) -> &Self::Target {
-
         &self.patch
-
     }
-
 }
-
 
-
 /// Detailed information on patch states
-
 #[derive(Debug, Default, PartialEq, Eq, Serialize)]
-
 #[serde(rename_all = "camelCase")]
-
 pub struct PatchCounts {
-
     pub open: usize,
-
     pub draft: usize,
-
     pub archived: usize,
-
     pub merged: usize,
-
 }
-
 
-
 impl PatchCounts {
-
     /// Total count.
-
     pub fn total(&self) -> usize {
-
         self.open + self.draft + self.archived + self.merged
-
     }
-
 }
-
 
-
 /// Result of looking up a `Patch`'s `Revision`.
-
 ///
-
 /// See [`Patches::find_by_revision`].
-
 #[derive(Debug, PartialEq, Eq)]
-
 pub struct ByRevision {
-
     pub id: PatchId,
-
     pub patch: Patch,
-
     pub revision_id: RevisionId,
-
     pub revision: Revision,
-
 }
-
 
-
 pub struct Patches<'a, R> {
-
     raw: store::Store<'a, Patch, R>,
-
 }
-
 
-
 impl<'a, R> Deref for Patches<'a, R> {
-
     type Target = store::Store<'a, Patch, R>;
-
 
-
     fn deref(&self) -> &Self::Target {
-
         &self.raw
-
     }
-
 }
-
 
-
 impl<R> HasRepoId for Patches<'_, R>
-
 where
-
     R: ReadRepository,
-
 {
-
     fn rid(&self) -> RepoId {
-
         self.as_ref().id()
-
     }
-
 }
-
 
-
 impl<'a, R> Patches<'a, R>
-
 where
-
     R: ReadRepository + cob::Store,
-
 {
-
     /// Open a patches store.
-
     pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
-
         let identity = repository.identity_head()?;
-
         let raw = store::Store::open(repository)?.identity(identity);
-
 
-
         Ok(Self { raw })
-
     }
-
 
-
     /// Patches count by state.
-
     pub fn counts(&self) -> Result<PatchCounts, store::Error> {
-
         let all = self.all()?;
-
         let state_groups =
-
             all.filter_map(|s| s.ok())
-
                 .fold(PatchCounts::default(), |mut state, (_, p)| {
-
                     match p.state() {
-
                         State::Draft => state.draft += 1,
-
                         State::Open { .. } => state.open += 1,
-
                         State::Archived => state.archived += 1,
-
                         State::Merged { .. } => state.merged += 1,
-
                     }
-
                     state
-
                 });
-
 
-
         Ok(state_groups)
-
     }
-
 
-
     /// Find the `Patch` containing the given `Revision`.
-
     pub fn find_by_revision(&self, revision: &RevisionId) -> Result<Option<ByRevision>, Error> {
-
         // Revision may be the patch's first, making it have the same ID.
-
         let p_id = ObjectId::from(revision.into_inner());
-
         if let Some(p) = self.get(&p_id)? {
-
             return Ok(p.revision(revision).map(|r| ByRevision {
-
                 id: p_id,
-
                 patch: p.clone(),
-
                 revision_id: *revision,
-
                 revision: r.clone(),
-
             }));
-
         }
-
         let result = self
-
             .all()?
-
             .filter_map(|result| result.ok())
-
             .find_map(|(p_id, p)| {
-
                 p.revision(revision).map(|r| ByRevision {
-
                     id: p_id,
-
                     patch: p.clone(),
-
                     revision_id: *revision,
-
                     revision: r.clone(),
-
                 })
-
             });
-
 
-
         Ok(result)
-
     }
-
 
-
     /// Get a patch.
-
     pub fn get(&self, id: &ObjectId) -> Result<Option<Patch>, store::Error> {
-
         self.raw.get(id)
-
     }
-
 
-
     /// Get proposed patches.
-
     pub fn proposed(&self) -> Result<impl Iterator<Item = (PatchId, Patch)> + '_, Error> {
-
         let all = self.all()?;
-
 
-
         Ok(all
-
             .into_iter()
-
             .filter_map(|result| result.ok())
-
             .filter(|(_, p)| p.is_open()))
-
     }
-
 
-
     /// Get patches proposed by the given key.
-
     pub fn proposed_by<'b>(
-
         &'b self,
-
         who: &'b Did,
-
     ) -> Result<impl Iterator<Item = (PatchId, Patch)> + 'b, Error> {
-
         Ok(self
-
             .proposed()?
-
             .filter(move |(_, p)| p.author().id() == who))
-
     }
-
 }
-
 
-
 impl<'a, R> Patches<'a, R>
-
 where
-
     R: ReadRepository + SignRepository + cob::Store,
-
 {
-
     /// Open a new patch.
-
     pub fn create<'g, C, G>(
-
         &'g mut self,
-
         title: impl ToString,
-
         description: impl ToString,
-
         target: MergeTarget,
-
         base: impl Into<git::Oid>,
-
         oid: impl Into<git::Oid>,
-
         labels: &[Label],
-
         cache: &'g mut C,
-
         signer: &G,
-
     ) -> Result<PatchMut<'a, 'g, R, C>, Error>
-
     where
-
         C: cob::cache::Update<Patch>,
-
         G: Signer,
-
     {
-
         self._create(
-
             title,
-
             description,
-
             target,
-
             base,
-
             oid,
-
             labels,
-
             Lifecycle::default(),
-
             cache,
-
             signer,
-
         )
-
     }
-
 
-
     /// Draft a patch. This patch will be created in a [`State::Draft`] state.
-
     pub fn draft<'g, C, G: Signer>(
-
         &'g mut self,
-
         title: impl ToString,
-
         description: impl ToString,
-
         target: MergeTarget,
-
         base: impl Into<git::Oid>,
-
         oid: impl Into<git::Oid>,
-
         labels: &[Label],
-
         cache: &'g mut C,
-
         signer: &G,
-
     ) -> Result<PatchMut<'a, 'g, R, C>, Error>
-
     where
-
         C: cob::cache::Update<Patch>,
-
     {
-
         self._create(
-
             title,
-
             description,
-
             target,
-
             base,
-
             oid,
-
             labels,
-
             Lifecycle::Draft,
-
             cache,
-
             signer,
-
         )
-
     }
-
 
-
     /// Get a patch mutably.
-
     pub fn get_mut<'g, C>(
-
         &'g mut self,
-
         id: &ObjectId,
-
         cache: &'g mut C,
-
     ) -> Result<PatchMut<'a, 'g, R, C>, store::Error> {
-
         let patch = self
-
             .raw
-
             .get(id)?
-
             .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *id))?;
-
 
-
         Ok(PatchMut {
-
             id: *id,
-
             patch,
-
             store: self,
-
             cache,
-
         })
-
     }
-
 
-
     /// Create a patch. This is an internal function used by `create` and `draft`.
-
     fn _create<'g, C, G: Signer>(
-
         &'g mut self,
-
         title: impl ToString,
-
         description: impl ToString,
-
         target: MergeTarget,
-
         base: impl Into<git::Oid>,
-
         oid: impl Into<git::Oid>,
-
         labels: &[Label],
-
         state: Lifecycle,
-
         cache: &'g mut C,
-
         signer: &G,
-
     ) -> Result<PatchMut<'a, 'g, R, C>, Error>
-
     where
-
         C: cob::cache::Update<Patch>,
-
     {
-
         let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx, _| {
-
             tx.revision(description, base, oid)?;
-
             tx.edit(title, target)?;
-
 
-
             if !labels.is_empty() {
-
                 tx.label(labels.to_owned())?;
-
             }
-
             if state != Lifecycle::default() {
-
                 tx.lifecycle(state)?;
-
             }
-
             Ok(())
-
         })?;
-
         cache
-
             .update(&self.raw.as_ref().id(), &id, &patch)
-
             .map_err(|e| Error::CacheUpdate { id, err: e.into() })?;
-
 
-
         Ok(PatchMut {
-
             id,
-
             patch,
-
             store: self,
-
             cache,
-
         })
-
     }
-
 }
-
 
-
 /// Models a comparison between two commit ranges,
-
 /// commonly obtained from two revisions (likely of the same patch).
-
 /// This can be used to generate a `git range-diff` command.
-
 /// See <https://git-scm.com/docs/git-range-diff>.
-
 #[derive(Debug, Clone, PartialEq, Eq, Copy)]
-
 pub struct RangeDiff {
-
     old: (git::Oid, git::Oid),
-
     new: (git::Oid, git::Oid),
-
 }
-
 
-
 impl RangeDiff {
-
     const COMMAND: &str = "git";
-
     const SUBCOMMAND: &str = "range-diff";
-
 
-
     pub fn new(old: &Revision, new: &Revision) -> Self {
-
         Self {
-
             old: old.range(),
-
             new: new.range(),
-
         }
-
     }
-
 
-
     pub fn to_command(&self) -> String {
-
         let range = if self.has_same_base() {
-
             format!("{} {} {}", self.old.0, self.old.1, self.new.1)
-
         } else {
-
             format!(
-
                 "{}..{} {}..{}",
-
                 self.old.0, self.old.1, self.new.0, self.new.1,
-
             )
-
         };
-
 
-
         Self::COMMAND.to_string() + " " + Self::SUBCOMMAND + " " + &range
-
     }
-
 
-
     fn has_same_base(&self) -> bool {
-
         self.old.0 == self.new.0
-
     }
-
 }
-
 
-
 impl From<RangeDiff> for std::process::Command {
-
     fn from(range_diff: RangeDiff) -> Self {
-
         let mut command = std::process::Command::new(RangeDiff::COMMAND);
-
 
-
         command.arg(RangeDiff::SUBCOMMAND);
-
 
-
         if range_diff.has_same_base() {
-
             command.args([
-
                 range_diff.old.0.to_string(),
-
                 range_diff.old.1.to_string(),
-
                 range_diff.new.1.to_string(),
-
             ]);
-
         } else {
-
             command.args([
-
                 format!("{}..{}", range_diff.old.0, range_diff.old.1),
-
                 format!("{}..{}", range_diff.new.0, range_diff.new.1),
-
             ]);
-
         }
-
         command
-
     }
-
 }
-
 
-
 /// Helpers for de/serialization of patch data types.
-
 mod ser {
-
     use std::collections::{BTreeMap, BTreeSet};
-
 
-
     use serde::ser::SerializeSeq;
-
 
-
-    use crate::cob::{thread::Reactions, ActorId, CodeLocation};
-
+    use crate::cob::{thread::Reactions, ActorId};
-
+
-
+    use super::CodeLocation;
-
 
-
     /// Serialize a `Revision`'s reaction as an object containing the
-
     /// `location`, `emoji`, and all `authors` that have performed the
-
     /// same reaction.
-
     #[derive(Debug, serde::Serialize, serde::Deserialize)]
-
     #[serde(rename_all = "camelCase")]
-
     struct Reaction {
-
         location: Option<CodeLocation>,
-
         emoji: super::Reaction,
-
         authors: Vec<ActorId>,
-
     }
-
 
-
     impl Reaction {
-
         fn as_revision_reactions(
-
             reactions: Vec<Reaction>,
-
         ) -> BTreeMap<Option<CodeLocation>, Reactions> {
-
             reactions.into_iter().fold(
-
                 BTreeMap::<Option<CodeLocation>, Reactions>::new(),
-
                 |mut reactions,
-
                  Reaction {
-
                      location,
-
                      emoji,
-
                      authors,
-
                  }| {
-
                     let mut inner = authors
-
                         .into_iter()
-
                         .map(|author| (author, emoji))
-
                         .collect::<BTreeSet<_>>();
-
                     let entry = reactions.entry(location).or_default();
-
                     entry.append(&mut inner);
-
                     reactions
-
                 },
-
             )
-
         }
-
     }
-
 
-
     /// Helper to serialize a `Revision`'s reactions, since
-
     /// `CodeLocation` cannot be a key for a JSON object.
-
     ///
-
     /// The set `reactions` are first turned into a set of
-
     /// [`Reaction`]s and then serialized via a `Vec`.
-
     pub fn serialize_reactions<S>(
-
         reactions: &BTreeMap<Option<CodeLocation>, Reactions>,
-
         serializer: S,
-
     ) -> Result<S::Ok, S::Error>
-
     where
-
         S: serde::Serializer,
-
     {
-
         let reactions = reactions
-
             .iter()
-
             .flat_map(|(location, reaction)| {
-
                 let reactions = reaction.iter().fold(
-
                     BTreeMap::new(),
-
                     |mut acc: BTreeMap<&super::Reaction, Vec<_>>, (author, emoji)| {
-
                         acc.entry(emoji).or_default().push(*author);
-
                         acc
-
                     },
-
                 );
-
                 reactions
-
                     .into_iter()
-
                     .map(|(emoji, authors)| Reaction {
-
                         location: location.clone(),
-
                         emoji: *emoji,
-
                         authors,
-
                     })
-
                     .collect::<Vec<_>>()
-
             })
-
             .collect::<Vec<_>>();
-
         let mut s = serializer.serialize_seq(Some(reactions.len()))?;
-
         for r in &reactions {
-
             s.serialize_element(r)?;
-
         }
-
         s.end()
-
     }
-
 
-
     /// Helper to deserialize a `Revision`'s reactions, the inverse of
-
     /// `serialize_reactions`.
-
     ///
-
     /// The `Vec` of [`Reaction`]s are deserialized and converted to a
-
     /// `BTreeMap<Option<CodeLocation>, Reactions>`.
-
     pub fn deserialize_reactions<'de, D>(
-
         deserializer: D,
-
     ) -> Result<BTreeMap<Option<CodeLocation>, Reactions>, D::Error>
-
     where
-
         D: serde::Deserializer<'de>,
-
     {
-
         struct ReactionsVisitor;
-
 
-
         impl<'de> serde::de::Visitor<'de> for ReactionsVisitor {
-
             type Value = Vec<Reaction>;
-
 
-
             fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-
                 formatter.write_str("a reaction of the form {'location', 'emoji', 'authors'}")
-
             }
-
 
-
             fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
-
             where
-
                 A: serde::de::SeqAccess<'de>,
-
             {
-
                 let mut reactions = Vec::new();
-
                 while let Some(reaction) = seq.next_element()? {
-
                     reactions.push(reaction);
-
                 }
-
                 Ok(reactions)
-
             }
-
         }
-
 
-
         let reactions = deserializer.deserialize_seq(ReactionsVisitor)?;
-
         Ok(Reaction::as_revision_reactions(reactions))
-
     }
-
 }
-
 
-
 #[cfg(test)]
-
 #[allow(clippy::unwrap_used)]
-
 mod test {
-
-    use std::path::PathBuf;
-
+    use std::path::Path;
-
     use std::str::FromStr;
-
     use std::vec;
-
 
-
     use pretty_assertions::assert_eq;
-
 
-
     use super::*;
-
     use crate::cob::common::CodeRange;
-
     use crate::cob::test::Actor;
-
     use crate::crypto::test::signer::MockSigner;
-
     use crate::identity;
-
     use crate::patch::cache::Patches as _;
-
     use crate::profile::env;
-
     use crate::test;
-
     use crate::test::arbitrary;
-
     use crate::test::arbitrary::gen;
-
     use crate::test::storage::MockRepository;
-
 
-
-    use cob::migrate;
-
+    use cob::{migrate, DiffLocation, HunkIndex};
-
 
-
     #[test]
-
     fn test_json_serialization() {
-
         let edit = Action::Label {
-
             labels: BTreeSet::new(),
-
         };
-
         assert_eq!(
-
             serde_json::to_string(&edit).unwrap(),
-
             String::from(r#"{"type":"label","labels":[]}"#)
-
         );
-
     }
-
 
-
     #[test]
-
     fn test_reactions_json_serialization() {
-
         #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
-
         #[serde(rename_all = "camelCase")]
-
         struct TestReactions {
-
             #[serde(
-
                 serialize_with = "super::ser::serialize_reactions",
-
                 deserialize_with = "super::ser::deserialize_reactions"
-
             )]
-
             inner: BTreeMap<Option<CodeLocation>, Reactions>,
-
         }
-
 
-
         let reactions = TestReactions {
-
             inner: [(
-
                 None,
-
                 [
-
                     (
-
                         "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"
-
                             .parse()
-
                             .unwrap(),
-
                         Reaction::new('🚀').unwrap(),
-
                     ),
-
                     (
-
                         "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
-
                             .parse()
-
                             .unwrap(),
-
                         Reaction::new('🙏').unwrap(),
-
                     ),
-
                 ]
-
                 .into_iter()
-
                 .collect(),
-
             )]
-
             .into_iter()
-
             .collect(),
-
         };
-
 
-
         assert_eq!(
-
             reactions,
-
             serde_json::from_str(&serde_json::to_string(&reactions).unwrap()).unwrap()
-
         );
-
     }
-
 
-
     #[test]
-
     fn test_patch_create_and_get() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let author: Did = alice.signer.public_key().into();
-
         let target = MergeTarget::Delegates;
-
         let patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 target,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let patch_id = patch.id;
-
         let patch = patches.get(&patch_id).unwrap().unwrap();
-
 
-
         assert_eq!(patch.title(), "My first patch");
-
         assert_eq!(patch.description(), "Blah blah blah.");
-
         assert_eq!(patch.author().id(), &author);
-
         assert_eq!(patch.state(), &State::Open { conflicts: vec![] });
-
         assert_eq!(patch.target(), target);
-
         assert_eq!(patch.version(), 0);
-
 
-
         let (rev_id, revision) = patch.latest();
-
 
-
         assert_eq!(revision.author.id(), &author);
-
         assert_eq!(revision.description(), "Blah blah blah.");
-
         assert_eq!(revision.discussion.len(), 0);
-
         assert_eq!(revision.oid, branch.oid);
-
         assert_eq!(revision.base, branch.base);
-
 
-
         let ByRevision { id, .. } = patches.find_by_revision(&rev_id).unwrap().unwrap();
-
         assert_eq!(id, patch_id);
-
     }
-
 
-
     #[test]
-
     fn test_patch_discussion() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let id = patch.id;
-
         let mut patch = patches.get_mut(&id).unwrap();
-
         let (revision_id, _) = patch.revisions().last().unwrap();
-
         assert!(
-
             patch
-
                 .comment(revision_id, "patch comment", None, None, [], &alice.signer)
-
                 .is_ok(),
-
             "can comment on patch"
-
         );
-
 
-
         let (_, revision) = patch.revisions().last().unwrap();
-
         let (_, comment) = revision.discussion.first().unwrap();
-
         assert_eq!("patch comment", comment.body(), "comment body untouched");
-
     }
-
 
-
     #[test]
-
     fn test_patch_merge() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let id = patch.id;
-
         let (rid, _) = patch.revisions().next().unwrap();
-
         let _merge = patch.merge(rid, branch.base, &alice.signer).unwrap();
-
         let patch = patches.get(&id).unwrap().unwrap();
-
 
-
         let merges = patch.merges.iter().collect::<Vec<_>>();
-
         assert_eq!(merges.len(), 1);
-
 
-
         let (merger, merge) = merges.first().unwrap();
-
         assert_eq!(*merger, alice.signer.public_key());
-
         assert_eq!(merge.commit, branch.base);
-
     }
-
 
-
     #[test]
-
     fn test_patch_review() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let (revision_id, _) = patch.latest();
-
         let review_id = patch
-
             .review(
-
                 revision_id,
-
                 Some(Verdict::Accept),
-
                 Some("LGTM".to_owned()),
-
                 vec![],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let id = patch.id;
-
         let mut patch = patches.get_mut(&id).unwrap();
-
         let (_, revision) = patch.latest();
-
         assert_eq!(revision.reviews.len(), 1);
-
 
-
         let review = revision.review_by(alice.signer.public_key()).unwrap();
-
         assert_eq!(review.verdict(), Some(Verdict::Accept));
-
         assert_eq!(review.summary(), Some("LGTM"));
-
 
-
         patch.redact_review(review_id, &alice.signer).unwrap();
-
         patch.reload().unwrap();
-
 
-
         let (_, revision) = patch.latest();
-
         assert_eq!(revision.reviews().count(), 0);
-
 
-
         // This is fine, redacting an already-redacted review is a no-op.
-
         patch.redact_review(review_id, &alice.signer).unwrap();
-
         // If the review never existed, it's an error.
-
         patch
-
             .redact_review(ReviewId(arbitrary::entry_id()), &alice.signer)
-
             .unwrap_err();
-
     }
-
 
-
     #[test]
-
     fn test_patch_review_revision_redact() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let update = checkout.branch_with([("README", b"Hello Radicle!")]);
-
         let updated = patch
-
             .update("I've made changes.", branch.base, update.oid, &alice.signer)
-
             .unwrap();
-
 
-
         // It's fine to redact a review from a redacted revision.
-
         let review = patch
-
             .review(updated, Some(Verdict::Accept), None, vec![], &alice.signer)
-
             .unwrap();
-
         patch.redact(updated, &alice.signer).unwrap();
-
         patch.redact_review(review, &alice.signer).unwrap();
-
     }
-
 
-
     #[test]
-
     fn test_revision_review_merge_redacted() {
-
         let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
-
         let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
-
         let mut alice = Actor::new(MockSigner::default());
-
         let rid = gen::<RepoId>(1);
-
         let doc = RawDoc::new(
-
             gen::<Project>(1),
-
             vec![alice.did()],
-
             1,
-
             identity::Visibility::Public,
-
         )
-
         .verified()
-
         .unwrap();
-
         let repo = MockRepository::new(rid, doc);
-
 
-
         let a1 = alice.op::<Patch>([
-
             Action::Revision {
-
                 description: String::new(),
-
                 base,
-
                 oid,
-
                 resolves: Default::default(),
-
             },
-
             Action::Edit {
-
                 title: String::from("My patch"),
-
                 target: MergeTarget::Delegates,
-
             },
-
         ]);
-
         let a2 = alice.op::<Patch>([Action::Revision {
-
             description: String::from("Second revision"),
-
             base,
-
             oid,
-
             resolves: Default::default(),
-
         }]);
-
         let a3 = alice.op::<Patch>([Action::RevisionRedact {
-
             revision: RevisionId(a2.id()),
-
         }]);
-
         let a4 = alice.op::<Patch>([Action::Review {
-
             revision: RevisionId(a2.id()),
-
             summary: None,
-
             verdict: Some(Verdict::Accept),
-
             labels: vec![],
-
         }]);
-
         let a5 = alice.op::<Patch>([Action::Merge {
-
             revision: RevisionId(a2.id()),
-
             commit: oid,
-
         }]);
-
 
-
         let mut patch = Patch::from_ops([a1, a2], &repo).unwrap();
-
         assert_eq!(patch.revisions().count(), 2);
-
 
-
         patch.op(a3, [], &repo).unwrap();
-
         assert_eq!(patch.revisions().count(), 1);
-
 
-
         patch.op(a4, [], &repo).unwrap();
-
         patch.op(a5, [], &repo).unwrap();
-
     }
-
 
-
     #[test]
-
     fn test_revision_edit_redact() {
-
         let base = arbitrary::oid();
-
         let oid = arbitrary::oid();
-
         let repo = gen::<MockRepository>(1);
-
         let time = env::local_time();
-
         let alice = MockSigner::default();
-
         let bob = MockSigner::default();
-
         let mut h0: cob::test::HistoryBuilder<Patch> = cob::test::history(
-
             &[
-
                 Action::Revision {
-
                     description: String::from("Original"),
-
                     base,
-
                     oid,
-
                     resolves: Default::default(),
-
                 },
-
                 Action::Edit {
-
                     title: String::from("Some patch"),
-
                     target: MergeTarget::Delegates,
-
                 },
-
             ],
-
             time.into(),
-
             &alice,
-
         );
-
         let r1 = h0.commit(
-
             &Action::Revision {
-
                 description: String::from("New"),
-
                 base,
-
                 oid,
-
                 resolves: Default::default(),
-
             },
-
             &alice,
-
         );
-
         let patch = Patch::from_history(&h0, &repo).unwrap();
-
         assert_eq!(patch.revisions().count(), 2);
-
 
-
         let mut h1 = h0.clone();
-
         h1.commit(
-
             &Action::RevisionRedact {
-
                 revision: RevisionId(r1),
-
             },
-
             &alice,
-
         );
-
 
-
         let mut h2 = h0.clone();
-
         h2.commit(
-
             &Action::RevisionEdit {
-
                 revision: RevisionId(*h0.root().id()),
-
                 description: String::from("Edited"),
-
                 embeds: Vec::default(),
-
             },
-
             &bob,
-
         );
-
 
-
         h0.merge(h1);
-
         h0.merge(h2);
-
 
-
         let patch = Patch::from_history(&h0, &repo).unwrap();
-
         assert_eq!(patch.revisions().count(), 1);
-
     }
-
 
-
     #[test]
-
     fn test_revision_reaction() {
-
         let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
-
         let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
-
         let mut alice = Actor::new(MockSigner::default());
-
         let repo = gen::<MockRepository>(1);
-
         let reaction = Reaction::new('👍').expect("failed to create a reaction");
-
 
-
         let a1 = alice.op::<Patch>([
-
             Action::Revision {
-
                 description: String::new(),
-
                 base,
-
                 oid,
-
                 resolves: Default::default(),
-
             },
-
             Action::Edit {
-
                 title: String::from("My patch"),
-
                 target: MergeTarget::Delegates,
-
             },
-
         ]);
-
         let a2 = alice.op::<Patch>([Action::RevisionReact {
-
             revision: RevisionId(a1.id()),
-
             location: None,
-
             reaction,
-
             active: true,
-
         }]);
-
         let patch = Patch::from_ops([a1, a2], &repo).unwrap();
-
 
-
         let (_, r1) = patch.revisions().next().unwrap();
-
         assert!(!r1.reactions.is_empty());
-
 
-
         let mut reactions = r1.reactions.get(&None).unwrap().clone();
-
         assert!(!reactions.is_empty());
-
 
-
         let (_, first_reaction) = reactions.pop_first().unwrap();
-
         assert_eq!(first_reaction, reaction);
-
     }
-
 
-
     #[test]
-
     fn test_patch_review_edit() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let (rid, _) = patch.latest();
-
         let review = patch
-
             .review(
-
                 rid,
-
                 Some(Verdict::Accept),
-
                 Some("LGTM".to_owned()),
-
                 vec![],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
         patch
-
             .review_edit(
-
                 review,
-
                 Some(Verdict::Reject),
-
                 Some("Whoops!".to_owned()),
-
                 vec![],
-
                 &alice.signer,
-
             )
-
             .unwrap(); // Overwrite the comment.
-
 
-
         let (_, revision) = patch.latest();
-
         let review = revision.review_by(alice.signer.public_key()).unwrap();
-
         assert_eq!(review.verdict(), Some(Verdict::Reject));
-
         assert_eq!(review.summary(), Some("Whoops!"));
-
     }
-
 
-
     #[test]
-
     fn test_patch_review_duplicate() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let (rid, _) = patch.latest();
-
         patch
-
             .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
-
             .unwrap();
-
         patch
-
             .review(rid, Some(Verdict::Reject), None, vec![], &alice.signer)
-
             .unwrap(); // This review is ignored, since there is already a review by this author.
-
 
-
         let (_, revision) = patch.latest();
-
         let review = revision.review_by(alice.signer.public_key()).unwrap();
-
         assert_eq!(review.verdict(), Some(Verdict::Accept));
-
     }
-
 
-
     #[test]
-
     fn test_patch_review_edit_comment() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let (rid, _) = patch.latest();
-
         let review = patch
-
             .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
-
             .unwrap();
-
         patch
-
             .review_comment(review, "First comment!", None, None, [], &alice.signer)
-
             .unwrap();
-
 
-
         let _review = patch
-
             .review_edit(review, Some(Verdict::Reject), None, vec![], &alice.signer)
-
             .unwrap();
-
         patch
-
             .review_comment(review, "Second comment!", None, None, [], &alice.signer)
-
             .unwrap();
-
 
-
         let (_, revision) = patch.latest();
-
         let review = revision.review_by(alice.signer.public_key()).unwrap();
-
         assert_eq!(review.verdict(), Some(Verdict::Reject));
-
         assert_eq!(review.comments().count(), 2);
-
         assert_eq!(review.comments().nth(0).unwrap().1.body(), "First comment!");
-
         assert_eq!(
-
             review.comments().nth(1).unwrap().1.body(),
-
             "Second comment!"
-
         );
-
     }
-
 
-
     #[test]
-
     fn test_patch_review_comment() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let (rid, _) = patch.latest();
-
-        let location = CodeLocation {
-
-            commit: branch.oid,
-
-            path: PathBuf::from_str("README").unwrap(),
-
-            old: None,
-
-            new: Some(CodeRange::Lines { range: 5..8 }),
-
+        let location = DiffLocation {
-
+            base: *patch.base(),
-
+            head: *patch.head(),
-
+            path: Path::new("README").to_path_buf(),
-
+            selection: Some(HunkIndex::new(1, CodeRange::lines(5..8))),
-
         };
-
         let review = patch
-
             .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer)
-
             .unwrap();
-
         patch
-
             .review_comment(
-
                 review,
-
                 "I like these lines of code",
-
                 Some(location.clone()),
-
                 None,
-
                 [],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let (_, revision) = patch.latest();
-
         let review = revision.review_by(alice.signer.public_key()).unwrap();
-
         let (_, comment) = review.comments().next().unwrap();
-
 
-
         assert_eq!(comment.body(), "I like these lines of code");
-
-        assert_eq!(comment.location(), Some(&location));
-
+        assert_eq!(comment.location(), Some(&location.into()));
-
     }
-
 
-
     #[test]
-
     fn test_patch_review_remove_summary() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = Cache::no_cache(&*alice.repo).unwrap();
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         let (rid, _) = patch.latest();
-
         let review = patch
-
             .review(
-
                 rid,
-
                 Some(Verdict::Accept),
-
                 Some("Nah".to_owned()),
-
                 vec![],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
         patch
-
             .review_edit(review, Some(Verdict::Accept), None, vec![], &alice.signer)
-
             .unwrap();
-
 
-
         let id = patch.id;
-
         let patch = patches.get_mut(&id).unwrap();
-
         let (_, revision) = patch.latest();
-
         let review = revision.review_by(alice.signer.public_key()).unwrap();
-
 
-
         assert_eq!(review.verdict(), Some(Verdict::Accept));
-
         assert_eq!(review.summary(), None);
-
     }
-
 
-
     #[test]
-
     fn test_patch_update() {
-
         let alice = test::setup::NodeWithRepo::default();
-
         let checkout = alice.repo.checkout();
-
         let branch = checkout.branch_with([("README", b"Hello World!")]);
-
         let mut patches = {
-
             let path = alice.tmp.path().join("cobs.db");
-
             let mut db = cob::cache::Store::open(path).unwrap();
-
             let store = cob::patch::Patches::open(&*alice.repo).unwrap();
-
 
-
             db.migrate(migrate::ignore).unwrap();
-
             cob::patch::Cache::open(store, db)
-
         };
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
 
-
         assert_eq!(patch.description(), "Blah blah blah.");
-
         assert_eq!(patch.version(), 0);
-
 
-
         let update = checkout.branch_with([("README", b"Hello Radicle!")]);
-
         let _ = patch
-
             .update("I've made changes.", branch.base, update.oid, &alice.signer)
-
             .unwrap();
-
 
-
         let id = patch.id;
-
         let patch = patches.get(&id).unwrap().unwrap();
-
         assert_eq!(patch.version(), 1);
-
         assert_eq!(patch.revisions.len(), 2);
-
         assert_eq!(patch.revisions().count(), 2);
-
         assert_eq!(
-
             patch.revisions().nth(0).unwrap().1.description(),
-
             "Blah blah blah."
-
         );
-
         assert_eq!(
-
             patch.revisions().nth(1).unwrap().1.description(),
-
             "I've made changes."
-
         );
-
 
-
         let (_, revision) = patch.latest();
-
 
-
         assert_eq!(patch.version(), 1);
-
         assert_eq!(revision.oid, update.oid);
-
         assert_eq!(revision.description(), "I've made changes.");
-
     }
-
 
-
     #[test]
-
     fn test_patch_redact() {
-
         let alice = test::setup::Node::default();
-
         let repo = alice.project();
-
         let branch = repo
-
             .checkout()
-
             .branch_with([("README.md", b"Hello, World!")]);
-
         let mut patches = Cache::no_cache(&*repo).unwrap();
-
         let mut patch = patches
-
             .create(
-
                 "My first patch",
-
                 "Blah blah blah.",
-
                 MergeTarget::Delegates,
-
                 branch.base,
-
                 branch.oid,
-
                 &[],
-
                 &alice.signer,
-
             )
-
             .unwrap();
-
         let patch_id = patch.id;
-
 
-
         let update = repo
-
             .checkout()
-
             .branch_with([("README.md", b"Hello, Radicle!")]);
-
         let revision_id = patch
-
             .update("I've made changes.", branch.base, update.oid, &alice.signer)
-
             .unwrap();
-
         assert_eq!(patch.revisions().count(), 2);
-
 
-
         patch.redact(revision_id, &alice.signer).unwrap();
-
         assert_eq!(patch.latest().0, RevisionId(*patch_id));
-
         assert_eq!(patch.revisions().count(), 1);
-
 
-
         // The patch's root must always exist.
-
         assert_eq!(patch.latest(), patch.root());
-
         assert!(patch.redact(patch.latest().0, &alice.signer).is_err());
-
     }
-
 
-
     #[test]
-
     fn test_json() {
-
         use serde_json::json;
-
 
-
         assert_eq!(
-
             serde_json::to_value(Action::Lifecycle {
-
                 state: Lifecycle::Draft
-
             })
-
             .unwrap(),
-
             json!({
-
                 "type": "lifecycle",
-
                 "state": { "status": "draft" }
-
             })
-
         );
-
 
-
         let revision = RevisionId(arbitrary::entry_id());
-
         assert_eq!(
-
             serde_json::to_value(Action::Review {
-
                 revision,
-
                 summary: None,
-
                 verdict: None,
-
                 labels: vec![],
-
             })
-
             .unwrap(),
-
             json!({
-
                 "type": "review",
-
                 "revision": revision,
-
             })
-
         );
-
 
-
         assert_eq!(
-
             serde_json::to_value(CodeRange::Lines { range: 4..8 }).unwrap(),
-
             json!({
-
                 "type": "lines",
-
                 "range": { "start": 4, "end": 8 },
-
             })
-
         );
-
     }
-
 }
-
>>>>>>> Conflict 1 of 1 ends