Radish alpha
r
Radicle CI broker
Radicle
Git (anonymous pull)
Log in to clone via SSH
refactor: node and CI event and trigger message creation
Lars Wirzenius committed 1 year ago
commit bb9e9f27bc34f33f15215654a4d6ba7ad8b99d09
parent 1bc0ad40dc9aeb2c4631f86b6305869105d45bce
3 files changed +225 -324
modified src/ci_event.rs
@@ -58,64 +58,57 @@ pub enum CiEventV1 {

impl CiEvent {
    pub fn branch_created(
-
        node: NodeId,
+
        from_node: NodeId,
        repo: RepoId,
        branch: &str,
        tip: Oid,
    ) -> Result<Self, CiEventError> {
        Ok(Self::V1(CiEventV1::BranchCreated {
-
            from_node: node,
+
            from_node,
            repo,
-
            branch: RefString::try_from(branch)
-
                .map_err(|e| CiEventError::RefString(branch.into(), e))?,
+
            branch: ref_string(branch)?,
            tip,
        }))
    }

    pub fn branch_updated(
-
        node: NodeId,
+
        from_node: NodeId,
        repo: RepoId,
        branch: &str,
        tip: Oid,
        old_tip: Oid,
    ) -> Result<Self, CiEventError> {
-
        let branch =
-
            namespaced_branch(branch).map_err(|_| CiEventError::without_namespace2(branch))?;
        Ok(Self::V1(CiEventV1::BranchUpdated {
-
            from_node: node,
+
            from_node,
            repo,
-
            branch: RefString::try_from(branch.clone())
-
                .map_err(|e| CiEventError::RefString(branch.clone(), e))?,
+
            branch: branch_from_namspeced_ref(branch)?,
            tip,
            old_tip,
        }))
    }

    pub fn branch_deleted(
-
        node: NodeId,
+
        from_node: NodeId,
        repo: RepoId,
        branch: &str,
        tip: Oid,
    ) -> Result<Self, CiEventError> {
-
        let branch =
-
            namespaced_branch(branch).map_err(|_| CiEventError::without_namespace2(branch))?;
        Ok(Self::V1(CiEventV1::BranchDeleted {
-
            from_node: node,
+
            from_node,
            repo,
-
            branch: RefString::try_from(branch.clone())
-
                .map_err(|e| CiEventError::RefString(branch.clone(), e))?,
+
            branch: branch_from_namspeced_ref(branch)?,
            tip,
        }))
    }

    pub fn patch_created(
-
        node: NodeId,
+
        from_node: NodeId,
        repo: RepoId,
        patch: PatchId,
        tip: Oid,
    ) -> Result<Self, CiEventError> {
        Ok(Self::V1(CiEventV1::PatchCreated {
-
            from_node: node,
+
            from_node,
            repo,
            patch,
            new_tip: tip,
@@ -123,27 +116,23 @@ impl CiEvent {
    }

    pub fn patch_updated(
-
        node: NodeId,
+
        from_node: NodeId,
        repo: RepoId,
        patch: PatchId,
-
        tip: Oid,
+
        new_tip: Oid,
    ) -> Result<Self, CiEventError> {
        Ok(Self::V1(CiEventV1::PatchUpdated {
-
            from_node: node,
+
            from_node,
            repo,
            patch,
-
            new_tip: tip,
+
            new_tip,
        }))
    }

    pub fn from_node_event(event: &Event) -> Result<Vec<Self>, CiEventError> {
-
        fn ref_string(s: String) -> Result<RefString, CiEventError> {
-
            RefString::try_from(s.clone()).map_err(|e| CiEventError::ref_string(s, e))
-
        }
-

        fn branch(ref_name: &str, update: &RefUpdate) -> Result<RefString, CiEventError> {
            ref_string(
-
                namespaced_branch(ref_name)
+
                &namespaced_branch(ref_name)
                    .map_err(|_| CiEventError::without_namespace(ref_name, update.clone()))?,
            )
        }
@@ -159,52 +148,29 @@ impl CiEvent {
                    let e = match update {
                        RefUpdate::Created { name, oid } => {
                            if let Ok(patch_id) = patch_id(name) {
-
                                CiEventV1::PatchCreated {
-
                                    from_node: *remote,
-
                                    repo: *rid,
-
                                    patch: patch_id,
-
                                    new_tip: *oid,
-
                                }
+
                                Self::patch_created(*remote, *rid, patch_id, *oid)?
                            } else if let Ok(branch) = namespaced_branch(name) {
-
                                CiEventV1::BranchCreated {
-
                                    from_node: *remote,
-
                                    repo: *rid,
-
                                    branch: ref_string(branch)?,
-
                                    tip: *oid,
-
                                }
+
                                Self::branch_created(*remote, *rid, &branch, *oid)?
                            } else {
                                continue;
                            }
                        }
                        RefUpdate::Updated { name, old, new } => {
                            if let Ok(patch_id) = patch_id(name) {
-
                                CiEventV1::PatchUpdated {
-
                                    from_node: *remote,
-
                                    repo: *rid,
-
                                    patch: patch_id,
-
                                    new_tip: *new,
-
                                }
-
                            } else if let Ok(branch) = namespaced_branch(name) {
-
                                CiEventV1::BranchUpdated {
-
                                    from_node: *remote,
-
                                    repo: *rid,
-
                                    branch: ref_string(branch)?,
-
                                    tip: *new,
-
                                    old_tip: *old,
-
                                }
+
                                Self::patch_updated(*remote, *rid, patch_id, *new)?
+
                            } else if namespaced_branch(name).is_ok() {
+
                                Self::branch_updated(*remote, *rid, name, *new, *old)?
                            } else {
                                continue;
                            }
                        }
-
                        RefUpdate::Deleted { name, oid } => CiEventV1::BranchDeleted {
-
                            from_node: *remote,
-
                            repo: *rid,
-
                            branch: branch(name, update)?,
-
                            tip: *oid,
-
                        },
+
                        RefUpdate::Deleted { name, oid } => {
+
                            branch(name, update)?; // check that there is a name spaced branch name
+
                            Self::branch_deleted(*remote, *rid, name, *oid)?
+
                        }
                        RefUpdate::Skipped { .. } => continue,
                    };
-
                    events.push(CiEvent::V1(e));
+
                    events.push(e);
                }
                Ok(events)
            }
@@ -222,6 +188,14 @@ impl CiEvent {
    }
}

+
fn ref_string(s: &str) -> Result<RefString, CiEventError> {
+
    RefString::try_from(s).map_err(|e| CiEventError::ref_string(s, e))
+
}
+

+
fn branch_from_namspeced_ref(branch: &str) -> Result<RefString, CiEventError> {
+
    ref_string(&namespaced_branch(branch).map_err(|_| CiEventError::without_namespace2(branch))?)
+
}
+

pub struct CiEvents {
    events: Vec<CiEvent>,
}
@@ -271,8 +245,8 @@ impl CiEventError {
        Self::WithoutNamespace2(refname.into())
    }

-
    fn ref_string(name: String, err: radicle::git::fmt::Error) -> Self {
-
        Self::RefString(name, err)
+
    fn ref_string(name: &str, err: radicle::git::fmt::Error) -> Self {
+
        Self::RefString(name.into(), err)
    }

    fn read_file(filename: &Path, err: std::io::Error) -> Self {
modified src/msg.rs
@@ -20,6 +20,7 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;

pub use radicle::{
+
    cob::patch::PatchId,
    git::Oid,
    prelude::{NodeId, RepoId},
};
@@ -131,6 +132,95 @@ impl<'a> RequestBuilder<'a> {

    /// Create a [`Request::Trigger``] message from a [`crate::ci_event::Civet`].
    pub fn build_trigger_from_ci_event(self) -> Result<Request, MessageError> {
+
        fn repository(repo: &RepoId, profile: &Profile) -> Result<Repository, MessageError> {
+
            logger::debug2(format!("build trigger: look up repository {repo}"));
+
            let rad_repo = profile.storage.repository(*repo)?;
+
            let project_info = rad_repo.project()?;
+
            Ok(Repository {
+
                id: *repo,
+
                name: project_info.name().to_string(),
+
                description: project_info.description().to_string(),
+
                private: !rad_repo.identity()?.visibility().is_public(),
+
                default_branch: project_info.default_branch().to_string(),
+
                delegates: rad_repo.delegates()?.iter().copied().collect(),
+
            })
+
        }
+

+
        fn common_fields(
+
            event_type: EventType,
+
            repo: &RepoId,
+
            profile: &Profile,
+
        ) -> Result<EventCommonFields, MessageError> {
+
            logger::debug2(format!("build trigger: create common fields for {repo}"));
+
            Ok(EventCommonFields {
+
                version: PROTOCOL_VERSION,
+
                event_type,
+
                repository: repository(repo, profile)?,
+
            })
+
        }
+

+
        fn author(node: &NodeId, profile: &Profile) -> Result<Author, MessageError> {
+
            logger::debug2(format!("build trigger: look up author {node}"));
+
            let did = Did::from(*node);
+
            did_to_author(profile, &did)
+
        }
+

+
        fn commits(
+
            git_repo: &radicle_surf::Repository,
+
            tip: Oid,
+
            base: Oid,
+
        ) -> Result<Vec<Oid>, radicle_surf::Error> {
+
            logger::debug2(format!("build trigger: look commits {git_repo:?}"));
+
            git_repo
+
                .history(tip)?
+
                .take_while(|c| if let Ok(c) = c { c.id != base } else { false })
+
                .map(|r| r.map(|c| c.id))
+
                .collect::<Result<Vec<Oid>, _>>()
+
        }
+

+
        fn patch_cob(
+
            rad_repo: &radicle::storage::git::Repository,
+
            patch_id: &PatchId,
+
        ) -> Result<radicle::cob::patch::Patch, MessageError> {
+
            logger::debug2(format!("build trigger: look patch cob {patch_id}"));
+
            patch::Patches::open(rad_repo)?
+
                .get(patch_id)?
+
                .ok_or(MessageError::PatchCob(*patch_id))
+
        }
+

+
        fn revisions(
+
            patch_cob: &radicle::cob::patch::Patch,
+
            author: &Author,
+
        ) -> Result<Vec<Revision>, MessageError> {
+
            logger::debug2(format!("build trigger: look patch revisions by {author:?}"));
+
            patch_cob
+
                .revisions()
+
                .map(|(rid, r)| {
+
                    Ok::<Revision, MessageError>(Revision {
+
                        id: rid.into(),
+
                        author: author.clone(),
+
                        description: r.description().to_string(),
+
                        base: *r.base(),
+
                        oid: r.head(),
+
                        timestamp: r.timestamp().as_secs(),
+
                    })
+
                })
+
                .collect::<Result<Vec<Revision>, MessageError>>()
+
        }
+

+
        fn patch_base(
+
            patch_cob: &radicle::cob::patch::Patch,
+
            patch_id: &PatchId,
+
            author: &Author,
+
        ) -> Result<Oid, MessageError> {
+
            logger::debug2(format!("build trigger: look patch base {patch_id}"));
+
            let author_pk = radicle::crypto::PublicKey::from(author.id);
+
            let (_id, revision) = patch_cob
+
                .latest_by(&author_pk)
+
                .ok_or(MessageError::LatestPatchRevision(*patch_id))?;
+
            Ok(*revision.base())
+
        }
+

        let profile = self.profile.ok_or(MessageError::NoProfile)?;

        match self.ci_event {
@@ -141,35 +231,16 @@ impl<'a> RequestBuilder<'a> {
                branch,
                tip,
            })) => {
-
                let rad_repo = profile.storage.repository(*repo)?;
-
                let project_info = rad_repo.project()?;
-

-
                let common = EventCommonFields {
-
                    version: PROTOCOL_VERSION,
-
                    event_type: EventType::Push,
-
                    repository: Repository {
-
                        id: *repo,
-
                        name: project_info.name().to_string(),
-
                        description: project_info.description().to_string(),
-
                        private: !rad_repo.identity()?.visibility().is_public(),
-
                        default_branch: project_info.default_branch().to_string(),
-
                        delegates: rad_repo.delegates()?.iter().copied().collect(),
-
                    },
-
                };
-

-
                let did = Did::from(*from_node);
-
                let pusher = did_to_author(profile, &did)?;
-

-
                let push = PushEvent {
-
                    pusher,
-
                    before: *tip, // Branch created: we only use the tip
-
                    after: *tip,
-
                    branch: branch.as_str().to_string(),
-
                    commits: vec![*tip], // Branch created, only use tip.
-
                };
+
                logger::debug("build trigger: branch created");
                Ok(Request::Trigger {
-
                    common,
-
                    push: Some(push),
+
                    common: common_fields(EventType::Push, repo, profile)?,
+
                    push: Some(PushEvent {
+
                        pusher: author(from_node, profile)?,
+
                        before: *tip, // Branch created: we only use the tip
+
                        after: *tip,
+
                        branch: branch.as_str().to_string(),
+
                        commits: vec![*tip], // Branch created, only use tip.
+
                    }),
                    patch: None,
                })
            }
@@ -180,53 +251,23 @@ impl<'a> RequestBuilder<'a> {
                tip,
                old_tip,
            })) => {
-
                let rad_repo = profile.storage.repository(*repo)?;
+
                logger::debug("build trigger: branch updated");
                let git_repo =
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))?;
-
                let project_info = rad_repo.project()?;
-

-
                let common = EventCommonFields {
-
                    version: PROTOCOL_VERSION,
-
                    event_type: EventType::Push,
-
                    repository: Repository {
-
                        id: *repo,
-
                        name: project_info.name().to_string(),
-
                        description: project_info.description().to_string(),
-
                        private: !rad_repo.identity()?.visibility().is_public(),
-
                        default_branch: project_info.default_branch().to_string(),
-
                        delegates: rad_repo.delegates()?.iter().copied().collect(),
-
                    },
-
                };
-

-
                let did = Did::from(*from_node);
-
                let pusher = did_to_author(profile, &did)?;
-

-
                let mut commits: Vec<Oid> = git_repo
-
                    .history(tip)?
-
                    .take_while(|c| {
-
                        if let Ok(c) = c {
-
                            c.id != *old_tip
-
                        } else {
-
                            false
-
                        }
-
                    })
-
                    .map(|r| r.map(|c| c.id))
-
                    .collect::<Result<Vec<Oid>, _>>()?;
+
                let mut commits = commits(&git_repo, *tip, *old_tip)?;
                if commits.is_empty() {
                    commits = vec![*old_tip];
                }

-
                let push = PushEvent {
-
                    pusher,
-
                    before: *tip, // Branch created: we only use the tip
-
                    after: *tip,
-
                    branch: branch.as_str().to_string(),
-
                    commits,
-
                };
-

                Ok(Request::Trigger {
-
                    common,
-
                    push: Some(push),
+
                    common: common_fields(EventType::Push, repo, profile)?,
+
                    push: Some(PushEvent {
+
                        pusher: author(from_node, profile)?,
+
                        before: *tip, // Branch created: we only use the tip
+
                        after: *tip,
+
                        branch: branch.as_str().to_string(),
+
                        commits,
+
                    }),
                    patch: None,
                })
            }
@@ -236,38 +277,16 @@ impl<'a> RequestBuilder<'a> {
                branch,
                tip,
            })) => {
-
                let rad_repo = profile.storage.repository(*repo)?;
-
                let project_info = rad_repo.project()?;
-

-
                let common = EventCommonFields {
-
                    version: PROTOCOL_VERSION,
-
                    event_type: EventType::Push,
-
                    repository: Repository {
-
                        id: *repo,
-
                        name: project_info.name().to_string(),
-
                        description: project_info.description().to_string(),
-
                        private: !rad_repo.identity()?.visibility().is_public(),
-
                        default_branch: project_info.default_branch().to_string(),
-
                        delegates: rad_repo.delegates()?.iter().copied().collect(),
-
                    },
-
                };
-

-
                let did = Did::from(*from_node);
-
                let pusher = did_to_author(profile, &did)?;
-

-
                let commits = vec![*tip];
-

-
                let push = PushEvent {
-
                    pusher,
-
                    before: *tip, // Branch created: we only use the tip
-
                    after: *tip,
-
                    branch: branch.as_str().to_string(),
-
                    commits,
-
                };
-

+
                logger::debug("build trigger: branch deleted");
                Ok(Request::Trigger {
-
                    common,
-
                    push: Some(push),
+
                    common: common_fields(EventType::Push, repo, profile)?,
+
                    push: Some(PushEvent {
+
                        pusher: author(from_node, profile)?,
+
                        before: *tip, // Branch created: we only use the tip
+
                        after: *tip,
+
                        branch: branch.as_str().to_string(),
+
                        commits: vec![*tip],
+
                    }),
                    patch: None,
                })
            }
@@ -277,100 +296,40 @@ impl<'a> RequestBuilder<'a> {
                patch: patch_id,
                new_tip,
            })) => {
-
                logger::debug("create Trigger from PatchCreated: open rad repository");
+
                logger::debug("build trigger: patch created");
                let rad_repo = profile.storage.repository(*repo)?;
-
                logger::debug("open git repository");
                let git_repo =
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))?;
-
                logger::debug("open project");
