mirror of
https://codeberg.org/Cyborus/forgejo-cli.git
synced 2024-11-12 21:19:49 +01:00
Merge pull request 'repo fork
command' (#83) from fork into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-cli/pulls/83
This commit is contained in:
commit
88d5356a44
4 changed files with 164 additions and 51 deletions
|
@ -7,7 +7,7 @@ use forgejo_api::structs::{
|
||||||
};
|
};
|
||||||
use forgejo_api::Forgejo;
|
use forgejo_api::Forgejo;
|
||||||
|
|
||||||
use crate::repo::{RepoInfo, RepoName};
|
use crate::repo::{RepoArg, RepoInfo, RepoName};
|
||||||
|
|
||||||
#[derive(Args, Clone, Debug)]
|
#[derive(Args, Clone, Debug)]
|
||||||
pub struct IssueCommand {
|
pub struct IssueCommand {
|
||||||
|
@ -24,7 +24,7 @@ pub enum IssueSubcommand {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
#[clap(long, short)]
|
#[clap(long, short)]
|
||||||
repo: Option<String>,
|
repo: Option<RepoArg>,
|
||||||
},
|
},
|
||||||
Edit {
|
Edit {
|
||||||
issue: IssueId,
|
issue: IssueId,
|
||||||
|
@ -42,7 +42,7 @@ pub enum IssueSubcommand {
|
||||||
},
|
},
|
||||||
Search {
|
Search {
|
||||||
#[clap(long, short)]
|
#[clap(long, short)]
|
||||||
repo: Option<String>,
|
repo: Option<RepoArg>,
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
#[clap(long, short)]
|
#[clap(long, short)]
|
||||||
labels: Option<String>,
|
labels: Option<String>,
|
||||||
|
@ -65,16 +65,16 @@ pub enum IssueSubcommand {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct IssueId {
|
pub struct IssueId {
|
||||||
pub repo: Option<String>,
|
pub repo: Option<RepoArg>,
|
||||||
pub number: u64,
|
pub number: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for IssueId {
|
impl FromStr for IssueId {
|
||||||
type Err = std::num::ParseIntError;
|
type Err = IssueIdError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let (repo, number) = match s.rsplit_once("#") {
|
let (repo, number) = match s.rsplit_once("#") {
|
||||||
Some((repo, number)) => (Some(repo.to_owned()), number),
|
Some((repo, number)) => (Some(repo.parse::<RepoArg>()?), number),
|
||||||
None => (None, s),
|
None => (None, s),
|
||||||
};
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -84,6 +84,35 @@ impl FromStr for IssueId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum IssueIdError {
|
||||||
|
Repo(crate::repo::RepoArgError),
|
||||||
|
Number(std::num::ParseIntError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for IssueIdError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
IssueIdError::Repo(e) => e.fmt(f),
|
||||||
|
IssueIdError::Number(e) => e.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::repo::RepoArgError> for IssueIdError {
|
||||||
|
fn from(value: crate::repo::RepoArgError) -> Self {
|
||||||
|
Self::Repo(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::num::ParseIntError> for IssueIdError {
|
||||||
|
fn from(value: std::num::ParseIntError) -> Self {
|
||||||
|
Self::Number(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for IssueIdError {}
|
||||||
|
|
||||||
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
|
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
Open,
|
Open,
|
||||||
|
@ -163,15 +192,15 @@ impl IssueCommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repo(&self) -> Option<&str> {
|
fn repo(&self) -> Option<&RepoArg> {
|
||||||
use IssueSubcommand::*;
|
use IssueSubcommand::*;
|
||||||
match &self.command {
|
match &self.command {
|
||||||
Create { repo, .. } | Search { repo, .. } => repo.as_deref(),
|
Create { repo, .. } | Search { repo, .. } => repo.as_ref(),
|
||||||
View { id: issue, .. }
|
View { id: issue, .. }
|
||||||
| Edit { issue, .. }
|
| Edit { issue, .. }
|
||||||
| Close { issue, .. }
|
| Close { issue, .. }
|
||||||
| Comment { issue, .. }
|
| Comment { issue, .. }
|
||||||
| Browse { id: issue, .. } => issue.repo.as_deref(),
|
| Browse { id: issue, .. } => issue.repo.as_ref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
src/prs.rs
12
src/prs.rs
|
@ -12,7 +12,7 @@ use forgejo_api::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
issues::IssueId,
|
issues::IssueId,
|
||||||
repo::{RepoInfo, RepoName},
|
repo::{RepoArg, RepoInfo, RepoName},
|
||||||
SpecialRender,
|
SpecialRender,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ pub enum PrSubcommand {
|
||||||
state: Option<crate::issues::State>,
|
state: Option<crate::issues::State>,
|
||||||
/// The repo to search in
|
/// The repo to search in
|
||||||
#[clap(long, short)]
|
#[clap(long, short)]
|
||||||
repo: Option<String>,
|
repo: Option<RepoArg>,
|
||||||
},
|
},
|
||||||
/// Create a new pull request
|
/// Create a new pull request
|
||||||
Create {
|
Create {
|
||||||
|
@ -59,7 +59,7 @@ pub enum PrSubcommand {
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
/// The repo to create this issue on
|
/// The repo to create this issue on
|
||||||
#[clap(long, short)]
|
#[clap(long, short)]
|
||||||
repo: Option<String>,
|
repo: Option<RepoArg>,
|
||||||
},
|
},
|
||||||
/// View the contents of a pull request
|
/// View the contents of a pull request
|
||||||
View {
|
View {
|
||||||
|
@ -345,17 +345,17 @@ impl PrCommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repo(&self) -> Option<&str> {
|
fn repo(&self) -> Option<&RepoArg> {
|
||||||
use PrSubcommand::*;
|
use PrSubcommand::*;
|
||||||
match &self.command {
|
match &self.command {
|
||||||
Search { repo, .. } | Create { repo, .. } => repo.as_deref(),
|
Search { repo, .. } | Create { repo, .. } => repo.as_ref(),
|
||||||
Checkout { .. } => None,
|
Checkout { .. } => None,
|
||||||
View { id: pr, .. }
|
View { id: pr, .. }
|
||||||
| Comment { pr, .. }
|
| Comment { pr, .. }
|
||||||
| Edit { pr, .. }
|
| Edit { pr, .. }
|
||||||
| Close { pr, .. }
|
| Close { pr, .. }
|
||||||
| Merge { pr, .. }
|
| Merge { pr, .. }
|
||||||
| Browse { id: pr } => pr.as_ref().and_then(|x| x.repo.as_deref()),
|
| Browse { id: pr } => pr.as_ref().and_then(|x| x.repo.as_ref()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
keys::KeyInfo,
|
keys::KeyInfo,
|
||||||
repo::{RepoInfo, RepoName},
|
repo::{RepoArg, RepoInfo, RepoName},
|
||||||
SpecialRender,
|
SpecialRender,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ pub struct ReleaseCommand {
|
||||||
#[clap(long, short = 'R')]
|
#[clap(long, short = 'R')]
|
||||||
remote: Option<String>,
|
remote: Option<String>,
|
||||||
#[clap(long, short)]
|
#[clap(long, short)]
|
||||||
repo: Option<String>,
|
repo: Option<RepoArg>,
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
command: ReleaseSubcommand,
|
command: ReleaseSubcommand,
|
||||||
}
|
}
|
||||||
|
@ -117,8 +117,7 @@ pub enum AssetCommand {
|
||||||
|
|
||||||
impl ReleaseCommand {
|
impl ReleaseCommand {
|
||||||
pub async fn run(self, keys: &mut KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> {
|
pub async fn run(self, keys: &mut KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> {
|
||||||
let repo =
|
let repo = RepoInfo::get_current(remote_name, self.repo.as_ref(), self.remote.as_deref())?;
|
||||||
RepoInfo::get_current(remote_name, self.repo.as_deref(), self.remote.as_deref())?;
|
|
||||||
let api = keys.get_api(&repo.host_url()).await?;
|
let api = keys.get_api(&repo.host_url()).await?;
|
||||||
let repo = repo
|
let repo = repo
|
||||||
.name()
|
.name()
|
||||||
|
|
149
src/repo.rs
149
src/repo.rs
|
@ -1,4 +1,4 @@
|
||||||
use std::{io::Write, path::PathBuf};
|
use std::{io::Write, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use eyre::{eyre, OptionExt};
|
use eyre::{eyre, OptionExt};
|
||||||
|
@ -15,7 +15,7 @@ pub struct RepoInfo {
|
||||||
impl RepoInfo {
|
impl RepoInfo {
|
||||||
pub fn get_current(
|
pub fn get_current(
|
||||||
host: Option<&str>,
|
host: Option<&str>,
|
||||||
repo: Option<&str>,
|
repo: Option<&RepoArg>,
|
||||||
remote: Option<&str>,
|
remote: Option<&str>,
|
||||||
) -> eyre::Result<Self> {
|
) -> eyre::Result<Self> {
|
||||||
// l = domain/owner/name
|
// l = domain/owner/name
|
||||||
|
@ -48,29 +48,17 @@ impl RepoInfo {
|
||||||
let mut repo_name: Option<RepoName> = None;
|
let mut repo_name: Option<RepoName> = None;
|
||||||
|
|
||||||
if let Some(repo) = repo {
|
if let Some(repo) = repo {
|
||||||
let (head, name) = repo
|
if let Some(host) = &repo.host {
|
||||||
.rsplit_once("/")
|
if let Ok(url) = Url::parse(host) {
|
||||||
.ok_or_eyre("repo name must contain owner and name")?;
|
repo_url = Some(url);
|
||||||
let name = name.strip_suffix(".git").unwrap_or(name);
|
} else if let Ok(url) = Url::parse(&format!("https://{host}/")) {
|
||||||
match head.rsplit_once("/") {
|
repo_url = Some(url);
|
||||||
Some((url, owner)) => {
|
|
||||||
if let Ok(url) = Url::parse(url) {
|
|
||||||
repo_url = Some(url);
|
|
||||||
} else if let Ok(url) = Url::parse(&format!("https://{url}/")) {
|
|
||||||
repo_url = Some(url);
|
|
||||||
}
|
|
||||||
repo_name = Some(RepoName {
|
|
||||||
owner: owner.to_owned(),
|
|
||||||
name: name.to_owned(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
repo_name = Some(RepoName {
|
|
||||||
owner: head.to_owned(),
|
|
||||||
name: name.to_owned(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
repo_name = Some(RepoName {
|
||||||
|
owner: repo.owner.clone(),
|
||||||
|
name: repo.name.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let repo_url = repo_url;
|
let repo_url = repo_url;
|
||||||
|
@ -241,10 +229,61 @@ impl RepoName {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RepoArg {
|
||||||
|
host: Option<String>,
|
||||||
|
owner: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RepoArg {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match &self.host {
|
||||||
|
Some(host) => write!(f, "{host}/{}/{}", self.owner, self.name),
|
||||||
|
None => write!(f, "{}/{}", self.owner, self.name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for RepoArg {
|
||||||
|
type Err = RepoArgError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (head, name) = s.rsplit_once("/").ok_or(RepoArgError::NoOwner)?;
|
||||||
|
let name = name.strip_suffix(".git").unwrap_or(name);
|
||||||
|
let (host, owner) = match head.rsplit_once("/") {
|
||||||
|
Some((host, owner)) => (Some(host), owner),
|
||||||
|
None => (None, head),
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
host: host.map(|s| s.to_owned()),
|
||||||
|
owner: owner.to_owned(),
|
||||||
|
name: name.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum RepoArgError {
|
||||||
|
NoOwner,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for RepoArgError {}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RepoArgError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
RepoArgError::NoOwner => {
|
||||||
|
write!(f, "repo name should be in the format [HOST/]OWNER/NAME")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
pub enum RepoCommand {
|
pub enum RepoCommand {
|
||||||
Create {
|
Create {
|
||||||
repo: String,
|
repo: RepoArg,
|
||||||
|
|
||||||
// flags
|
// flags
|
||||||
#[clap(long, short)]
|
#[clap(long, short)]
|
||||||
|
@ -259,23 +298,30 @@ pub enum RepoCommand {
|
||||||
#[clap(long, short)]
|
#[clap(long, short)]
|
||||||
push: bool,
|
push: bool,
|
||||||
},
|
},
|
||||||
View {
|
Fork {
|
||||||
|
repo: RepoArg,
|
||||||
|
#[clap(long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[clap(long, short = 'R')]
|
#[clap(long, short = 'R')]
|
||||||
remote: Option<String>,
|
remote: Option<String>,
|
||||||
},
|
},
|
||||||
|
View {
|
||||||
|
name: Option<RepoArg>,
|
||||||
|
#[clap(long, short = 'R')]
|
||||||
|
remote: Option<String>,
|
||||||
|
},
|
||||||
Clone {
|
Clone {
|
||||||
repo: String,
|
repo: RepoArg,
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
Star {
|
Star {
|
||||||
repo: String,
|
repo: RepoArg,
|
||||||
},
|
},
|
||||||
Unstar {
|
Unstar {
|
||||||
repo: String,
|
repo: RepoArg,
|
||||||
},
|
},
|
||||||
Browse {
|
Browse {
|
||||||
name: Option<String>,
|
name: Option<RepoArg>,
|
||||||
#[clap(long, short = 'R')]
|
#[clap(long, short = 'R')]
|
||||||
remote: Option<String>,
|
remote: Option<String>,
|
||||||
},
|
},
|
||||||
|
@ -309,7 +355,7 @@ impl RepoCommand {
|
||||||
gitignores: None,
|
gitignores: None,
|
||||||
issue_labels: None,
|
issue_labels: None,
|
||||||
license: None,
|
license: None,
|
||||||
name: repo.clone(),
|
name: format!("{}/{}", repo.owner, repo.name),
|
||||||
object_format_name: None,
|
object_format_name: None,
|
||||||
private: Some(private),
|
private: Some(private),
|
||||||
readme: Some(String::new()),
|
readme: Some(String::new()),
|
||||||
|
@ -355,8 +401,47 @@ impl RepoCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RepoCommand::Fork { repo, name, remote } => {
|
||||||
|
match (repo.host.as_deref(), host_name) {
|
||||||
|
(Some(a), Some(b)) => {
|
||||||
|
fn strip(s: &str) -> &str {
|
||||||
|
let no_scheme = s
|
||||||
|
.strip_prefix("https://")
|
||||||
|
.or_else(|| s.strip_prefix("http://"))
|
||||||
|
.unwrap_or(s);
|
||||||
|
let no_trailing_slash =
|
||||||
|
no_scheme.strip_suffix("/").unwrap_or(no_scheme);
|
||||||
|
no_trailing_slash
|
||||||
|
}
|
||||||
|
if strip(a) != strip(b) {
|
||||||
|
eyre::bail!("conflicting hosts {a} and {b}. please only specify one");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
let repo_info = RepoInfo::get_current(host_name, Some(&repo), remote.as_deref())?;
|
||||||
|
let api = keys.get_api(&repo_info.host_url()).await?;
|
||||||
|
let repo = repo_info
|
||||||
|
.name()
|
||||||
|
.ok_or_eyre("couldn't get repo name, please specify")?;
|
||||||
|
let opt = forgejo_api::structs::CreateForkOption {
|
||||||
|
name,
|
||||||
|
organization: None,
|
||||||
|
};
|
||||||
|
let new_fork = api.create_fork(repo.owner(), repo.name(), opt).await?;
|
||||||
|
let fork_full_name = new_fork
|
||||||
|
.full_name
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_eyre("fork does not have name")?;
|
||||||
|
println!(
|
||||||
|
"Forked {}/{} into {}",
|
||||||
|
repo.owner(),
|
||||||
|
repo.name(),
|
||||||
|
fork_full_name
|
||||||
|
);
|
||||||
|
}
|
||||||
RepoCommand::View { name, remote } => {
|
RepoCommand::View { name, remote } => {
|
||||||
let repo = RepoInfo::get_current(host_name, name.as_deref(), remote.as_deref())?;
|
let repo = RepoInfo::get_current(host_name, name.as_ref(), remote.as_deref())?;
|
||||||
let api = keys.get_api(&repo.host_url()).await?;
|
let api = keys.get_api(&repo.host_url()).await?;
|
||||||
let repo = repo
|
let repo = repo
|
||||||
.name()
|
.name()
|
||||||
|
@ -573,7 +658,7 @@ impl RepoCommand {
|
||||||
println!("Removed star from {}/{}", name.owner(), name.name());
|
println!("Removed star from {}/{}", name.owner(), name.name());
|
||||||
}
|
}
|
||||||
RepoCommand::Browse { name, remote } => {
|
RepoCommand::Browse { name, remote } => {
|
||||||
let repo = RepoInfo::get_current(host_name, name.as_deref(), remote.as_deref())?;
|
let repo = RepoInfo::get_current(host_name, name.as_ref(), remote.as_deref())?;
|
||||||
let mut url = repo.host_url().clone();
|
let mut url = repo.host_url().clone();
|
||||||
let repo = repo
|
let repo = repo
|
||||||
.name()
|
.name()
|
||||||
|
|
Loading…
Reference in a new issue