use gotcha_gitea::models; #[derive(Debug, Eq, PartialEq)] pub enum Target { Commit { owner: String, repository: String, sha: String, }, Issue { owner: String, repository: String, number: i64, }, Pull { owner: String, repository: String, number: i64, }, } pub fn target(activity: &models::Activity) -> Option { use models::activity::OpType; let (owner, repository) = activity .repo .as_ref()? .full_name .as_deref()? .split_once('/')?; let pair = || (owner.to_string(), repository.to_string()); match activity.op_type? { OpType::CommitRepo | OpType::MirrorSyncPush => { let (owner, repository) = pair(); Some(Target::Commit { owner, repository, sha: commit_sha(activity.content.as_deref()?)?, }) } OpType::CreateIssue | OpType::CloseIssue | OpType::ReopenIssue | OpType::CommentIssue => { let (owner, repository) = pair(); Some(Target::Issue { owner, repository, number: issue_number(activity)?, }) } OpType::CreatePullRequest | OpType::MergePullRequest | OpType::AutoMergePullRequest | OpType::ClosePullRequest | OpType::ReopenPullRequest | OpType::CommentPull | OpType::ApprovePullRequest | OpType::RejectPullRequest | OpType::PullReviewDismissed | OpType::PullRequestReadyForReview => { let (owner, repository) = pair(); Some(Target::Pull { owner, repository, number: issue_number(activity)?, }) } _ => None, } } fn issue_number(activity: &models::Activity) -> Option { activity .comment .as_ref() .and_then(|comment| { comment .pull_request_url .as_deref() .or(comment.issue_url.as_deref()) }) .and_then(url_number) .or_else(|| { activity .content .as_deref()? .split(|character: char| !character.is_ascii_digit()) .find(|part| !part.is_empty())? .parse() .ok() }) } fn url_number(url: &str) -> Option { url.trim_end_matches('/').rsplit('/').next()?.parse().ok() } fn commit_sha(content: &str) -> Option { let payload: serde_json::Value = serde_json::from_str(content).ok()?; payload.get("HeadCommit").and_then(find_sha).or_else(|| { payload .get("Commits")? .as_array()? .last() .and_then(find_sha) }) } fn find_sha(value: &serde_json::Value) -> Option { value.as_object()?.iter().find_map(|(key, value)| { (key.to_ascii_lowercase().contains("sha")) .then(|| value.as_str()) .flatten() .filter(|sha| sha.len() >= 7) .map(str::to_string) }) } #[cfg(test)] mod tests { use super::*; use models::activity::OpType; fn activity(op_type: OpType, content: &str) -> models::Activity { models::Activity { op_type: Some(op_type), content: Some(content.into()), repo: Some(Box::new(models::Repository { full_name: Some("octo/demo".into()), ..Default::default() })), ..Default::default() } } #[test] fn decodes_linkable_activity_targets() { assert_eq!( target(&activity(OpType::CreatePullRequest, "42|feature")), Some(Target::Pull { owner: "octo".into(), repository: "demo".into(), number: 42, }) ); assert_eq!( target(&activity( OpType::CommitRepo, r#"{"HeadCommit":{"Sha1":"0123456789abcdef"}}"#, )), Some(Target::Commit { owner: "octo".into(), repository: "demo".into(), sha: "0123456789abcdef".into(), }) ); } }