-
                let project_info = rad_repo.project()?;
-

-
                logger::debug("create common fields");
-
                let common = EventCommonFields {
-
                    version: PROTOCOL_VERSION,
-
                    event_type: EventType::Patch,
-
                    repository: Repository {
-
                        id: *repo,
-
                        name: project_info.name().to_string(),
-
                        description: project_info.description().to_string(),
-
                        private: !rad_repo.identity()?.visibility().is_public(),
-
                        default_branch: project_info.default_branch().to_string(),
-
                        delegates: rad_repo.delegates()?.iter().copied().collect(),
-
                    },
-
                };
-

-
                logger::debug("look up did and author");
-
                let did = Did::from(*from_node);
-
                let author = did_to_author(profile, &did)?;
-

-
                logger::debug("look up patch COB");
-
                let patch_cob = patch::Patches::open(&rad_repo)?
-
                    .get(patch_id)?
-
                    .ok_or(MessageError::Trigger)?;
-

-
                logger::debug("look up revisions");
-
                let revisions: Vec<Revision> = patch_cob
-
                    .revisions()
-
                    .map(|(rid, r)| {
-
                        Ok::<Revision, MessageError>(Revision {
-
                            id: rid.into(),
-
                            author: did_to_author(profile, r.author().id())?,
-
                            description: r.description().to_string(),
-
                            base: *r.base(),
-
                            oid: r.head(),
-
                            timestamp: r.timestamp().as_secs(),
-
                        })
-
                    })
