use std::collections::BTreeSet; use crate::{ Client, Error, Result, domain::{DEFAULT_PAGE_SIZE, Page, PullDetails, RepositoryId}, models, }; use gitea_openapi::apis; #[derive(Clone, Debug, Eq, PartialEq)] pub enum PullFileSource { Branch(String), Commit(String), Request, } pub fn pull_state(pull: &models::PullRequest) -> &str { if pull.merged.unwrap_or(false) { "merged" } else if pull.draft.unwrap_or(false) { "draft" } else { pull.state.as_deref().unwrap_or("unknown") } } pub fn pull_file_source(pull: &models::PullRequest) -> PullFileSource { if pull.state.as_deref() == Some("open") { PullFileSource::Branch( pull.head .as_ref() .and_then(|head| head.r#ref.clone().or(head.label.clone())) .unwrap_or_else(|| "head branch".into()), ) } else if let Some(sha) = pull.merge_commit_sha.as_deref() { PullFileSource::Commit(sha.chars().take(8).collect()) } else { PullFileSource::Request } } impl Client { pub async fn repository_pulls( &self, repository: &RepositoryId, state: &str, page: i32, limit: i32, ) -> Result> { validate_page(page, limit)?; apis::repository_api::repo_list_pull_requests( &self.configuration(), &repository.owner, &repository.repository, None, Some(state), None, None, None, None, Some(page), Some(limit), ) .await .map(|items| Page::from_items(items, limit)) .map_err(Error::generated) } pub async fn search_pulls( &self, state: &str, milestone: Option<&str>, page: i32, limit: i32, ) -> Result> { validate_page(page, limit)?; let owner = self .current_user() .await? .login .ok_or_else(|| Error::Generated("The server account has no username.".into()))?; apis::issue_api::issue_search_issues( &self.configuration(), Some(state), None, milestone, None, None, Some("pulls"), None, None, None, None, None, None, None, Some(&owner), None, Some(page), Some(limit), ) .await .map(|items| Page::from_items(items, limit)) .map_err(Error::generated) } pub async fn pull_milestones(&self) -> Result> { let (open, closed) = tokio::try_join!( self.all_searched_pulls("open"), self.all_searched_pulls("closed"), )?; Ok(open .into_iter() .chain(closed) .filter_map(|pull| pull.milestone?.title) .collect::>() .into_iter() .collect()) } async fn all_searched_pulls(&self, state: &str) -> Result> { let mut pulls = Vec::new(); for page in 1.. { let batch = self .search_pulls(state, None, page, DEFAULT_PAGE_SIZE) .await?; pulls.extend(batch.items); if !batch.has_more { break; } } Ok(pulls) } pub async fn pull( &self, repository: &RepositoryId, number: i64, ) -> Result { positive(number, "pull request number")?; apis::repository_api::repo_get_pull_request( &self.configuration(), &repository.owner, &repository.repository, number, ) .await .map_err(Error::generated) } pub async fn pull_details( &self, repository: &RepositoryId, number: i64, page: i32, ) -> Result { let pull = self.pull(repository, number); let comments = async { if page == 1 { self.issue_comments(repository, number).await } else { Ok(Vec::new()) } }; let files = self.pull_files_page(repository, number, page, DEFAULT_PAGE_SIZE); let (pull, comments, files) = tokio::try_join!(pull, comments, files)?; Ok(PullDetails { pull, comments, has_more: files.has_more, files: files.items, }) } pub async fn create_pull( &self, repository: &RepositoryId, option: models::CreatePullRequestOption, ) -> Result { if [ option.title.as_deref(), option.head.as_deref(), option.base.as_deref(), ] .into_iter() .any(|value| value.unwrap_or_default().trim().is_empty()) { return Err(Error::InvalidInput( "pull request title, head, and base must not be empty".into(), )); } apis::repository_api::repo_create_pull_request( &self.configuration(), &repository.owner, &repository.repository, Some(option), ) .await .map_err(Error::generated) } pub async fn edit_pull( &self, repository: &RepositoryId, number: i64, option: models::EditPullRequestOption, ) -> Result { positive(number, "pull request number")?; apis::repository_api::repo_edit_pull_request( &self.configuration(), &repository.owner, &repository.repository, number, Some(option), ) .await .map_err(Error::generated) } pub async fn merge_pull( &self, repository: &RepositoryId, number: i64, option: models::MergePullRequestOption, ) -> Result<()> { positive(number, "pull request number")?; apis::repository_api::repo_merge_pull_request( &self.configuration(), &repository.owner, &repository.repository, number, Some(option), ) .await .map(|_| ()) .map_err(Error::generated) } pub async fn pull_commits( &self, repository: &RepositoryId, number: i64, ) -> Result> { positive(number, "pull request number")?; apis::repository_api::repo_get_pull_request_commits( &self.configuration(), &repository.owner, &repository.repository, number, None, None, None, None, ) .await .map_err(Error::generated) } pub async fn pull_files_page( &self, repository: &RepositoryId, number: i64, page: i32, limit: i32, ) -> Result> { positive(number, "pull request number")?; validate_page(page, limit)?; apis::repository_api::repo_get_pull_request_files( &self.configuration(), &repository.owner, &repository.repository, number, None, None, Some(page), Some(limit), ) .await .map(|items| Page::from_items(items, limit)) .map_err(Error::generated) } pub async fn pull_files( &self, repository: &RepositoryId, number: i64, ) -> Result> { self.pull_files_page(repository, number, 1, DEFAULT_PAGE_SIZE) .await .map(|page| page.items) } pub async fn pull_reviews( &self, repository: &RepositoryId, number: i64, ) -> Result> { positive(number, "pull request number")?; apis::repository_api::repo_list_pull_reviews( &self.configuration(), &repository.owner, &repository.repository, number, None, None, ) .await .map_err(Error::generated) } pub async fn pull_diff(&self, repository: &RepositoryId, number: i64) -> Result { positive(number, "pull request number")?; apis::repository_api::repo_download_pull_diff_or_patch( &self.configuration(), &repository.owner, &repository.repository, number, "diff", Some(false), ) .await .map_err(Error::generated) } } fn validate_page(page: i32, limit: i32) -> Result<()> { if page < 1 || limit < 1 { return Err(Error::InvalidInput( "page and limit must be positive".into(), )); } Ok(()) } fn positive(value: i64, name: &str) -> Result<()> { if value < 1 { return Err(Error::InvalidInput(format!( "{name} must be a positive integer" ))); } Ok(()) }