logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
725
tooling/compliance/src/github.rs
Normal file
725
tooling/compliance/src/github.rs
Normal file
@@ -0,0 +1,725 @@
|
||||
use std::{borrow::Cow, collections::HashMap, fmt};
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_more::Deref;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::git::CommitSha;
|
||||
|
||||
pub const PR_REVIEW_LABEL: &str = "PR state:needs review";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GithubUser {
|
||||
pub login: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PullRequestData {
|
||||
pub number: u64,
|
||||
pub user: Option<GithubUser>,
|
||||
pub merged_by: Option<GithubUser>,
|
||||
pub labels: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ReviewState {
|
||||
Approved,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AuthorAssociation {
|
||||
Owner,
|
||||
Member,
|
||||
Collaborator,
|
||||
Contributor,
|
||||
FirstTimeContributor,
|
||||
FirstTimer,
|
||||
Mannequin,
|
||||
None,
|
||||
}
|
||||
|
||||
impl AuthorAssociation {
|
||||
pub fn has_write_access(&self) -> bool {
|
||||
matches!(self, Self::Owner | Self::Member | Self::Collaborator)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Approvable {
|
||||
fn author_login(&self) -> Option<&str>;
|
||||
fn review_state(&self) -> Option<ReviewState>;
|
||||
fn body(&self) -> Option<&str>;
|
||||
fn author_association(&self) -> Option<AuthorAssociation>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PullRequestReview {
|
||||
pub user: Option<GithubUser>,
|
||||
pub state: Option<ReviewState>,
|
||||
pub body: Option<String>,
|
||||
pub author_association: Option<AuthorAssociation>,
|
||||
}
|
||||
|
||||
impl PullRequestReview {
|
||||
pub fn with_body(self, body: impl ToString) -> Self {
|
||||
Self {
|
||||
body: Some(body.to_string()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Approvable for PullRequestReview {
|
||||
fn author_login(&self) -> Option<&str> {
|
||||
self.user.as_ref().map(|user| user.login.as_str())
|
||||
}
|
||||
|
||||
fn review_state(&self) -> Option<ReviewState> {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn body(&self) -> Option<&str> {
|
||||
self.body.as_deref()
|
||||
}
|
||||
|
||||
fn author_association(&self) -> Option<AuthorAssociation> {
|
||||
self.author_association
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PullRequestComment {
|
||||
pub user: GithubUser,
|
||||
pub body: Option<String>,
|
||||
pub author_association: Option<AuthorAssociation>,
|
||||
}
|
||||
|
||||
impl Approvable for PullRequestComment {
|
||||
fn author_login(&self) -> Option<&str> {
|
||||
Some(&self.user.login)
|
||||
}
|
||||
|
||||
fn review_state(&self) -> Option<ReviewState> {
|
||||
None
|
||||
}
|
||||
|
||||
fn body(&self) -> Option<&str> {
|
||||
self.body.as_deref()
|
||||
}
|
||||
|
||||
fn author_association(&self) -> Option<AuthorAssociation> {
|
||||
self.author_association
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Deref, PartialEq, Eq)]
|
||||
pub struct GithubLogin {
|
||||
login: String,
|
||||
}
|
||||
|
||||
impl GithubLogin {
|
||||
pub fn new(login: String) -> Self {
|
||||
Self { login }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for GithubLogin {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "@{}", self.login)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CommitAuthor {
|
||||
name: String,
|
||||
email: String,
|
||||
user: Option<GithubLogin>,
|
||||
}
|
||||
|
||||
impl CommitAuthor {
|
||||
pub(crate) fn user(&self) -> Option<&GithubLogin> {
|
||||
self.user.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for CommitAuthor {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.user.as_ref().zip(other.user.as_ref()).map_or_else(
|
||||
|| self.email == other.email || self.name == other.name,
|
||||
|(l, r)| l == r,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CommitAuthor {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.user.as_ref() {
|
||||
Some(user) => write!(formatter, "{} ({user})", self.name),
|
||||
None => write!(formatter, "{} ({})", self.name, self.email),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CommitSignature {
|
||||
#[serde(rename = "isValid")]
|
||||
is_valid: bool,
|
||||
signer: Option<GithubLogin>,
|
||||
}
|
||||
|
||||
impl CommitSignature {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.is_valid
|
||||
}
|
||||
|
||||
pub fn signer(&self) -> Option<&GithubLogin> {
|
||||
self.signer.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CommitFileChange {
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CommitMetadata {
|
||||
#[serde(rename = "author")]
|
||||
primary_author: CommitAuthor,
|
||||
#[serde(rename = "authors", deserialize_with = "graph_ql::deserialize_nodes")]
|
||||
co_authors: Vec<CommitAuthor>,
|
||||
#[serde(default)]
|
||||
signature: Option<CommitSignature>,
|
||||
#[serde(default)]
|
||||
additions: u64,
|
||||
#[serde(default)]
|
||||
deletions: u64,
|
||||
}
|
||||
|
||||
impl CommitMetadata {
|
||||
pub fn co_authors(&self) -> Option<impl Iterator<Item = &CommitAuthor>> {
|
||||
let mut co_authors = self
|
||||
.co_authors
|
||||
.iter()
|
||||
.filter(|co_author| *co_author != &self.primary_author)
|
||||
.peekable();
|
||||
|
||||
co_authors.peek().is_some().then_some(co_authors)
|
||||
}
|
||||
|
||||
pub fn primary_author(&self) -> &CommitAuthor {
|
||||
&self.primary_author
|
||||
}
|
||||
|
||||
pub fn signature(&self) -> Option<&CommitSignature> {
|
||||
self.signature.as_ref()
|
||||
}
|
||||
|
||||
pub fn additions(&self) -> u64 {
|
||||
self.additions
|
||||
}
|
||||
|
||||
pub fn deletions(&self) -> u64 {
|
||||
self.deletions
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deref)]
|
||||
pub struct CommitMetadataBySha(HashMap<CommitSha, CommitMetadata>);
|
||||
|
||||
impl CommitMetadataBySha {
|
||||
const SHA_PREFIX: &'static str = "commit";
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for CommitMetadataBySha {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw = HashMap::<String, CommitMetadata>::deserialize(deserializer)?;
|
||||
let map = raw
|
||||
.into_iter()
|
||||
.map(|(key, value)| {
|
||||
let sha = key
|
||||
.strip_prefix(CommitMetadataBySha::SHA_PREFIX)
|
||||
.unwrap_or(&key);
|
||||
(CommitSha::new(sha.to_owned()), value)
|
||||
})
|
||||
.collect();
|
||||
Ok(Self(map))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Repository<'a> {
|
||||
owner: Cow<'a, str>,
|
||||
name: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl<'a> Repository<'a> {
|
||||
pub const ZED: Repository<'static> = Repository::new_static("zed-industries", "zed");
|
||||
|
||||
pub fn new(owner: &'a str, name: &'a str) -> Self {
|
||||
Self {
|
||||
owner: Cow::Borrowed(owner),
|
||||
name: Cow::Borrowed(name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> &str {
|
||||
&self.owner
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Repository<'static> {
|
||||
pub const fn new_static(owner: &'static str, name: &'static str) -> Self {
|
||||
Self {
|
||||
owner: Cow::Borrowed(owner),
|
||||
name: Cow::Borrowed(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
pub trait GithubApiClient {
|
||||
async fn get_pull_request(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
pr_number: u64,
|
||||
) -> Result<PullRequestData>;
|
||||
async fn get_pull_request_reviews(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
pr_number: u64,
|
||||
) -> Result<Vec<PullRequestReview>>;
|
||||
async fn get_pull_request_comments(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
pr_number: u64,
|
||||
) -> Result<Vec<PullRequestComment>>;
|
||||
async fn get_commit_metadata(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
commit_shas: &[&CommitSha],
|
||||
) -> Result<CommitMetadataBySha>;
|
||||
async fn get_commit_files(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
sha: &CommitSha,
|
||||
) -> Result<Vec<CommitFileChange>>;
|
||||
async fn check_repo_write_permission(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
login: &GithubLogin,
|
||||
) -> Result<bool>;
|
||||
async fn add_label_to_issue(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
label: &str,
|
||||
issue_number: u64,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
pub mod graph_ql {
|
||||
use anyhow::{Context as _, Result};
|
||||
use itertools::Itertools as _;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::git::CommitSha;
|
||||
|
||||
use super::CommitMetadataBySha;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GraphQLResponse<T> {
|
||||
pub data: Option<T>,
|
||||
pub errors: Option<Vec<GraphQLError>>,
|
||||
}
|
||||
|
||||
impl<T> GraphQLResponse<T> {
|
||||
pub fn into_data(self) -> Result<T> {
|
||||
if let Some(errors) = &self.errors {
|
||||
if !errors.is_empty() {
|
||||
let messages: String = errors.iter().map(|e| e.message.as_str()).join("; ");
|
||||
anyhow::bail!("GraphQL error: {messages}");
|
||||
}
|
||||
}
|
||||
|
||||
self.data.context("GraphQL response contained no data")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GraphQLError {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CommitMetadataResponse {
|
||||
pub repository: CommitMetadataBySha,
|
||||
}
|
||||
|
||||
pub fn deserialize_nodes<'de, T, D>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Nodes<T> {
|
||||
nodes: Vec<T>,
|
||||
}
|
||||
Nodes::<T>::deserialize(deserializer).map(|wrapper| wrapper.nodes)
|
||||
}
|
||||
|
||||
pub fn build_commit_metadata_query<'a>(
|
||||
org: &str,
|
||||
repo: &str,
|
||||
shas: impl IntoIterator<Item = &'a CommitSha>,
|
||||
) -> String {
|
||||
const FRAGMENT: &str = r#"
|
||||
... on Commit {
|
||||
author {
|
||||
name
|
||||
email
|
||||
user { login }
|
||||
}
|
||||
authors(first: 10) {
|
||||
nodes {
|
||||
name
|
||||
email
|
||||
user { login }
|
||||
}
|
||||
}
|
||||
signature {
|
||||
isValid
|
||||
signer { login }
|
||||
}
|
||||
additions
|
||||
deletions
|
||||
}
|
||||
"#;
|
||||
|
||||
let objects = shas
|
||||
.into_iter()
|
||||
.map(|commit_sha| {
|
||||
format!(
|
||||
"{sha_prefix}{sha}: object(oid: \"{sha}\") {{ {FRAGMENT} }}",
|
||||
sha_prefix = CommitMetadataBySha::SHA_PREFIX,
|
||||
sha = **commit_sha,
|
||||
)
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
format!("{{ repository(owner: \"{org}\", name: \"{repo}\") {{ {objects} }} }}")
|
||||
.replace("\n", "")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "octo-client")]
|
||||
mod octo_client {
|
||||
use anyhow::{Context, Result};
|
||||
use futures::TryStreamExt as _;
|
||||
use jsonwebtoken::EncodingKey;
|
||||
use octocrab::{
|
||||
Octocrab, Page,
|
||||
models::{
|
||||
AuthorAssociation as OctocrabAuthorAssociation,
|
||||
pulls::ReviewState as OctocrabReviewState,
|
||||
},
|
||||
service::middleware::cache::mem::InMemoryCache,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::pin;
|
||||
|
||||
use crate::{
|
||||
git::CommitSha,
|
||||
github::{Repository, graph_ql},
|
||||
};
|
||||
|
||||
use super::{
|
||||
AuthorAssociation, CommitFileChange, CommitMetadataBySha, GithubApiClient, GithubLogin,
|
||||
GithubUser, PullRequestComment, PullRequestData, PullRequestReview, ReviewState,
|
||||
};
|
||||
|
||||
fn convert_author_association(association: OctocrabAuthorAssociation) -> AuthorAssociation {
|
||||
match association {
|
||||
OctocrabAuthorAssociation::Owner => AuthorAssociation::Owner,
|
||||
OctocrabAuthorAssociation::Member => AuthorAssociation::Member,
|
||||
OctocrabAuthorAssociation::Collaborator => AuthorAssociation::Collaborator,
|
||||
OctocrabAuthorAssociation::Contributor => AuthorAssociation::Contributor,
|
||||
OctocrabAuthorAssociation::FirstTimeContributor => {
|
||||
AuthorAssociation::FirstTimeContributor
|
||||
}
|
||||
OctocrabAuthorAssociation::FirstTimer => AuthorAssociation::FirstTimer,
|
||||
OctocrabAuthorAssociation::Mannequin => AuthorAssociation::Mannequin,
|
||||
OctocrabAuthorAssociation::None => AuthorAssociation::None,
|
||||
_ => AuthorAssociation::None,
|
||||
}
|
||||
}
|
||||
|
||||
const PAGE_SIZE: u8 = 100;
|
||||
|
||||
pub struct OctocrabClient {
|
||||
client: Octocrab,
|
||||
}
|
||||
|
||||
impl OctocrabClient {
|
||||
pub async fn new(app_id: u64, app_private_key: &str, org: &str) -> Result<Self> {
|
||||
let octocrab = Octocrab::builder()
|
||||
.cache(InMemoryCache::new())
|
||||
.app(
|
||||
app_id.into(),
|
||||
EncodingKey::from_rsa_pem(app_private_key.as_bytes())?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let installations = octocrab
|
||||
.apps()
|
||||
.installations()
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to fetch installations")?
|
||||
.take_items();
|
||||
|
||||
let installation_id = installations
|
||||
.into_iter()
|
||||
.find(|installation| installation.account.login == org)
|
||||
.context("Could not find Zed repository in installations")?
|
||||
.id;
|
||||
|
||||
let client = octocrab.installation(installation_id)?;
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
async fn graphql<R: DeserializeOwned>(&self, query: &serde_json::Value) -> Result<R> {
|
||||
let response: serde_json::Value = self.client.graphql(query).await?;
|
||||
let parsed: graph_ql::GraphQLResponse<R> = serde_json::from_value(response)
|
||||
.context("Failed to parse GraphQL response envelope")?;
|
||||
parsed.into_data()
|
||||
}
|
||||
|
||||
async fn get_all<T: DeserializeOwned + 'static>(
|
||||
&self,
|
||||
page: Page<T>,
|
||||
) -> octocrab::Result<Vec<T>> {
|
||||
self.get_filtered(page, |_| true).await
|
||||
}
|
||||
|
||||
async fn get_filtered<T: DeserializeOwned + 'static>(
|
||||
&self,
|
||||
page: Page<T>,
|
||||
predicate: impl Fn(&T) -> bool,
|
||||
) -> octocrab::Result<Vec<T>> {
|
||||
let stream = page.into_stream(&self.client);
|
||||
pin!(stream);
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
while let Some(item) = stream.try_next().await?
|
||||
&& predicate(&item)
|
||||
{
|
||||
results.push(item);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl GithubApiClient for OctocrabClient {
|
||||
async fn get_pull_request(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
pr_number: u64,
|
||||
) -> Result<PullRequestData> {
|
||||
let pr = self
|
||||
.client
|
||||
.pulls(repo.owner.as_ref(), repo.name.as_ref())
|
||||
.get(pr_number)
|
||||
.await?;
|
||||
Ok(PullRequestData {
|
||||
number: pr.number,
|
||||
user: pr.user.map(|user| GithubUser { login: user.login }),
|
||||
merged_by: pr.merged_by.map(|user| GithubUser { login: user.login }),
|
||||
labels: pr
|
||||
.labels
|
||||
.map(|labels| labels.into_iter().map(|label| label.name).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_pull_request_reviews(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
pr_number: u64,
|
||||
) -> Result<Vec<PullRequestReview>> {
|
||||
let page = self
|
||||
.client
|
||||
.pulls(repo.owner.as_ref(), repo.name.as_ref())
|
||||
.list_reviews(pr_number)
|
||||
.per_page(PAGE_SIZE)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let reviews = self.get_all(page).await?;
|
||||
|
||||
Ok(reviews
|
||||
.into_iter()
|
||||
.map(|review| PullRequestReview {
|
||||
user: review.user.map(|user| GithubUser { login: user.login }),
|
||||
state: review.state.map(|state| match state {
|
||||
OctocrabReviewState::Approved => ReviewState::Approved,
|
||||
_ => ReviewState::Other,
|
||||
}),
|
||||
body: review.body,
|
||||
author_association: review.author_association.map(convert_author_association),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_pull_request_comments(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
pr_number: u64,
|
||||
) -> Result<Vec<PullRequestComment>> {
|
||||
let page = self
|
||||
.client
|
||||
.issues(repo.owner.as_ref(), repo.name.as_ref())
|
||||
.list_comments(pr_number)
|
||||
.per_page(PAGE_SIZE)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let comments = self.get_all(page).await?;
|
||||
|
||||
Ok(comments
|
||||
.into_iter()
|
||||
.map(|comment| PullRequestComment {
|
||||
user: GithubUser {
|
||||
login: comment.user.login,
|
||||
},
|
||||
body: comment.body,
|
||||
author_association: comment.author_association.map(convert_author_association),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_commit_metadata(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
commit_shas: &[&CommitSha],
|
||||
) -> Result<CommitMetadataBySha> {
|
||||
let query = graph_ql::build_commit_metadata_query(
|
||||
repo.owner.as_ref(),
|
||||
repo.name.as_ref(),
|
||||
commit_shas.iter().copied(),
|
||||
);
|
||||
let query = serde_json::json!({ "query": query });
|
||||
self.graphql::<graph_ql::CommitMetadataResponse>(&query)
|
||||
.await
|
||||
.map(|response| response.repository)
|
||||
}
|
||||
|
||||
async fn get_commit_files(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
sha: &CommitSha,
|
||||
) -> Result<Vec<CommitFileChange>> {
|
||||
let response = self
|
||||
.client
|
||||
.commits(repo.owner.as_ref(), repo.name.as_ref())
|
||||
.get(sha.as_str())
|
||||
.await?;
|
||||
|
||||
Ok(response
|
||||
.files
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|file| CommitFileChange {
|
||||
filename: file.filename,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn check_repo_write_permission(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
login: &GithubLogin,
|
||||
) -> Result<bool> {
|
||||
// Check org membership first - we save ourselves a few request that way
|
||||
let page = self
|
||||
.client
|
||||
.orgs(repo.owner.as_ref())
|
||||
.list_members()
|
||||
.per_page(PAGE_SIZE)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let members = self.get_all(page).await?;
|
||||
|
||||
if members
|
||||
.into_iter()
|
||||
.any(|member| member.login == login.as_str())
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// TODO: octocrab fails to deserialize the permission response and
|
||||
// does not adhere to the scheme laid out at
|
||||
// https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2026-03-10#get-repository-permissions-for-a-user
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum RepoPermission {
|
||||
Admin,
|
||||
Write,
|
||||
Read,
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RepositoryPermissions {
|
||||
permission: RepoPermission,
|
||||
}
|
||||
|
||||
self.client
|
||||
.get::<RepositoryPermissions, _, _>(
|
||||
format!(
|
||||
"/repos/{owner}/{repo}/collaborators/{user}/permission",
|
||||
owner = repo.owner.as_ref(),
|
||||
repo = repo.name.as_ref(),
|
||||
user = login.as_str()
|
||||
),
|
||||
None::<&()>,
|
||||
)
|
||||
.await
|
||||
.map(|response| {
|
||||
matches!(
|
||||
response.permission,
|
||||
RepoPermission::Write | RepoPermission::Admin
|
||||
)
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_label_to_issue(
|
||||
&self,
|
||||
repo: &Repository<'_>,
|
||||
label: &str,
|
||||
issue_number: u64,
|
||||
) -> Result<()> {
|
||||
self.client
|
||||
.issues(repo.owner.as_ref(), repo.name.as_ref())
|
||||
.add_labels(issue_number, &[label.to_owned()])
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "octo-client")]
|
||||
pub use octo_client::OctocrabClient;
|
||||
Reference in New Issue
Block a user