-
                    .collect::<Result<Vec<Revision>, MessageError>>()?;
-
                logger::debug("look up author public key");
-
                let patch_author_pk = radicle::crypto::PublicKey::from(author.id);
-
                logger::debug("look up latest revision");
-
                let patch_latest_revision = patch_cob
-
                    .latest_by(&patch_author_pk)
-
                    .ok_or(MessageError::Trigger)?;
-
                logger::debug("look up patch base");
-
                let patch_base = patch_latest_revision.1.base();
-
                logger::debug("look up commits");
-
                let commits: Vec<Oid> = git_repo
-
                    .history(*new_tip)?
-
                    .take_while(|c| {
-
                        if let Ok(c) = c {
-
                            c.id != *patch_base
-
                        } else {
-
                            false
-
                        }
-
                    })
-
                    .map(|r| r.map(|c| c.id))
-
                    .collect::<Result<Vec<Oid>, _>>()?;
-

-
                logger::debug("construct Patch");
-
                let patch = Patch {
-
                    id: **patch_id,
-
                    author,
-
                    title: patch_cob.title().to_string(),
-
                    state: State {
-
                        status: patch_cob.state().to_string(),
-
                        conflicts: match patch_cob.state() {
-
                            patch::State::Open { conflicts, .. } => conflicts.to_vec(),
-
                            _ => vec![],
-
                        },
-
                    },
-
                    before: *patch_base,
-
                    after: *new_tip,
-
                    commits,
-
                    target: patch_cob.target().head(&rad_repo)?,
-
                    labels: patch_cob.labels().map(|l| l.name().to_string()).collect(),
-
                    assignees: patch_cob.assignees().collect(),
-
                    revisions,
-
                };
