343 lines
9.1 KiB
Rust
343 lines
9.1 KiB
Rust
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<Page<models::PullRequest>> {
|
|
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<Page<models::Issue>> {
|
|
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<Vec<String>> {
|
|
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::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect())
|
|
}
|
|
|
|
async fn all_searched_pulls(&self, state: &str) -> Result<Vec<models::Issue>> {
|
|
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<models::PullRequest> {
|
|
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<PullDetails> {
|
|
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<models::PullRequest> {
|
|
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<models::PullRequest> {
|
|
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<Vec<models::Commit>> {
|
|
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<Page<models::ChangedFile>> {
|
|
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<Vec<models::ChangedFile>> {
|
|
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<Vec<models::PullReview>> {
|
|
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<String> {
|
|
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(())
|
|
}
|