use crate::backend::{
error::{Result, invalid},
models::{Anchor, ContextKey, ForgeObservation, ForgeRepositoryUrl},
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RemoteAuthor {
pub name: String,
pub email: String,
pub account_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RemoteComment {
pub pending: bool,
pub id: String,
pub kind: String,
pub review_id: Option<String>,
pub body: String,
pub updated_at: String,
pub author: RemoteAuthor,
pub reply_to: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RemoteThread {
pub id: String,
pub anchor: Anchor,
#[serde(default)]
pub observation: Option<ForgeObservation>,
pub resolved: bool,
#[serde(default)]
pub orphan_reason: Option<crate::backend::models::OrphanReason>,
pub comments: Vec<RemoteComment>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ForgeSnapshot {
pub complete: bool,
#[serde(default)]
pub state: Option<crate::backend::database::types::ForgeState>,
pub head: String,
pub base: Option<String>,
pub threads: Vec<RemoteThread>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PostReceipt {
pub comment_id: String,
pub kind: String,
pub thread_id: Option<String>,
pub review_id: Option<String>,
pub posting_account_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PullRequest {
pub number: i64,
pub state: crate::backend::database::types::ForgeState,
pub title: String,
pub head: String,
pub base: String,
pub repository_url: String,
#[serde(default)]
pub author_account_id: Option<String>,
}
#[derive(Clone, Debug)]
pub struct Repository {
pub owner: String,
pub name: String,
pub repository_url: String,
}
impl Repository {
pub fn route(&self) -> String {
format!("/repos/{}/{}", self.owner, self.name)
}
pub fn from_repository_url(repository_url: &str) -> Result<Self> {
let repository_url = ForgeRepositoryUrl::parse(repository_url)?;
let url = url::Url::parse(repository_url.as_str())
.map_err(|_| invalid("Invalid repository URL"))?;
if url.scheme() != "https" || url.host_str() != Some("github.com") || url.port().is_some() {
return Err(invalid(
"Only https://github.com repositories are supported",
));
}
let parts: Vec<_> = url.path().trim_matches('/').split('/').collect();
if parts.len() != 2
|| parts.iter().any(|v| {
v.is_empty()
|| !v
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
})
{
return Err(invalid("GitHub repository URL requires owner/repository"));
}
let name = parts[1].trim_end_matches(".git");
if name.is_empty() {
return Err(invalid("GitHub repository name is empty"));
}
Ok(Self {
owner: parts[0].into(),
name: name.into(),
repository_url: format!("https://github.com/{}/{}", parts[0], name),
})
}
pub fn from_context_key(key: &ContextKey) -> Result<(Self, i64)> {
match key {
ContextKey::ForgeBacked {
repository_url,
number,
} => Ok((Self::from_repository_url(repository_url.as_str())?, *number)),
_ => Err(invalid("Remote operations require a forge-backed context")),
}
}
}