initial commit
This commit is contained in:
853
crates/cli/src/work_items.rs
Normal file
853
crates/cli/src/work_items.rs
Normal file
@@ -0,0 +1,853 @@
|
||||
use std::{error::Error, io::Read};
|
||||
|
||||
use gotcha_gitea::{
|
||||
Client, apis,
|
||||
models::{
|
||||
ChangedFile, Comment, Commit, CreateIssueCommentOption, CreateIssueOption,
|
||||
CreateMilestoneOption, CreatePullRequestOption, EditIssueOption, EditMilestoneOption,
|
||||
EditPullRequestOption, Issue, MergePullRequestOption, Milestone, PullRequest, PullReview,
|
||||
},
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::config::{RepositoryScope, Selection};
|
||||
|
||||
const ISSUE_HELP: &str = "\
|
||||
Usage: gotcha issue SUBCOMMAND
|
||||
|
||||
Subcommands:
|
||||
list [OWNER/REPOSITORY] List issues
|
||||
show INDEX [OWNER/REPOSITORY] Show an issue
|
||||
create [OWNER/REPOSITORY] Create from CreateIssueOption YAML on stdin
|
||||
edit INDEX [OWNER/REPOSITORY] Edit from EditIssueOption YAML on stdin
|
||||
delete INDEX [OWNER/REPOSITORY] Delete an issue
|
||||
comments INDEX [OWNER/REPOSITORY]
|
||||
List comments
|
||||
comment INDEX [OWNER/REPOSITORY] Add a comment read as text from stdin";
|
||||
|
||||
const MILESTONE_HELP: &str = "\
|
||||
Usage: gotcha milestone SUBCOMMAND
|
||||
|
||||
Subcommands:
|
||||
list [OWNER/REPOSITORY] List milestones
|
||||
show ID [OWNER/REPOSITORY] Show a milestone
|
||||
create [OWNER/REPOSITORY] Create from CreateMilestoneOption YAML on stdin
|
||||
edit ID [OWNER/REPOSITORY] Edit from EditMilestoneOption YAML on stdin
|
||||
delete ID [OWNER/REPOSITORY] Delete a milestone";
|
||||
|
||||
const PULL_HELP: &str = "\
|
||||
Usage: gotcha pull SUBCOMMAND
|
||||
|
||||
Subcommands:
|
||||
list [OWNER/REPOSITORY] List pull requests
|
||||
show INDEX [OWNER/REPOSITORY] Show a pull request
|
||||
create [OWNER/REPOSITORY] Create from CreatePullRequestOption YAML on stdin
|
||||
edit INDEX [OWNER/REPOSITORY] Edit from EditPullRequestOption YAML on stdin
|
||||
merge INDEX [OWNER/REPOSITORY] Merge from MergePullRequestOption YAML on stdin
|
||||
commits INDEX [OWNER/REPOSITORY] List commits
|
||||
files INDEX [OWNER/REPOSITORY] List changed files
|
||||
reviews INDEX [OWNER/REPOSITORY] List reviews";
|
||||
|
||||
pub fn domain_help(domain: &str) -> Option<&'static str> {
|
||||
match domain {
|
||||
"issue" => Some(ISSUE_HELP),
|
||||
"milestone" => Some(MILESTONE_HELP),
|
||||
"pull" => Some(PULL_HELP),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subcommand_help(domain: &str, command: &str) -> Option<&'static str> {
|
||||
match (domain, command) {
|
||||
("issue", "list") => Some("Usage: gotcha issue list [OWNER/REPOSITORY]"),
|
||||
("issue", "show") => Some("Usage: gotcha issue show INDEX [OWNER/REPOSITORY]"),
|
||||
("issue", "create") => Some(
|
||||
"Usage: gotcha issue create [OWNER/REPOSITORY] < issue.yaml\n\nReads CreateIssueOption YAML from stdin, for example:\n title: Fix the bug\n body: Reproduction steps",
|
||||
),
|
||||
("issue", "edit") => Some(
|
||||
"Usage: gotcha issue edit INDEX [OWNER/REPOSITORY] < issue.yaml\n\nReads EditIssueOption YAML from stdin.",
|
||||
),
|
||||
("issue", "delete") => Some("Usage: gotcha issue delete INDEX [OWNER/REPOSITORY]"),
|
||||
("issue", "comments") => Some("Usage: gotcha issue comments INDEX [OWNER/REPOSITORY]"),
|
||||
("issue", "comment") => Some(
|
||||
"Usage: gotcha issue comment INDEX [OWNER/REPOSITORY] < comment.txt\n\nReads the comment body as text from stdin.",
|
||||
),
|
||||
("milestone", "list") => Some("Usage: gotcha milestone list [OWNER/REPOSITORY]"),
|
||||
("milestone", "show") => Some("Usage: gotcha milestone show ID [OWNER/REPOSITORY]"),
|
||||
("milestone", "create") => Some(
|
||||
"Usage: gotcha milestone create [OWNER/REPOSITORY] < milestone.yaml\n\nReads CreateMilestoneOption YAML from stdin, for example:\n title: Version 1.0\n due_on: 2026-09-01T00:00:00Z",
|
||||
),
|
||||
("milestone", "edit") => Some(
|
||||
"Usage: gotcha milestone edit ID [OWNER/REPOSITORY] < milestone.yaml\n\nReads EditMilestoneOption YAML from stdin.",
|
||||
),
|
||||
("milestone", "delete") => Some("Usage: gotcha milestone delete ID [OWNER/REPOSITORY]"),
|
||||
("pull", "list") => Some("Usage: gotcha pull list [OWNER/REPOSITORY]"),
|
||||
("pull", "show") => Some("Usage: gotcha pull show INDEX [OWNER/REPOSITORY]"),
|
||||
("pull", "create") => Some(
|
||||
"Usage: gotcha pull create [OWNER/REPOSITORY] < pull.yaml\n\nReads CreatePullRequestOption YAML from stdin, for example:\n title: Add feature\n head: feature\n base: main",
|
||||
),
|
||||
("pull", "edit") => Some(
|
||||
"Usage: gotcha pull edit INDEX [OWNER/REPOSITORY] < pull.yaml\n\nReads EditPullRequestOption YAML from stdin.",
|
||||
),
|
||||
("pull", "merge") => Some(
|
||||
"Usage: gotcha pull merge INDEX [OWNER/REPOSITORY] < merge.yaml\n\nReads MergePullRequestOption YAML from stdin, for example:\n Do: squash\n delete_branch_after_merge: true",
|
||||
),
|
||||
("pull", "commits") => Some("Usage: gotcha pull commits INDEX [OWNER/REPOSITORY]"),
|
||||
("pull", "files") => Some("Usage: gotcha pull files INDEX [OWNER/REPOSITORY]"),
|
||||
("pull", "reviews") => Some("Usage: gotcha pull reviews INDEX [OWNER/REPOSITORY]"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
command: &[String],
|
||||
selection: &Selection,
|
||||
client: &Client,
|
||||
) -> Result<bool, Box<dyn Error>> {
|
||||
let configuration = client.configuration();
|
||||
match command {
|
||||
[domain, action] if domain == "issue" && action == "list" => {
|
||||
let scope = scope(selection, None)?;
|
||||
let issues = apis::issue_api::issue_list_issues(
|
||||
&configuration,
|
||||
&scope.owner,
|
||||
&scope.repository,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some("issues"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some("issues"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
print_issues(&issues);
|
||||
}
|
||||
[domain, action, repository] if domain == "issue" && action == "list" => {
|
||||
let scope = scope(selection, Some(repository))?;
|
||||
let issues = apis::issue_api::issue_list_issues(
|
||||
&configuration,
|
||||
&scope.owner,
|
||||
&scope.repository,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
print_issues(&issues);
|
||||
}
|
||||
[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?,
|
||||
);
|
||||
}
|
||||
[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?,
|
||||
);
|
||||
}
|
||||
[domain, action] if domain == "issue" && action == "create" => {
|
||||
create_issue(&configuration, &scope(selection, None)?).await?;
|
||||
}
|
||||
[domain, action, repository] if domain == "issue" && action == "create" => {
|
||||
create_issue(&configuration, &scope(selection, Some(repository))?).await?;
|
||||
}
|
||||
[domain, action, index] if domain == "issue" && action == "edit" => {
|
||||
edit_issue(&configuration, &scope(selection, None)?, number(index)?).await?;
|
||||
}
|
||||
[domain, action, index, repository] if domain == "issue" && action == "edit" => {
|
||||
edit_issue(
|
||||
&configuration,
|
||||
&scope(selection, Some(repository))?,
|
||||
number(index)?,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
[domain, action, index] if domain == "issue" && action == "delete" => {
|
||||
delete_issue(&configuration, &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?;
|
||||
}
|
||||
[domain, action, index] if domain == "issue" && action == "comments" => {
|
||||
list_comments(&configuration, &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?;
|
||||
}
|
||||
[domain, action, index] if domain == "issue" && action == "comment" => {
|
||||
create_comment(&configuration, &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?;
|
||||
}
|
||||
[domain, action] if domain == "milestone" && action == "list" => {
|
||||
list_milestones(&configuration, &scope(selection, None)?).await?;
|
||||
}
|
||||
[domain, action, repository] if domain == "milestone" && action == "list" => {
|
||||
list_milestones(&configuration, &scope(selection, Some(repository))?).await?;
|
||||
}
|
||||
[domain, action, id] if domain == "milestone" && action == "show" => {
|
||||
show_milestone(&configuration, &scope(selection, None)?, id).await?;
|
||||
}
|
||||
[domain, action, id, repository] if domain == "milestone" && action == "show" => {
|
||||
show_milestone(&configuration, &scope(selection, Some(repository))?, id).await?;
|
||||
}
|
||||
[domain, action] if domain == "milestone" && action == "create" => {
|
||||
create_milestone(&configuration, &scope(selection, None)?).await?;
|
||||
}
|
||||
[domain, action, repository] if domain == "milestone" && action == "create" => {
|
||||
create_milestone(&configuration, &scope(selection, Some(repository))?).await?;
|
||||
}
|
||||
[domain, action, id] if domain == "milestone" && action == "edit" => {
|
||||
edit_milestone(&configuration, &scope(selection, None)?, id).await?;
|
||||
}
|
||||
[domain, action, id, repository] if domain == "milestone" && action == "edit" => {
|
||||
edit_milestone(&configuration, &scope(selection, Some(repository))?, id).await?;
|
||||
}
|
||||
[domain, action, id] if domain == "milestone" && action == "delete" => {
|
||||
delete_milestone(&configuration, &scope(selection, None)?, id).await?;
|
||||
}
|
||||
[domain, action, id, repository] if domain == "milestone" && action == "delete" => {
|
||||
delete_milestone(&configuration, &scope(selection, Some(repository))?, id).await?;
|
||||
}
|
||||
[domain, action] if domain == "pull" && action == "list" => {
|
||||
list_pulls(&configuration, &scope(selection, None)?).await?;
|
||||
}
|
||||
[domain, action, repository] if domain == "pull" && action == "list" => {
|
||||
list_pulls(&configuration, &scope(selection, Some(repository))?).await?;
|
||||
}
|
||||
[domain, action, index] if domain == "pull" && action == "show" => {
|
||||
show_pull(&configuration, &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?;
|
||||
}
|
||||
[domain, action] if domain == "pull" && action == "create" => {
|
||||
create_pull(&configuration, &scope(selection, None)?).await?;
|
||||
}
|
||||
[domain, action, repository] if domain == "pull" && action == "create" => {
|
||||
create_pull(&configuration, &scope(selection, Some(repository))?).await?;
|
||||
}
|
||||
[domain, action, index] if domain == "pull" && action == "edit" => {
|
||||
edit_pull(&configuration, &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?;
|
||||
}
|
||||
[domain, action, index] if domain == "pull" && action == "merge" => {
|
||||
merge_pull(&configuration, &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?;
|
||||
}
|
||||
[domain, action, index]
|
||||
if domain == "pull" && matches!(action.as_str(), "commits" | "files" | "reviews") =>
|
||||
{
|
||||
pull_details(
|
||||
&configuration,
|
||||
&scope(selection, None)?,
|
||||
number(index)?,
|
||||
action,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
[domain, action, index, repository]
|
||||
if domain == "pull" && matches!(action.as_str(), "commits" | "files" | "reviews") =>
|
||||
{
|
||||
pull_details(
|
||||
&configuration,
|
||||
&scope(selection, Some(repository))?,
|
||||
number(index)?,
|
||||
action,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
[domain, ..] if matches!(domain.as_str(), "issue" | "milestone" | "pull") => {
|
||||
return Ok(false);
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn scope(selection: &Selection, value: Option<&str>) -> Result<RepositoryScope, Box<dyn Error>> {
|
||||
match value {
|
||||
Some(value) => Ok(RepositoryScope::parse(value)?),
|
||||
None => selection.repository.clone().ok_or_else(|| {
|
||||
"cannot infer a repository; run inside one or pass OWNER/REPOSITORY".into()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn number(value: &str) -> Result<i64, Box<dyn Error>> {
|
||||
let number: i64 = value.parse()?;
|
||||
if number < 1 {
|
||||
return Err("index must be a positive integer".into());
|
||||
}
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
fn read_stdin() -> Result<String, Box<dyn Error>> {
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_to_string(&mut input)?;
|
||||
if input.trim().is_empty() {
|
||||
return Err("stdin must not be empty".into());
|
||||
}
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
fn read_yaml<T: DeserializeOwned>() -> Result<T, Box<dyn Error>> {
|
||||
parse_yaml(&read_stdin()?)
|
||||
}
|
||||
|
||||
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 body: CreateIssueOption = read_yaml()?;
|
||||
if body.title.trim().is_empty() {
|
||||
return Err("issue title must not be empty".into());
|
||||
}
|
||||
print_issue(
|
||||
&apis::issue_api::issue_create_issue(
|
||||
configuration,
|
||||
&scope.owner,
|
||||
&scope.repository,
|
||||
Some(body),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn edit_issue(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
scope: &RepositoryScope,
|
||||
index: i64,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let issue = apis::issue_api::issue_edit_issue(
|
||||
configuration,
|
||||
&scope.owner,
|
||||
&scope.repository,
|
||||
index,
|
||||
Some(read_yaml::<EditIssueOption>()?),
|
||||
)
|
||||
.await?;
|
||||
print_issue(&issue);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_issue(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
scope: &RepositoryScope,
|
||||
index: i64,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
apis::issue_api::issue_delete(configuration, &scope.owner, &scope.repository, index).await?;
|
||||
println!("Deleted issue #{index}.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_comments(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
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?;
|
||||
print_comments(&comments);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_comment(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
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?;
|
||||
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?;
|
||||
print_milestones(&milestones);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_milestone(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
scope: &RepositoryScope,
|
||||
id: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let milestone =
|
||||
apis::issue_api::issue_get_milestone(configuration, &scope.owner, &scope.repository, id)
|
||||
.await?;
|
||||
print_milestone(&milestone);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_milestone(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
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?;
|
||||
print_milestone(&milestone);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn edit_milestone(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
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?;
|
||||
print_milestone(&milestone);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_milestone(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
scope: &RepositoryScope,
|
||||
id: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
apis::issue_api::issue_delete_milestone(configuration, &scope.owner, &scope.repository, 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);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_pull(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
scope: &RepositoryScope,
|
||||
index: i64,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let pull = apis::repository_api::repo_get_pull_request(
|
||||
configuration,
|
||||
&scope.owner,
|
||||
&scope.repository,
|
||||
index,
|
||||
)
|
||||
.await?;
|
||||
print_pull(&pull);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_pull(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
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?;
|
||||
print_pull(&pull);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn edit_pull(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
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?;
|
||||
print_pull(&pull);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn merge_pull(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
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?;
|
||||
println!("Merged pull request #{index}.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pull_details(
|
||||
configuration: &apis::configuration::Configuration,
|
||||
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?,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_issues(issues: &[Issue]) {
|
||||
println!("INDEX\tSTATE\tTITLE\tUPDATED");
|
||||
for issue in issues {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{}",
|
||||
issue.number.unwrap_or_default(),
|
||||
issue.state.as_deref().unwrap_or("unknown"),
|
||||
one_line(issue.title.as_deref().unwrap_or("")),
|
||||
issue.updated_at.as_deref().unwrap_or("-")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_issue(issue: &Issue) {
|
||||
println!(
|
||||
"#{} [{}] {}",
|
||||
issue.number.unwrap_or_default(),
|
||||
issue.state.as_deref().unwrap_or("unknown"),
|
||||
issue.title.as_deref().unwrap_or("")
|
||||
);
|
||||
field("url", issue.html_url.as_deref());
|
||||
field(
|
||||
"author",
|
||||
issue.user.as_deref().and_then(|user| user.login.as_deref()),
|
||||
);
|
||||
field("updated", issue.updated_at.as_deref());
|
||||
body(issue.body.as_deref());
|
||||
}
|
||||
|
||||
fn print_comments(comments: &[Comment]) {
|
||||
for comment in comments {
|
||||
println!(
|
||||
"{} · {}",
|
||||
comment
|
||||
.user
|
||||
.as_deref()
|
||||
.and_then(|user| user.login.as_deref())
|
||||
.unwrap_or("unknown"),
|
||||
comment.created_at.as_deref().unwrap_or("-")
|
||||
);
|
||||
body(comment.body.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
fn print_milestones(milestones: &[Milestone]) {
|
||||
println!("ID\tSTATE\tTITLE\tDUE\tOPEN/CLOSED");
|
||||
for milestone in milestones {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{}\t{}/{}",
|
||||
milestone.id.unwrap_or_default(),
|
||||
milestone.state.as_deref().unwrap_or("unknown"),
|
||||
one_line(milestone.title.as_deref().unwrap_or("")),
|
||||
milestone.due_on.as_deref().unwrap_or("-"),
|
||||
milestone.open_issues.unwrap_or_default(),
|
||||
milestone.closed_issues.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_milestone(milestone: &Milestone) {
|
||||
println!(
|
||||
"{} [{}] {}",
|
||||
milestone.id.unwrap_or_default(),
|
||||
milestone.state.as_deref().unwrap_or("unknown"),
|
||||
milestone.title.as_deref().unwrap_or("")
|
||||
);
|
||||
field("due", milestone.due_on.as_deref());
|
||||
println!(
|
||||
"issues: {} open, {} closed",
|
||||
milestone.open_issues.unwrap_or_default(),
|
||||
milestone.closed_issues.unwrap_or_default()
|
||||
);
|
||||
body(milestone.description.as_deref());
|
||||
}
|
||||
|
||||
fn print_pulls(pulls: &[PullRequest]) {
|
||||
println!("INDEX\tSTATE\tTITLE\tUPDATED");
|
||||
for pull in pulls {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{}",
|
||||
pull.number.unwrap_or_default(),
|
||||
pull.state.as_deref().unwrap_or("unknown"),
|
||||
one_line(pull.title.as_deref().unwrap_or("")),
|
||||
pull.updated_at.as_deref().unwrap_or("-")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_pull(pull: &PullRequest) {
|
||||
println!(
|
||||
"#{} [{}] {}",
|
||||
pull.number.unwrap_or_default(),
|
||||
pull.state.as_deref().unwrap_or("unknown"),
|
||||
pull.title.as_deref().unwrap_or("")
|
||||
);
|
||||
field("url", pull.html_url.as_deref());
|
||||
field(
|
||||
"author",
|
||||
pull.user.as_deref().and_then(|user| user.login.as_deref()),
|
||||
);
|
||||
field("updated", pull.updated_at.as_deref());
|
||||
println!("mergeable: {}", pull.mergeable.unwrap_or(false));
|
||||
body(pull.body.as_deref());
|
||||
}
|
||||
|
||||
fn print_commits(commits: &[Commit]) {
|
||||
println!("SHA\tMESSAGE");
|
||||
for commit in commits {
|
||||
println!(
|
||||
"{}\t{}",
|
||||
commit.sha.as_deref().unwrap_or("unknown"),
|
||||
one_line(
|
||||
commit
|
||||
.commit
|
||||
.as_deref()
|
||||
.and_then(|commit| commit.message.as_deref())
|
||||
.unwrap_or("")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_files(files: &[ChangedFile]) {
|
||||
println!("STATUS\tCHANGES\tFILE");
|
||||
for file in files {
|
||||
println!(
|
||||
"{}\t{}\t{}",
|
||||
file.status.as_deref().unwrap_or("unknown"),
|
||||
file.changes.unwrap_or_default(),
|
||||
file.filename.as_deref().unwrap_or("")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_reviews(reviews: &[PullReview]) {
|
||||
println!("ID\tSTATE\tREVIEWER\tSUBMITTED");
|
||||
for review in reviews {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{}",
|
||||
review.id.unwrap_or_default(),
|
||||
review.state.as_deref().unwrap_or("unknown"),
|
||||
review
|
||||
.user
|
||||
.as_deref()
|
||||
.and_then(|user| user.login.as_deref())
|
||||
.unwrap_or("unknown"),
|
||||
review.submitted_at.as_deref().unwrap_or("-")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn field(name: &str, value: Option<&str>) {
|
||||
if let Some(value) = value.filter(|value| !value.is_empty()) {
|
||||
println!("{name}: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
fn body(value: Option<&str>) {
|
||||
if let Some(value) = value.filter(|value| !value.is_empty()) {
|
||||
println!("\n{value}");
|
||||
}
|
||||
}
|
||||
|
||||
fn one_line(value: &str) -> &str {
|
||||
value.lines().next().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_typed_yaml_and_rejects_bad_indexes() {
|
||||
let issue: CreateIssueOption = parse_yaml("title: Fix it\nbody: Details\n").unwrap();
|
||||
|
||||
assert_eq!(issue.title, "Fix it");
|
||||
assert_eq!(issue.body.as_deref(), Some("Details"));
|
||||
assert!(number("0").is_err());
|
||||
assert!(number("7").is_ok());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user