Radish alpha
r
Radicle CI broker
Radicle
Git (anonymous pull)
Log in to clone via SSH
feat(src/msg.rs): build trigger message from CI event
Lars Wirzenius committed 1 year ago
commit 42d6184967e3e184f044aa468ab5fe0532bd457e
parent 31ee715d30cf047f71027f80455223f315e2ef75
1 file changed +563 -1
modified src/msg.rs
@@ -31,7 +31,10 @@ use radicle::{
    Profile,
};

-
use crate::event::{parse_ref, push_branch, BrokerEvent, ParsedRef};
+
use crate::{
+
    ci_event::CiEvent,
+
    event::{parse_ref, push_branch, BrokerEvent, ParsedRef},
+
};

// This gets put into every [`Request`] message so the adapter can
// detect its getting a message it knows how to handle.
@@ -111,6 +114,7 @@ impl fmt::Display for RunResult {
pub struct RequestBuilder<'a> {
    profile: Option<&'a Profile>,
    event: Option<&'a BrokerEvent>,
+
    ci_event: Option<&'a CiEvent>,
}

impl<'a> RequestBuilder<'a> {
@@ -126,6 +130,297 @@ impl<'a> RequestBuilder<'a> {
        self
    }

+
    /// Set the CI event to use.
+
    pub fn ci_event(mut self, event: &'a CiEvent) -> Self {
+
        self.ci_event = Some(event);
+
        self
+
    }
+

+
    /// Create a [`Request::Trigger``] message from a [`crate::ci_event::Civet`].
+
    pub fn build_trigger_from_ci_event(self) -> Result<Request, MessageError> {
+
        let profile = self.profile.ok_or(MessageError::NoProfile)?;
+

+
        match self.ci_event {
+
            None => Err(MessageError::CiEventNotSet),
+
            Some(CiEvent::BranchCreated {
+
                from_node,
+
                repo,
+
                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: push_branch(branch),
+
                    commits: vec![*tip], // Branch created, only use tip.
+
                };
+
                Ok(Request::Trigger {
+
                    common,
+
                    push: Some(push),
+
                    patch: None,
+
                })
+
            }
+
            Some(CiEvent::BranchUpdated {
+
                from_node,
+
                repo,
+
                branch,
+
                tip,
+
                old_tip,
+
            }) => {
+
                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::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>, _>>()?;
+
                if commits.is_empty() {
+
                    commits = vec![*old_tip];
+
                }
+

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

+
                Ok(Request::Trigger {
+
                    common,
+
                    push: Some(push),
+
                    patch: None,
+
                })
+
            }
+
            Some(CiEvent::PatchCreated {
+
                from_node,
+
                repo,
+
                patch: patch_id,
+
                new_tip,
+
            }) => {
+
                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,
+
                };
+

+
                Ok(Request::Trigger {
+
                    common,
+
                    push: None,
+
                    patch: Some(PatchEvent {
+
                        action: PatchAction::Created,
+
                        patch,
+
                    }),
+
                })
+
            }
+
            Some(CiEvent::PatchUpdated {
+
                from_node,
+
                repo,
+
                patch: patch_id,
+
                new_tip,
+
            }) => {
+
                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,
+
                };
+

+
                Ok(Request::Trigger {
+
                    common,
+
                    push: None,
+
                    patch: Some(PatchEvent {
+
                        action: PatchAction::Updated,
+
                        patch,
+
                    }),
+
                })
+
            }
+
            _ => Err(MessageError::UnknownCiEvent(self.ci_event.unwrap().clone())),
+
        }
+
    }
