mirror of
https://codeberg.org/Cyborus/forgejo-cli.git
synced 2024-11-27 04:03:48 +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 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<String>,
|
||||
#[clap(long, short)]
|
||||
repo: Option<String>,
|
||||
repo: Option<RepoArg>,
|
||||
},
|
||||
Edit {
|
||||
issue: IssueId,
|
||||
|
@ -42,7 +42,7 @@ pub enum IssueSubcommand {
|
|||
},
|
||||
Search {
|
||||
#[clap(long, short)]
|
||||
repo: Option<String>,
|
||||
repo: Option<RepoArg>,
|
||||
query: Option<String>,
|
||||
#[clap(long, short)]
|
||||
labels: Option<String>,
|
||||
|
@ -65,16 +65,16 @@ pub enum IssueSubcommand {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IssueId {
|
||||
pub repo: Option<String>,
|
||||
pub repo: Option<RepoArg>,
|
||||
pub number: u64,
|
||||
}
|
||||
|
||||
impl FromStr for IssueId {
|
||||
type Err = std::num::ParseIntError;
|
||||
type Err = IssueIdError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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),
|
||||
};
|
||||
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)]
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
12
src/prs.rs
12
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<crate::issues::State>,
|
||||
/// The repo to search in
|
||||
#[clap(long, short)]
|
||||
repo: Option<String>,
|
||||
repo: Option<RepoArg>,
|
||||
},
|
||||
/// Create a new pull request
|
||||
Create {
|
||||
|
@ -59,7 +59,7 @@ pub enum PrSubcommand {
|
|||
body: Option<String>,
|
||||
/// The repo to create this issue on
|
||||
#[clap(long, short)]
|
||||
repo: Option<String>,
|
||||
repo: Option<RepoArg>,
|
||||
},
|
||||
/// 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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
#[clap(long, short)]
|
||||
repo: Option<String>,
|
||||
repo: Option<RepoArg>,
|
||||
#[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()
|
||||
|
|
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 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<Self> {
|
||||
// l = domain/owner/name
|
||||
|
@ -48,29 +48,17 @@ impl RepoInfo {
|
|||
let mut repo_name: Option<RepoName> = 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<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)]
|
||||
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<String>,
|
||||
#[clap(long, short = 'R')]
|
||||
remote: Option<String>,
|
||||
},
|
||||
View {
|
||||
name: Option<RepoArg>,
|
||||
#[clap(long, short = 'R')]
|
||||
remote: Option<String>,
|
||||
},
|
||||
Clone {
|
||||
repo: String,
|
||||
repo: RepoArg,
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
Star {
|
||||
repo: String,
|
||||
repo: RepoArg,
|
||||
},
|
||||
Unstar {
|
||||
repo: String,
|
||||
repo: RepoArg,
|
||||
},
|
||||
Browse {
|
||||
name: Option<String>,
|
||||
name: Option<RepoArg>,
|
||||
#[clap(long, short = 'R')]
|
||||
remote: Option<String>,
|
||||
},
|
||||
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue