use http::HeaderMap;
use serde_json::Value;
use vgi_forge::{ForgeError, Resource, Result};
use crate::forge::GitHubForge;
use crate::secret::Secret;
use crate::webhook;
const PERMS_PUSH: &[(&str, &str)] = &[("contents", "write"), ("metadata", "read")];
pub const ZERO_SHA: &str = "0000000000000000000000000000000000000000";
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PushEvent {
pub repo: Resource,
pub repo_id: u64,
pub git_ref: String,
pub before: String,
pub after: String,
pub created: bool,
pub deleted: bool,
pub forced: bool,
pub sender_login: String,
pub sender_id: u64,
pub delivery_id: Option<String>,
pub pushed_at: Option<i64>,
}
impl PushEvent {
pub fn branch(&self) -> Option<&str> {
self.git_ref.strip_prefix("refs/heads/")
}
}
fn object_id(v: &Value, key: &str) -> Result<String> {
let s = v
.get(key)
.and_then(Value::as_str)
.ok_or_else(|| ForgeError::Webhook(format!("push is missing `{key}`")))?;
crate::checks::check_sha(s)
.map_err(|_| ForgeError::Webhook(format!("push `{key}` is not an object id")))?;
Ok(s.to_string())
}
fn pushed_at(p: &Value) -> Option<i64> {
p.pointer("/repository/pushed_at").and_then(Value::as_i64)
}
impl GitHubForge {
pub fn parse_push(&self, headers: &HeaderMap, body: &[u8]) -> Result<Option<PushEvent>> {
webhook::verify_signature(self.webhook_secret(), headers, body)?;
let event = headers
.get("x-github-event")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| ForgeError::Webhook("missing X-GitHub-Event".into()))?;
if event != "push" {
return Ok(None);
}
let delivery_id = headers
.get("x-github-delivery")
.and_then(|v| v.to_str().ok())
.map(str::to_string);
let p: Value = serde_json::from_slice(body)
.map_err(|e| ForgeError::Webhook(format!("body is not JSON: {e}")))?;
let full_name = p
.pointer("/repository/full_name")
.and_then(Value::as_str)
.ok_or_else(|| ForgeError::Webhook("push is missing `repository.full_name`".into()))?;
let repo = Resource::parse_owner_repo(&format!("{}/{full_name}", self.config().host))?;
let repo_id = p
.pointer("/repository/id")
.and_then(Value::as_u64)
.ok_or_else(|| ForgeError::Webhook("push is missing `repository.id`".into()))?;
let git_ref = p
.get("ref")
.and_then(Value::as_str)
.filter(|r| !r.is_empty())
.ok_or_else(|| ForgeError::Webhook("push is missing `ref`".into()))?
.to_string();
let flag = |k: &str| p.get(k).and_then(Value::as_bool).unwrap_or(false);
let sender_login = p
.pointer("/sender/login")
.and_then(Value::as_str)
.filter(|l| !l.is_empty())
.ok_or_else(|| ForgeError::Webhook("push is missing `sender.login`".into()))?
.to_string();
let sender_id = p
.pointer("/sender/id")
.and_then(Value::as_u64)
.ok_or_else(|| ForgeError::Webhook("push is missing `sender.id`".into()))?;
Ok(Some(PushEvent {
repo,
repo_id,
git_ref,
before: object_id(&p, "before")?,
after: object_id(&p, "after")?,
created: flag("created"),
deleted: flag("deleted"),
forced: flag("forced"),
sender_login,
sender_id,
delivery_id,
pushed_at: pushed_at(&p),
}))
}
pub async fn contents_write_token(&self, repo: &Resource) -> Result<Secret> {
Ok(self.repo_token_for(repo, PERMS_PUSH).await?.0)
}
}