+

    /// Create a [`Request::Trigger`] message.
    pub fn build_trigger(self) -> Result<Request, MessageError> {
        let profile = self.profile.ok_or(MessageError::NoProfile)?;
@@ -716,6 +1011,14 @@ pub enum MessageError {
    #[error("RequestBuilder has no event handler set")]
    NoEventHandler,

+
    /// We got a CI event we don't know what to do with.
+
    #[error("programming error: unknown CI event {0:?}")]
+
    UnknownCiEvent(CiEvent),
+

+
    /// CI event was not set for [`Requestbuilder`].
+
    #[error("programming error: CI event was not set for request builder")]
+
    CiEventNotSet,
+

    /// Request message lacks commits to run CI on.
    #[error("unacceptable request message: lacks Git commits to run CI on")]
    NoCommits,
@@ -927,3 +1230,262 @@ pub mod tests {
        Ok(())
    }
}
+

+
#[cfg(test)]
+
pub mod trigger_from_ci_event_tests {
+
    use crate::ci_event::CiEvent;
+
    use crate::msg::{EventType, Request, RequestBuilder};
+
    use radicle::git::RefString;
+
    use radicle::patch::{MergeTarget, Patches};
+
    use radicle::prelude::Did;
+
    use radicle::storage::ReadRepository;
+

+
    use crate::test::{MockNode, TestResult};
+

+
    #[test]
+
    fn trigger_push_from_branch_created() -> TestResult<()> {
+
        let mock_node = MockNode::new()?;
+
        let profile = mock_node.profile()?;
+

+
        let project = mock_node.node().project();
+
        let (_, repo_head) = project.repo.head()?;
+
        let cmt = radicle::test::fixtures::commit(
+
            "my test commit",
+
            &[repo_head.into()],
+
            &project.backend,
+
        );
+

+
        let ci_event = CiEvent::BranchCreated {
+
            from_node: *profile.id(),
+
            repo: project.id,
+
            branch: RefString::try_from(
+
                "refs/namespaces/$nid/refs/heads/master".replace("$nid", &profile.id().to_string()),
+
            )?,
+
            tip: cmt,
+
        };
+

+
        let req = RequestBuilder::default()
+
            .profile(&profile)
+
            .ci_event(&ci_event)
+
            .build_trigger_from_ci_event()?;
+
        let Request::Trigger {
+
            common,
+
            push,
+
            patch,
+
        } = req;
+

+
        assert!(patch.is_none());
+
        assert!(push.is_some());
+
        assert_eq!(common.event_type, EventType::Push);
+
        assert_eq!(common.repository.id, project.id);
+
        assert_eq!(common.repository.name, project.repo.project()?.name());
+

+
        let push = push.unwrap();
+
        assert_eq!(push.after, cmt);
+
        assert_eq!(push.before, cmt); // in this case of branch creation
+
        assert_eq!(
+
            push.branch,
+
            "master".replace("$nid", &profile.id().to_string())
+
        );
+
        assert_eq!(push.commits, vec![cmt]);
+
        assert_eq!(push.pusher.id, Did::from(profile.id()));
+

+
        Ok(())
+
    }
+

+
    #[test]
+
    fn trigger_push_from_branch_updated() -> TestResult<()> {
+
        let mock_node = MockNode::new()?;
+
        let profile = mock_node.profile()?;
+

+
        let project = mock_node.node().project();
+
        let (_, repo_head) = project.repo.head()?;
+
        let cmt = radicle::test::fixtures::commit(
+
            "my test commit",
+
            &[repo_head.into()],
+
            &project.backend,
+
        );
+

+
        let ci_event = CiEvent::BranchUpdated {
+
            from_node: *profile.id(),
+
            repo: project.id,
+
            branch: RefString::try_from(
+
                "refs/namespaces/$nid/refs/heads/master".replace("$nid", &profile.id().to_string()),
+
            )?,
+
            old_tip: repo_head,
+
            tip: cmt,
+
        };
+

+
        let req = RequestBuilder::default()
+
            .profile(&profile)
+
            .ci_event(&ci_event)
+
            .build_trigger_from_ci_event()?;
+
        let Request::Trigger {
+
            common,
+
            push,
+
            patch,
+
        } = req;
+

+
        assert!(patch.is_none());
+
        assert!(push.is_some());
+
        assert_eq!(common.event_type, EventType::Push);
+
        assert_eq!(common.repository.id, project.id);
+
        assert_eq!(common.repository.name, project.repo.project()?.name());
+

+
        let push = push.unwrap();
+
        assert_eq!(push.after, cmt);
+
        assert_eq!(push.before, cmt); // in this case of branch creation
+
        assert_eq!(
+
            push.branch,
+
            "master".replace("$nid", &profile.id().to_string())
+
        );
+
        assert_eq!(push.commits, vec![cmt]);
+
        assert_eq!(push.pusher.id, Did::from(profile.id()));
+

+
        Ok(())
+
    }
+

+
    #[test]
+
    fn trigger_patch_from_patch_created() -> TestResult<()> {
+
        let mock_node = MockNode::new()?;
+
        let profile = mock_node.profile()?;
+

+
        let project = mock_node.node().project();
+
        let (_, repo_head) = project.repo.head()?;
+
        let cmt = radicle::test::fixtures::commit(
+
            "my test commit",
+
            &[repo_head.into()],
+
            &project.backend,
+
        );
+

+
        let node = mock_node.node();
+

+
        let mut patches = Patches::open(&project.repo)?;
+
        let mut cache = radicle::cob::cache::NoCache;
+
        let patch_cob = patches.create(
+
            "my patch title",
+
            "my patch description",
+
            MergeTarget::Delegates,
+
            repo_head,
+
            cmt,
+
            &[],
+
            &mut cache,
+
            &node.signer,
+
        )?;
+

+
        let ci_event = CiEvent::PatchCreated {
+
            from_node: *profile.id(),
+
            repo: project.id,
+
            patch: *patch_cob.id(),
+
            new_tip: cmt,
+
        };
+

+
        let req = RequestBuilder::default()
+
            .profile(&profile)
+
            .ci_event(&ci_event)
+
            .build_trigger_from_ci_event()?;
+
        let Request::Trigger {
+
            common,
+
            push,
+
            patch,
+
        } = req;
+

+
        assert!(patch.is_some());
+
        assert!(push.is_none());
+
        assert_eq!(common.event_type, EventType::Patch);
+
        assert_eq!(common.repository.id, project.id);
+
        assert_eq!(common.repository.name, project.repo.project()?.name());
+

+
        let patch = patch.unwrap();
+
        assert_eq!(patch.action.as_str(), "created");
+
        assert_eq!(patch.patch.id.to_string(), patch_cob.id.to_string());
+
        assert_eq!(patch.patch.title, patch_cob.title());
+
        assert_eq!(patch.patch.state.status, patch_cob.state().to_string());
+
        assert_eq!(patch.patch.target, repo_head);
+
        assert_eq!(patch.patch.revisions.len(), 1);
+
        let rev = patch.patch.revisions.first().unwrap();
+
        assert_eq!(rev.id.to_string(), patch_cob.id.to_string());
+
        assert_eq!(rev.base, repo_head);
+
        assert_eq!(rev.oid, cmt);
+
        assert_eq!(rev.author.id, Did::from(profile.id()));
+
        assert_eq!(rev.description, patch_cob.description());
+
        assert_eq!(rev.timestamp, patch_cob.timestamp().as_secs());
+
        assert_eq!(patch.patch.after, cmt);
+
        assert_eq!(patch.patch.before, repo_head);
+
        assert_eq!(patch.patch.commits, vec![cmt]);
+

+
        Ok(())
+
    }
+

+
    #[test]
+
    fn trigger_patch_from_patch_updated() -> TestResult<()> {
+
        let mock_node = MockNode::new()?;
+
        let profile = mock_node.profile()?;
+

+
        let project = mock_node.node().project();
+
        let (_, repo_head) = project.repo.head()?;
+
        let cmt = radicle::test::fixtures::commit(
+
            "my test commit",
+
            &[repo_head.into()],
+
            &project.backend,
+
        );
+

+
        let node = mock_node.node();
+

+
        let mut patches = Patches::open(&project.repo)?;
+
        let mut cache = radicle::cob::cache::NoCache;
+
        let patch_cob = patches.create(
+
            "my patch title",
+
            "my patch description",
+
            MergeTarget::Delegates,
+
            repo_head,
+
            cmt,
+
            &[],
+
            &mut cache,
+
            &node.signer,
+
        )?;
+

+
        let ci_event = CiEvent::PatchUpdated {
+
            from_node: *profile.id(),
+
            repo: project.id,
+
            patch: *patch_cob.id(),
+
            new_tip: cmt,
+
        };
+

+
        let req = RequestBuilder::default()
+
            .profile(&profile)
+
            .ci_event(&ci_event)
+
            .build_trigger_from_ci_event()?;
+
        let Request::Trigger {
+
            common,
+
            push,
+
            patch,
+
        } = req;
+

+
        assert!(patch.is_some());
+
        assert!(push.is_none());
+
        assert_eq!(common.event_type, EventType::Patch);
+
        assert_eq!(common.repository.id, project.id);
+
        assert_eq!(common.repository.name, project.repo.project()?.name());
+

+
        let patch = patch.unwrap();
+
        assert_eq!(patch.action.as_str(), "updated");
+
        assert_eq!(patch.patch.id.to_string(), patch_cob.id.to_string());
+
        assert_eq!(patch.patch.title, patch_cob.title());
+
        assert_eq!(patch.patch.state.status, patch_cob.state().to_string());
+
        assert_eq!(patch.patch.target, repo_head);
+
        assert_eq!(patch.patch.revisions.len(), 1);
+
        let rev = patch.patch.revisions.first().unwrap();
+
        assert_eq!(rev.id.to_string(), patch_cob.id.to_string());
+
        assert_eq!(rev.base, repo_head);
+
        assert_eq!(rev.oid, cmt);
+
        assert_eq!(rev.author.id, Did::from(profile.id()));
+
        assert_eq!(rev.description, patch_cob.description());
+
        assert_eq!(rev.timestamp, patch_cob.timestamp().as_secs());
+
        assert_eq!(patch.patch.after, cmt);
+
        assert_eq!(patch.patch.before, repo_head);
+
        assert_eq!(patch.patch.commits, vec![cmt]);
+

+
        Ok(())
+
    }
+
}