-

-
                logger::debug("construct Trigger");
+
                let author = author(from_node, profile)?;
+
                let patch_cob = patch_cob(&rad_repo, patch_id)?;
+
                let revisions = revisions(&patch_cob, &author)?;
+
                let patch_base = patch_base(&patch_cob, patch_id, &author)?;
+
                let commits = commits(&git_repo, *new_tip, patch_base)?;
+

                Ok(Request::Trigger {
-
                    common,
+
                    common: common_fields(EventType::Patch, repo, profile)?,
                    push: None,
                    patch: Some(PatchEvent {
                        action: PatchAction::Created,
-
                        patch,
+
                        patch: Patch {
+
                            id: **patch_id,
+
                            author,
+
                            title: patch_cob.title().to_string(),
+
                            state: State {
+
                                status: patch_cob.state().to_string(),
+
                                conflicts: match patch_cob.state() {
+
                                    patch::State::Open { conflicts, .. } => conflicts.to_vec(),
+
                                    _ => vec![],
+
                                },
+
                            },
+
                            before: patch_base,
+
                            after: *new_tip,
+
                            commits,
+
                            target: patch_cob.target().head(&rad_repo)?,
+
                            labels: patch_cob.labels().map(|l| l.name().to_string()).collect(),
+
                            assignees: patch_cob.assignees().collect(),
+
                            revisions,
+
                        },
                    }),
                })
            }
