summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCyborus <cyborus@noreply.codeberg.org>2024-07-09 01:56:38 +0200
committerCyborus <cyborus@noreply.codeberg.org>2024-07-09 01:56:38 +0200
commit88d5356a4449ea1357f141f871e2481f36c902b7 (patch)
treee3e428591ce98dc3a2f70a7c3a81128ec8be5a01
parentMerge pull request 'add user commands' (#86) from user-commands into main (diff)
parentfeat(repo): fork command (diff)
downloadforgejo-cli-88d5356a4449ea1357f141f871e2481f36c902b7.tar.xz
forgejo-cli-88d5356a4449ea1357f141f871e2481f36c902b7.zip
Merge pull request '`repo fork` command' (#83) from fork into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-cli/pulls/83
-rw-r--r--src/issues.rs47
-rw-r--r--src/prs.rs12
-rw-r--r--src/release.rs7
-rw-r--r--src/repo.rs149
4 files changed, 164 insertions, 51 deletions
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<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(),
}
}
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<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()),
}
}
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<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()
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<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()