Centralize Gitea domain workflows

This commit is contained in:
Georg Bauer
2026-07-31 16:44:16 +02:00
parent b468a2670c
commit f38c3c4939
19 changed files with 2248 additions and 1624 deletions

View File

@@ -10,7 +10,7 @@ use std::{
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use gotcha_gitea::{Client, Url};
use gotcha_gitea::{Client, RepositoryId, Url};
use serde::{Deserialize, Serialize};
type Result<T> = std::result::Result<T, String>;
@@ -36,26 +36,7 @@ pub struct Selection {
pub repository: Option<RepositoryScope>,
}
#[derive(Clone)]
pub struct RepositoryScope {
pub owner: String,
pub repository: String,
}
impl RepositoryScope {
pub fn parse(value: &str) -> Result<Self> {
let (owner, repository) = value
.split_once('/')
.ok_or("repository must be OWNER/REPOSITORY")?;
if owner.is_empty() || repository.is_empty() || repository.contains('/') {
return Err("repository must be OWNER/REPOSITORY".into());
}
Ok(Self {
owner: owner.into(),
repository: repository.into(),
})
}
}
pub type RepositoryScope = RepositoryId;
impl Config {
pub fn load() -> Result<Self> {

View File

@@ -1,12 +1,13 @@
use std::{error::Error, io::Read};
use gotcha_gitea::{
Client, apis,
Client, CreateIssue, EditIssue, IssueQuery,
models::{
ChangedFile, Comment, Commit, CreateIssueCommentOption, CreateIssueOption,
CreateMilestoneOption, CreatePullRequestOption, EditIssueOption, EditMilestoneOption,
EditPullRequestOption, Issue, MergePullRequestOption, Milestone, PullRequest, PullReview,
ChangedFile, Comment, Commit, CreateIssueOption, CreateMilestoneOption,
CreatePullRequestOption, EditIssueOption, EditMilestoneOption, EditPullRequestOption,
Issue, MergePullRequestOption, Milestone, PullRequest, PullReview,
},
pull_state,
};
use serde::{Deserialize, de::DeserializeOwned};
@@ -248,69 +249,48 @@ pub async fn run(
selection: &Selection,
client: &Client,
) -> Result<bool, Box<dyn Error>> {
let configuration = client.configuration();
match command {
[domain, action, arguments @ ..] if domain == "issue" && action == "list" => {
let options = parse_issue_list(arguments)?;
let scope = scope(selection, options.repository.as_deref())?;
let issues = apis::issue_api::issue_list_issues(
&configuration,
&scope.owner,
&scope.repository,
Some(&options.state),
options.labels.as_deref(),
options.keyword.as_deref(),
Some(&options.kind),
options.milestones.as_deref(),
options.from,
options.until,
options.author.as_deref(),
options.assignee.as_deref(),
options.mentions.as_deref(),
Some(options.page),
Some(options.limit),
)
.await?;
print_issues(&issues);
let issues = client
.issues(
&scope,
&IssueQuery {
state: options.state,
labels: options.labels,
keyword: options.keyword,
kind: options.kind,
milestones: options.milestones,
from: options.from,
until: options.until,
author: options.author,
assignee: options.assignee,
mentions: options.mentions,
page: options.page,
limit: options.limit,
},
)
.await?;
print_issues(&issues.items);
}
[domain, action, index] if domain == "issue" && action == "show" => {
let scope = scope(selection, None)?;
print_issue(
&apis::issue_api::issue_get_issue(
&configuration,
&scope.owner,
&scope.repository,
number(index)?,
)
.await?,
);
print_issue(&client.issue(&scope, number(index)?).await?);
}
[domain, action, index, repository] if domain == "issue" && action == "show" => {
let scope = scope(selection, Some(repository))?;
print_issue(
&apis::issue_api::issue_get_issue(
&configuration,
&scope.owner,
&scope.repository,
number(index)?,
)
.await?,
);
print_issue(&client.issue(&scope, number(index)?).await?);
}
[domain, action] if domain == "issue" && action == "create" => {
create_issue(&configuration, &scope(selection, None)?).await?;
create_issue(client, &scope(selection, None)?).await?;
}
[domain, action, repository] if domain == "issue" && action == "create" => {
create_issue(&configuration, &scope(selection, Some(repository))?).await?;
create_issue(client, &scope(selection, Some(repository))?).await?;
}
[domain, action, arguments @ ..] if domain == "issue" && action == "edit" => {
let (indexes, repository) = issue_targets(arguments)?;
edit_issues(
&configuration,
&scope(selection, repository.as_deref())?,
&indexes,
)
.await?;
edit_issues(client, &scope(selection, repository.as_deref())?, &indexes).await?;
}
[domain, action, arguments @ ..]
if domain == "issue" && matches!(action.as_str(), "close" | "reopen") =>
@@ -318,7 +298,7 @@ pub async fn run(
let (indexes, repository) = issue_targets(arguments)?;
let scope = scope(selection, repository.as_deref())?;
set_issue_state(
&configuration,
client,
&scope,
&indexes,
if action == "close" { "closed" } else { "open" },
@@ -326,129 +306,93 @@ pub async fn run(
.await?;
}
[domain, action, index] if domain == "issue" && action == "delete" => {
delete_issue(&configuration, &scope(selection, None)?, number(index)?).await?;
delete_issue(client, &scope(selection, None)?, number(index)?).await?;
}
[domain, action, index, repository] if domain == "issue" && action == "delete" => {
delete_issue(
&configuration,
&scope(selection, Some(repository))?,
number(index)?,
)
.await?;
delete_issue(client, &scope(selection, Some(repository))?, number(index)?).await?;
}
[domain, action, index] if domain == "issue" && action == "comments" => {
list_comments(&configuration, &scope(selection, None)?, number(index)?).await?;
list_comments(client, &scope(selection, None)?, number(index)?).await?;
}
[domain, action, index, repository] if domain == "issue" && action == "comments" => {
list_comments(
&configuration,
&scope(selection, Some(repository))?,
number(index)?,
)
.await?;
list_comments(client, &scope(selection, Some(repository))?, number(index)?).await?;
}
[domain, action, index] if domain == "issue" && action == "comment" => {
create_comment(&configuration, &scope(selection, None)?, number(index)?).await?;
create_comment(client, &scope(selection, None)?, number(index)?).await?;
}
[domain, action, index, repository] if domain == "issue" && action == "comment" => {
create_comment(
&configuration,
&scope(selection, Some(repository))?,
number(index)?,
)
.await?;
create_comment(client, &scope(selection, Some(repository))?, number(index)?).await?;
}
[domain, action] if domain == "milestone" && action == "list" => {
list_milestones(&configuration, &scope(selection, None)?).await?;
list_milestones(client, &scope(selection, None)?).await?;
}
[domain, action, repository] if domain == "milestone" && action == "list" => {
list_milestones(&configuration, &scope(selection, Some(repository))?).await?;
list_milestones(client, &scope(selection, Some(repository))?).await?;
}
[domain, action, id] if domain == "milestone" && action == "show" => {
show_milestone(&configuration, &scope(selection, None)?, id).await?;
show_milestone(client, &scope(selection, None)?, id).await?;
}
[domain, action, id, repository] if domain == "milestone" && action == "show" => {
show_milestone(&configuration, &scope(selection, Some(repository))?, id).await?;
show_milestone(client, &scope(selection, Some(repository))?, id).await?;
}
[domain, action] if domain == "milestone" && action == "create" => {
create_milestone(&configuration, &scope(selection, None)?).await?;
create_milestone(client, &scope(selection, None)?).await?;
}
[domain, action, repository] if domain == "milestone" && action == "create" => {
create_milestone(&configuration, &scope(selection, Some(repository))?).await?;
create_milestone(client, &scope(selection, Some(repository))?).await?;
}
[domain, action, id] if domain == "milestone" && action == "edit" => {
edit_milestone(&configuration, &scope(selection, None)?, id).await?;
edit_milestone(client, &scope(selection, None)?, id).await?;
}
[domain, action, id, repository] if domain == "milestone" && action == "edit" => {
edit_milestone(&configuration, &scope(selection, Some(repository))?, id).await?;
edit_milestone(client, &scope(selection, Some(repository))?, id).await?;
}
[domain, action, id] if domain == "milestone" && action == "delete" => {
delete_milestone(&configuration, &scope(selection, None)?, id).await?;
delete_milestone(client, &scope(selection, None)?, id).await?;
}
[domain, action, id, repository] if domain == "milestone" && action == "delete" => {
delete_milestone(&configuration, &scope(selection, Some(repository))?, id).await?;
delete_milestone(client, &scope(selection, Some(repository))?, id).await?;
}
[domain, action] if domain == "pull" && action == "list" => {
list_pulls(&configuration, &scope(selection, None)?).await?;
list_pulls(client, &scope(selection, None)?).await?;
}
[domain, action, repository] if domain == "pull" && action == "list" => {
list_pulls(&configuration, &scope(selection, Some(repository))?).await?;
list_pulls(client, &scope(selection, Some(repository))?).await?;
}
[domain, action, index] if domain == "pull" && action == "show" => {
show_pull(&configuration, &scope(selection, None)?, number(index)?).await?;
show_pull(client, &scope(selection, None)?, number(index)?).await?;
}
[domain, action, index, repository] if domain == "pull" && action == "show" => {
show_pull(
&configuration,
&scope(selection, Some(repository))?,
number(index)?,
)
.await?;
show_pull(client, &scope(selection, Some(repository))?, number(index)?).await?;
}
[domain, action] if domain == "pull" && action == "create" => {
create_pull(&configuration, &scope(selection, None)?).await?;
create_pull(client, &scope(selection, None)?).await?;
}
[domain, action, repository] if domain == "pull" && action == "create" => {
create_pull(&configuration, &scope(selection, Some(repository))?).await?;
create_pull(client, &scope(selection, Some(repository))?).await?;
}
[domain, action, index] if domain == "pull" && action == "edit" => {
edit_pull(&configuration, &scope(selection, None)?, number(index)?).await?;
edit_pull(client, &scope(selection, None)?, number(index)?).await?;
}
[domain, action, index, repository] if domain == "pull" && action == "edit" => {
edit_pull(
&configuration,
&scope(selection, Some(repository))?,
number(index)?,
)
.await?;
edit_pull(client, &scope(selection, Some(repository))?, number(index)?).await?;
}
[domain, action, index] if domain == "pull" && action == "merge" => {
merge_pull(&configuration, &scope(selection, None)?, number(index)?).await?;
merge_pull(client, &scope(selection, None)?, number(index)?).await?;
}
[domain, action, index, repository] if domain == "pull" && action == "merge" => {
merge_pull(
&configuration,
&scope(selection, Some(repository))?,
number(index)?,
)
.await?;
merge_pull(client, &scope(selection, Some(repository))?, number(index)?).await?;
}
[domain, action, index]
if domain == "pull" && matches!(action.as_str(), "commits" | "files" | "reviews") =>
{
pull_details(
&configuration,
&scope(selection, None)?,
number(index)?,
action,
)
.await?;
pull_details(client, &scope(selection, None)?, number(index)?, action).await?;
}
[domain, action, index, repository]
if domain == "pull" && matches!(action.as_str(), "commits" | "files" | "reviews") =>
{
pull_details(
&configuration,
client,
&scope(selection, Some(repository))?,
number(index)?,
action,
@@ -497,475 +441,193 @@ fn parse_yaml<T: DeserializeOwned>(input: &str) -> Result<T, Box<dyn Error>> {
Ok(serde_yaml::from_str(input)?)
}
async fn create_issue(
configuration: &apis::configuration::Configuration,
scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
let mut input: CreateIssueInput = read_yaml()?;
if input.issue.title.trim().is_empty() {
return Err("issue title must not be empty".into());
}
if input.issue.milestone.is_some() && input.milestone_name.is_some() {
return Err("use either milestone or milestone_name, not both".into());
}
if let Some(name) = input.milestone_name.as_deref() {
input.issue.milestone = Some(resolve_milestone_id(configuration, scope, name).await?);
}
if !input.label_names.is_empty() {
input
.issue
.labels
.get_or_insert_default()
.extend(resolve_label_ids(configuration, scope, &input.label_names).await?);
}
async fn create_issue(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
let input: CreateIssueInput = read_yaml()?;
print_issue(
&apis::issue_api::issue_create_issue(
configuration,
&scope.owner,
&scope.repository,
Some(input.issue),
)
.await?,
);
Ok(())
}
async fn edit_issues(
configuration: &apis::configuration::Configuration,
scope: &RepositoryScope,
indexes: &[i64],
) -> Result<(), Box<dyn Error>> {
let mut input: EditIssueInput = read_yaml()?;
if input.issue.milestone.is_some() && input.milestone_name.is_some() {
return Err("use either milestone or milestone_name, not both".into());
}
if let Some(name) = input.milestone_name.as_deref() {
input.issue.milestone = Some(resolve_milestone_id(configuration, scope, name).await?);
}
if input.issue.assignees.is_some() && !input.add_assignees.is_empty() {
return Err("use either assignees or add_assignees, not both".into());
}
let remove = resolve_label_ids(configuration, scope, &input.remove_labels).await?;
let add = resolve_label_ids(configuration, scope, &input.add_labels).await?;
for &index in indexes {
apply_issue_edit(configuration, scope, index, &input, &remove, &add).await?;
}
Ok(())
}
async fn apply_issue_edit(
configuration: &apis::configuration::Configuration,
scope: &RepositoryScope,
index: i64,
input: &EditIssueInput,
remove: &[i64],
add: &[i64],
) -> Result<(), Box<dyn Error>> {
let mut edit = input.issue.clone();
if !input.add_assignees.is_empty() {
let issue =
apis::issue_api::issue_get_issue(configuration, &scope.owner, &scope.repository, index)
.await?;
let mut assignees = issue
.assignees
.as_deref()
.unwrap_or_default()
.iter()
.filter_map(|user| user.login.clone())
.collect::<Vec<_>>();
for assignee in &input.add_assignees {
if !assignees.contains(assignee) {
assignees.push(assignee.clone());
}
}
edit.assignees = Some(assignees);
}
if edit != EditIssueOption::default() {
apis::issue_api::issue_edit_issue(
configuration,
&scope.owner,
&scope.repository,
index,
Some(edit),
)
.await?;
}
for &id in remove {
apis::issue_api::issue_remove_label(
configuration,
&scope.owner,
&scope.repository,
index,
id,
)
.await?;
}
if !add.is_empty() {
apis::issue_api::issue_add_label(
configuration,
&scope.owner,
&scope.repository,
index,
Some(gotcha_gitea::models::IssueLabelsOption {
labels: Some(add.iter().copied().map(serde_json::Value::from).collect()),
}),
)
.await?;
}
print_issue(
&apis::issue_api::issue_get_issue(configuration, &scope.owner, &scope.repository, index)
&client
.create_issue(
scope,
CreateIssue {
option: input.issue,
label_names: input.label_names,
milestone_name: input.milestone_name,
},
)
.await?,
);
Ok(())
}
async fn resolve_label_ids(
configuration: &apis::configuration::Configuration,
async fn edit_issues(
client: &Client,
scope: &RepositoryScope,
names: &[String],
) -> Result<Vec<i64>, Box<dyn Error>> {
if names.is_empty() {
return Ok(Vec::new());
}
let mut labels = Vec::new();
let mut page = 1;
loop {
let batch = apis::issue_api::issue_list_labels(
configuration,
&scope.owner,
&scope.repository,
Some(page),
Some(50),
indexes: &[i64],
) -> Result<(), Box<dyn Error>> {
let input: EditIssueInput = read_yaml()?;
for issue in client
.edit_issues(
scope,
indexes,
EditIssue {
option: input.issue,
replace_labels: None,
add_labels: input.add_labels,
remove_labels: input.remove_labels,
add_assignees: input.add_assignees,
milestone_name: input.milestone_name,
},
)
.await?;
let has_more = batch.len() == 50;
labels.extend(batch);
if !has_more {
break;
}
page += 1;
.await?
{
print_issue(&issue);
}
names
.iter()
.map(|name| {
labels
.iter()
.find(|label| label.name.as_deref() == Some(name))
.and_then(|label| label.id)
.ok_or_else(|| format!("unknown label {name:?}").into())
})
.collect()
}
async fn resolve_milestone_id(
configuration: &apis::configuration::Configuration,
scope: &RepositoryScope,
name: &str,
) -> Result<i64, Box<dyn Error>> {
apis::issue_api::issue_get_milestones_list(
configuration,
&scope.owner,
&scope.repository,
Some("all"),
Some(name),
Some(1),
Some(2),
)
.await?
.into_iter()
.find(|milestone| milestone.title.as_deref() == Some(name))
.and_then(|milestone| milestone.id)
.ok_or_else(|| format!("unknown milestone {name:?}").into())
Ok(())
}
async fn set_issue_state(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
indexes: &[i64],
state: &str,
) -> Result<(), Box<dyn Error>> {
for &index in indexes {
let issue = apis::issue_api::issue_edit_issue(
configuration,
&scope.owner,
&scope.repository,
index,
Some(EditIssueOption {
state: Some(state.into()),
..Default::default()
}),
)
.await?;
for issue in client.set_issue_state(scope, indexes, state).await? {
print_issue(&issue);
}
Ok(())
}
async fn delete_issue(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
index: i64,
) -> Result<(), Box<dyn Error>> {
apis::issue_api::issue_delete(configuration, &scope.owner, &scope.repository, index).await?;
client.delete_issue(scope, index).await?;
println!("Deleted issue #{index}.");
Ok(())
}
async fn list_comments(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
index: i64,
) -> Result<(), Box<dyn Error>> {
let comments = apis::issue_api::issue_get_comments(
configuration,
&scope.owner,
&scope.repository,
index,
None,
None,
)
.await?;
let comments = client.issue_comments(scope, index).await?;
print_comments(&comments);
Ok(())
}
async fn create_comment(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
index: i64,
) -> Result<(), Box<dyn Error>> {
let body = read_stdin()?.trim_end().to_owned();
let comment = apis::issue_api::issue_create_comment(
configuration,
&scope.owner,
&scope.repository,
index,
Some(CreateIssueCommentOption::new(body)),
)
.await?;
let comment = client.create_issue_comment(scope, index, body).await?;
print_comments(&[comment]);
Ok(())
}
async fn list_milestones(
configuration: &apis::configuration::Configuration,
scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
let milestones = apis::issue_api::issue_get_milestones_list(
configuration,
&scope.owner,
&scope.repository,
None,
None,
None,
None,
)
.await?;
async fn list_milestones(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
let milestones = client.milestones(scope).await?;
print_milestones(&milestones);
Ok(())
}
async fn show_milestone(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
id: &str,
) -> Result<(), Box<dyn Error>> {
let milestone =
apis::issue_api::issue_get_milestone(configuration, &scope.owner, &scope.repository, id)
.await?;
let milestone = client.milestone(scope, number(id)?).await?;
print_milestone(&milestone);
Ok(())
}
async fn create_milestone(
configuration: &apis::configuration::Configuration,
scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
async fn create_milestone(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
let body: CreateMilestoneOption = read_yaml()?;
if body.title.as_deref().unwrap_or_default().trim().is_empty() {
return Err("milestone title must not be empty".into());
}
let milestone = apis::issue_api::issue_create_milestone(
configuration,
&scope.owner,
&scope.repository,
Some(body),
)
.await?;
let milestone = client.create_milestone(scope, body).await?;
print_milestone(&milestone);
Ok(())
}
async fn edit_milestone(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
id: &str,
) -> Result<(), Box<dyn Error>> {
let milestone = apis::issue_api::issue_edit_milestone(
configuration,
&scope.owner,
&scope.repository,
id,
Some(read_yaml::<EditMilestoneOption>()?),
)
.await?;
let milestone = client
.edit_milestone(scope, id, read_yaml::<EditMilestoneOption>()?)
.await?;
print_milestone(&milestone);
Ok(())
}
async fn delete_milestone(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
id: &str,
) -> Result<(), Box<dyn Error>> {
apis::issue_api::issue_delete_milestone(configuration, &scope.owner, &scope.repository, id)
.await?;
client.delete_milestone(scope, id).await?;
println!("Deleted milestone {id}.");
Ok(())
}
async fn list_pulls(
configuration: &apis::configuration::Configuration,
scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
let pulls = apis::repository_api::repo_list_pull_requests(
configuration,
&scope.owner,
&scope.repository,
None,
None,
None,
None,
None,
None,
None,
None,
)
.await?;
print_pulls(&pulls);
async fn list_pulls(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
print_pulls(&client.repository_pulls(scope, "open", 1, 30).await?.items);
Ok(())
}
async fn show_pull(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
index: i64,
) -> Result<(), Box<dyn Error>> {
let pull = apis::repository_api::repo_get_pull_request(
configuration,
&scope.owner,
&scope.repository,
index,
)
.await?;
let pull = client.pull(scope, index).await?;
print_pull(&pull);
Ok(())
}
async fn create_pull(
configuration: &apis::configuration::Configuration,
scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
async fn create_pull(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
let body: CreatePullRequestOption = read_yaml()?;
if [
body.title.as_deref(),
body.head.as_deref(),
body.base.as_deref(),
]
.into_iter()
.any(|value| value.unwrap_or_default().trim().is_empty())
{
return Err("pull request title, head, and base must not be empty".into());
}
let pull = apis::repository_api::repo_create_pull_request(
configuration,
&scope.owner,
&scope.repository,
Some(body),
)
.await?;
let pull = client.create_pull(scope, body).await?;
print_pull(&pull);
Ok(())
}
async fn edit_pull(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
index: i64,
) -> Result<(), Box<dyn Error>> {
let pull = apis::repository_api::repo_edit_pull_request(
configuration,
&scope.owner,
&scope.repository,
index,
Some(read_yaml::<EditPullRequestOption>()?),
)
.await?;
let pull = client
.edit_pull(scope, index, read_yaml::<EditPullRequestOption>()?)
.await?;
print_pull(&pull);
Ok(())
}
async fn merge_pull(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
index: i64,
) -> Result<(), Box<dyn Error>> {
apis::repository_api::repo_merge_pull_request(
configuration,
&scope.owner,
&scope.repository,
index,
Some(read_yaml::<MergePullRequestOption>()?),
)
.await?;
client
.merge_pull(scope, index, read_yaml::<MergePullRequestOption>()?)
.await?;
println!("Merged pull request #{index}.");
Ok(())
}
async fn pull_details(
configuration: &apis::configuration::Configuration,
client: &Client,
scope: &RepositoryScope,
index: i64,
action: &str,
) -> Result<(), Box<dyn Error>> {
match action {
"commits" => print_commits(
&apis::repository_api::repo_get_pull_request_commits(
configuration,
&scope.owner,
&scope.repository,
index,
None,
None,
None,
None,
)
.await?,
),
"files" => print_files(
&apis::repository_api::repo_get_pull_request_files(
configuration,
&scope.owner,
&scope.repository,
index,
None,
None,
None,
None,
)
.await?,
),
"reviews" => print_reviews(
&apis::repository_api::repo_list_pull_reviews(
configuration,
&scope.owner,
&scope.repository,
index,
None,
None,
)
.await?,
),
"commits" => print_commits(&client.pull_commits(scope, index).await?),
"files" => print_files(&client.pull_files(scope, index).await?),
"reviews" => print_reviews(&client.pull_reviews(scope, index).await?),
_ => unreachable!(),
}
Ok(())
@@ -1129,7 +791,7 @@ fn print_pull(pull: &PullRequest) {
println!(
"#{} [{}] {}",
pull.number.unwrap_or_default(),
pull.state.as_deref().unwrap_or("unknown"),
pull_state(pull),
pull.title.as_deref().unwrap_or("")
);
field("url", pull.html_url.as_deref());