@@ -380,87 +339,40 @@ impl<'a> RequestBuilder<'a> {
                patch: patch_id,
                new_tip,
            })) => {
+
                logger::debug("build trigger: patch updated");
                let rad_repo = profile.storage.repository(*repo)?;
                let git_repo =
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))?;
-
                let project_info = rad_repo.project()?;
-

-
                let common = EventCommonFields {
-
                    version: PROTOCOL_VERSION,
-
                    event_type: EventType::Patch,
-
                    repository: Repository {
-
                        id: *repo,
-
                        name: project_info.name().to_string(),
-
                        description: project_info.description().to_string(),
-
                        private: !rad_repo.identity()?.visibility().is_public(),
-
                        default_branch: project_info.default_branch().to_string(),
-
                        delegates: rad_repo.delegates()?.iter().copied().collect(),
-
                    },
-
                };
-

-
                let did = Did::from(*from_node);
-
                let author = did_to_author(profile, &did)?;
-

-
                let patch_cob = patch::Patches::open(&rad_repo)?
-
                    .get(patch_id)?
-
                    .ok_or(MessageError::Trigger)?;
-

-
                let revisions: Vec<Revision> = patch_cob
-
                    .revisions()
-
                    .map(|(rid, r)| {
-
                        Ok::<Revision, MessageError>(Revision {
-
                            id: rid.into(),
-
                            author: did_to_author(profile, r.author().id())?,
-
                            description: r.description().to_string(),
-
                            base: *r.base(),
-
                            oid: r.head(),
-
                            timestamp: r.timestamp().as_secs(),
-
                        })
-
                    })
