Files
Gotcha/crates/app/src/activity.rs
2026-07-30 16:12:22 +02:00

157 lines
4.2 KiB
Rust

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<Target> {
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<i64> {
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<i64> {
url.trim_end_matches('/').rsplit('/').next()?.parse().ok()
}
fn commit_sha(content: &str) -> Option<String> {
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<String> {
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(),
})
);
}
}