diff --git a/src/issues.rs b/src/issues.rs index 92f0e06..d428d0e 100644 --- a/src/issues.rs +++ b/src/issues.rs @@ -7,7 +7,7 @@ use forgejo_api::structs::{ }; use forgejo_api::Forgejo; -use crate::repo::{RepoInfo, RepoName}; +use crate::repo::{RepoArg, RepoInfo, RepoName}; #[derive(Args, Clone, Debug)] pub struct IssueCommand { @@ -24,7 +24,7 @@ pub enum IssueSubcommand { #[clap(long)] body: Option, #[clap(long, short)] - repo: Option, + repo: Option, }, Edit { issue: IssueId, @@ -42,7 +42,7 @@ pub enum IssueSubcommand { }, Search { #[clap(long, short)] - repo: Option, + repo: Option, query: Option, #[clap(long, short)] labels: Option, @@ -65,16 +65,16 @@ pub enum IssueSubcommand { #[derive(Clone, Debug)] pub struct IssueId { - pub repo: Option, + pub repo: Option, pub number: u64, } impl FromStr for IssueId { - type Err = std::num::ParseIntError; + type Err = IssueIdError; fn from_str(s: &str) -> Result { let (repo, number) = match s.rsplit_once("#") { - Some((repo, number)) => (Some(repo.to_owned()), number), + Some((repo, number)) => (Some(repo.parse::()?), number), None => (None, s), }; 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 for IssueIdError { + fn from(value: crate::repo::RepoArgError) -> Self { + Self::Repo(value) + } +} + +impl From for IssueIdError { + fn from(value: std::num::ParseIntError) -> Self { + Self::Number(value) + } +} + +impl std::error::Error for IssueIdError {} + #[derive(clap::ValueEnum, Clone, Copy, Debug)] pub enum State { Open, @@ -163,15 +192,15 @@ impl IssueCommand { Ok(()) } - fn repo(&self) -> Option<&str> { + fn repo(&self) -> Option<&RepoArg> { use IssueSubcommand::*; match &self.command { - Create { repo, .. } | Search { repo, .. } => repo.as_deref(), + Create { repo, .. } | Search { repo, .. } => repo.as_ref(), View { id: issue, .. } | Edit { issue, .. } | Close { issue, .. } | Comment { issue, .. } - | Browse { id: issue, .. } => issue.repo.as_deref(), + | Browse { id: issue, .. } => issue.repo.as_ref(), } } diff --git a/src/prs.rs b/src/prs.rs index 6ad6cad..0a46296 100644 --- a/src/prs.rs +++ b/src/prs.rs @@ -12,7 +12,7 @@ use forgejo_api::{ use crate::{ issues::IssueId, - repo::{RepoInfo, RepoName}, + repo::{RepoArg, RepoInfo, RepoName}, SpecialRender, }; @@ -40,7 +40,7 @@ pub enum PrSubcommand { state: Option, /// The repo to search in #[clap(long, short)] - repo: Option, + repo: Option, }, /// Create a new pull request Create { @@ -59,7 +59,7 @@ pub enum PrSubcommand { body: Option, /// The repo to create this issue on #[clap(long, short)] - repo: Option, + repo: Option, }, /// View the contents of a pull request View { @@ -345,17 +345,17 @@ impl PrCommand { Ok(()) } - fn repo(&self) -> Option<&str> { + fn repo(&self) -> Option<&RepoArg> { use PrSubcommand::*; match &self.command { - Search { repo, .. } | Create { repo, .. } => repo.as_deref(), + Search { repo, .. } | Create { repo, .. } => repo.as_ref(), Checkout { .. } => None, View { id: pr, .. } | Comment { pr, .. } | Edit { pr, .. } | Close { 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()), } } diff --git a/src/release.rs b/src/release.rs index 4fd85ed..2c9d065 100644 --- a/src/release.rs +++ b/src/release.rs @@ -8,7 +8,7 @@ use tokio::io::AsyncWriteExt; use crate::{ keys::KeyInfo, - repo::{RepoInfo, RepoName}, + repo::{RepoArg, RepoInfo, RepoName}, SpecialRender, }; @@ -17,7 +17,7 @@ pub struct ReleaseCommand { #[clap(long, short = 'R')] remote: Option, #[clap(long, short)] - repo: Option, + repo: Option, #[clap(subcommand)] command: ReleaseSubcommand, } @@ -117,8 +117,7 @@ pub enum AssetCommand { impl ReleaseCommand { pub async fn run(self, keys: &mut KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> { - let repo = - RepoInfo::get_current(remote_name, self.repo.as_deref(), self.remote.as_deref())?; + let repo = RepoInfo::get_current(remote_name, self.repo.as_ref(), self.remote.as_deref())?; let api = keys.get_api(&repo.host_url()).await?; let repo = repo .name() diff --git a/src/repo.rs b/src/repo.rs index f0c6a90..0e12f01 100644 --- a/src/repo.rs +++ b/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 eyre::{eyre, OptionExt}; @@ -15,7 +15,7 @@ pub struct RepoInfo { impl RepoInfo { pub fn get_current( host: Option<&str>, - repo: Option<&str>, + repo: Option<&RepoArg>, remote: Option<&str>, ) -> eyre::Result { // l = domain/owner/name @@ -48,29 +48,17 @@ impl RepoInfo { let mut repo_name: Option = None; if let Some(repo) = repo { - let (head, name) = repo - .rsplit_once("/") - .ok_or_eyre("repo name must contain owner and name")?; - let name = name.strip_suffix(".git").unwrap_or(name); - match head.rsplit_once("/") { - 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(), - }); + if let Some(host) = &repo.host { + if let Ok(url) = Url::parse(host) { + repo_url = Some(url); + } else if let Ok(url) = Url::parse(&format!("https://{host}/")) { + repo_url = Some(url); } } + repo_name = Some(RepoName { + owner: repo.owner.clone(), + name: repo.name.clone(), + }); } let repo_url = repo_url; @@ -241,10 +229,61 @@ impl RepoName { } } +#[derive(Debug, Clone)] +pub struct RepoArg { + host: Option, + 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 { + 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)] pub enum RepoCommand { Create { - repo: String, + repo: RepoArg, // flags #[clap(long, short)] @@ -259,23 +298,30 @@ pub enum RepoCommand { #[clap(long, short)] push: bool, }, - View { + Fork { + repo: RepoArg, + #[clap(long)] name: Option, #[clap(long, short = 'R')] remote: Option, }, + View { + name: Option, + #[clap(long, short = 'R')] + remote: Option, + }, Clone { - repo: String, + repo: RepoArg, path: Option, }, Star { - repo: String, + repo: RepoArg, }, Unstar { - repo: String, + repo: RepoArg, }, Browse { - name: Option, + name: Option, #[clap(long, short = 'R')] remote: Option, }, @@ -309,7 +355,7 @@ impl RepoCommand { gitignores: None, issue_labels: None, license: None, - name: repo.clone(), + name: format!("{}/{}", repo.owner, repo.name), object_format_name: None, private: Some(private), 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 } => { - 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 repo = repo .name() @@ -573,7 +658,7 @@ impl RepoCommand { println!("Removed star from {}/{}", name.owner(), name.name()); } 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 repo = repo .name()