-
                    .collect::<Result<Vec<Revision>, MessageError>>()?;
-
                let patch_author_pk = radicle::crypto::PublicKey::from(author.id);
-
                let patch_latest_revision = patch_cob
-
                    .latest_by(&patch_author_pk)
-
                    .ok_or(MessageError::Trigger)?;
-
                let patch_base = patch_latest_revision.1.base();
-
                let commits: Vec<Oid> = git_repo
-
                    .history(*new_tip)?
-
                    .take_while(|c| {
-
                        if let Ok(c) = c {
-
                            c.id != *patch_base
-
                        } else {
-
                            false
-
                        }
-
                    })
-
                    .map(|r| r.map(|c| c.id))
-
                    .collect::<Result<Vec<Oid>, _>>()?;
-

-
                let patch = Patch {
-
                    id: **patch_id,
-
                    author,
-
                    title: patch_cob.title().to_string(),
-
                    state: State {
-
                        status: patch_cob.state().to_string(),
-
                        conflicts: match patch_cob.state() {
-
                            patch::State::Open { conflicts, .. } => conflicts.to_vec(),
-
                            _ => vec![],
-
                        },
-
                    },
-
                    before: *patch_base,
-
                    after: *new_tip,
-
                    commits,
-
                    target: patch_cob.target().head(&rad_repo)?,
-
                    labels: patch_cob.labels().map(|l| l.name().to_string()).collect(),
-
                    assignees: patch_cob.assignees().collect(),
-
                    revisions,
-
                };
+
                let author = author(from_node, profile)?;
+
                let patch_cob = patch_cob(&rad_repo, patch_id)?;
+
                let revisions = revisions(&patch_cob, &author)?;
+
                let patch_base = patch_base(&patch_cob, patch_id, &author)?;
+
                let commits = commits(&git_repo, *new_tip, patch_base)?;

                Ok(Request::Trigger {
-
                    common,
+
                    common: common_fields(EventType::Patch, repo, profile)?,
                    push: None,
                    patch: Some(PatchEvent {
                        action: PatchAction::Updated,
-
                        patch,
+
                        patch: Patch {
+
                            id: **patch_id,
+
                            author,
+
                            title: patch_cob.title().to_string(),
+
                            state: State {
+
                                status: patch_cob.state().to_string(),
+
                                conflicts: match patch_cob.state() {
+
                                    patch::State::Open { conflicts, .. } => conflicts.to_vec(),
+
                                    _ => vec![],
+
                                },
+
                            },
+
                            before: patch_base,
+
                            after: *new_tip,
+
                            commits,
+
                            target: patch_cob.target().head(&rad_repo)?,
+
                            labels: patch_cob.labels().map(|l| l.name().to_string()).collect(),
+
                            assignees: patch_cob.assignees().collect(),
+
                            revisions,
+
                        },
                    }),
                })
            }
@@ -953,6 +865,14 @@ pub enum MessageError {
    #[error("could not generate trigger from event")]
    Trigger,

+
    /// Error looking up the patch COB.
+
    #[error("could look up patch COB {0}: not found?")]
+
    PatchCob(PatchId),
+

+
    /// Error looking up latest revision for a patch COB.
+
    #[error("failed to look up latest revision for patch {0}")]
+
    LatestPatchRevision(PatchId),
+

    /// Error from Radicle storage.
    #[error(transparent)]
    StorageError(#[from] radicle::storage::Error),
modified src/queueproc.rs
@@ -116,7 +116,8 @@ impl QueueProcessor {
    }

    fn process_event(&mut self, event: &CiEvent) -> Result<bool, QueueError> {
-
        match event {
+
        logger::debug2(format!("queproc::process_event: called; event={event:#?}"));
+
        let x = match event {
            CiEvent::V1(CiEventV1::Shutdown) => {
                logger::queueproc_action_shutdown();
                Ok(true)
@@ -196,18 +197,24 @@ impl QueueProcessor {
                patch: _,
                new_tip,
            }) => {
+
                logger::debug("patch updated");
                logger::queueproc_action_run(repo, new_tip);
                let trigger = RequestBuilder::default()
                    .profile(&self.profile)
                    .ci_event(event)
                    .build_trigger_from_ci_event()
-
                    .map_err(|e| QueueError::build_trigger(event, e))?;
+
                    .map_err(|e| QueueError::build_trigger(event, e));
+
                logger::debug2(format!("got trigger {trigger:?}"));
+
                let trigger = trigger?;
                self.broker
                    .execute_ci(&trigger, &self.run_tx)
                    .map_err(QueueError::execute_ci)?;
+
                logger::debug("executed ci");
                Ok(false)
            }
-
        }
+
        };
+
        logger::debug("queproc::process_event: end");
+
        x
    }

    fn drop_event(&mut self, id: &QueueId) -> Result<(), QueueError> {