diff options
-rw-r--r-- | Cargo.lock | 68 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | forgejo-api/src/lib.rs | 73 | ||||
-rw-r--r-- | src/main.rs | 136 |
4 files changed, 180 insertions, 98 deletions
@@ -122,6 +122,9 @@ name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -271,6 +274,7 @@ dependencies = [ "eyre", "forgejo-api", "futures", + "git2", "open", "serde", "serde_json", @@ -429,6 +433,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] +name = "git2" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] name = "h2" version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -625,6 +644,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" [[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] name = "js-sys" version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -646,6 +674,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] +name = "libgit2-sys" +version = "0.15.2+1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a80df2e11fb4a61f4ba2ab42dbe7f74468da143f1a75c74e11dee7c813f694fa" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -11,6 +11,7 @@ directories = "5.0.1" eyre = "0.6.8" forgejo-api = { path = "./forgejo-api" } futures = "0.3.28" +git2 = "0.17.2" open = "5.0.0" serde = { version = "1.0.170", features = ["derive"] } serde_json = "1.0.100" diff --git a/forgejo-api/src/lib.rs b/forgejo-api/src/lib.rs index 0eb1940..d5bd6a0 100644 --- a/forgejo-api/src/lib.rs +++ b/forgejo-api/src/lib.rs @@ -1,7 +1,7 @@ +use reqwest::{Client, Request, StatusCode}; use serde::{de::DeserializeOwned, Serialize}; -use url::Url; use soft_assert::*; -use reqwest::{Client, StatusCode, Request}; +use url::Url; pub struct Forgejo { url: Url, @@ -23,7 +23,7 @@ pub enum ForgejoError { #[error("unexpected status code {} {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""))] UnexpectedStatusCode(StatusCode), #[error("{} {}: {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1)] - ApiError(StatusCode, String) + ApiError(StatusCode, String), } impl From<reqwest::Error> for ForgejoError { @@ -37,23 +37,32 @@ impl From<reqwest::Error> for ForgejoError { } impl Forgejo { - pub fn new(api_key: &str, url: Url) -> Result<Self, ForgejoError> { + pub fn new(api_key: &str, url: Url) -> Result<Self, ForgejoError> { Self::with_user_agent(api_key, url, "forgejo-api-rs") } - pub fn with_user_agent(api_key: &str, url: Url, user_agent: &str) -> Result<Self, ForgejoError> { - soft_assert!(matches!(url.scheme(), "http" | "https"), Err(ForgejoError::HttpRequired)); + pub fn with_user_agent( + api_key: &str, + url: Url, + user_agent: &str, + ) -> Result<Self, ForgejoError> { + soft_assert!( + matches!(url.scheme(), "http" | "https"), + Err(ForgejoError::HttpRequired) + ); let mut headers = reqwest::header::HeaderMap::new(); - let mut key_header: reqwest::header::HeaderValue = format!("token {api_key}").try_into().map_err(|_| ForgejoError::KeyNotAscii)?; + let mut key_header: reqwest::header::HeaderValue = format!("token {api_key}") + .try_into() + .map_err(|_| ForgejoError::KeyNotAscii)?; // key_header.set_sensitive(true); headers.insert("Authorization", key_header); - let client = Client::builder().user_agent(user_agent).default_headers(headers).build()?; + let client = Client::builder() + .user_agent(user_agent) + .default_headers(headers) + .build()?; dbg!(&client); - Ok(Self { - url, - client, - }) + Ok(Self { url, client }) } pub async fn get_repo(&self, user: &str, repo: &str) -> Result<Option<Repo>, ForgejoError> { @@ -93,29 +102,42 @@ impl Forgejo { self.execute_opt(request).await } - async fn post<T: Serialize, U: DeserializeOwned>(&self, path: &str, body: &T) -> Result<U, ForgejoError> { + async fn post<T: Serialize, U: DeserializeOwned>( + &self, + path: &str, + body: &T, + ) -> Result<U, ForgejoError> { let url = self.url.join("api/v1/").unwrap().join(path).unwrap(); let request = self.client.post(url).json(body).build()?; self.execute(request).await - } + } async fn execute<T: DeserializeOwned>(&self, request: Request) -> Result<T, ForgejoError> { let response = self.client.execute(dbg!(request)).await?; match response.status() { status if status.is_success() => Ok(response.json::<T>().await?), - status if status.is_client_error() => Err(ForgejoError::ApiError(status, response.json::<ErrorMessage>().await?.message)), - status => Err(ForgejoError::UnexpectedStatusCode(status)) + status if status.is_client_error() => Err(ForgejoError::ApiError( + status, + response.json::<ErrorMessage>().await?.message, + )), + status => Err(ForgejoError::UnexpectedStatusCode(status)), } } /// Like `execute`, but returns `Ok(None)` on 404. - async fn execute_opt<T: DeserializeOwned>(&self, request: Request) -> Result<Option<T>, ForgejoError> { + async fn execute_opt<T: DeserializeOwned>( + &self, + request: Request, + ) -> Result<Option<T>, ForgejoError> { let response = self.client.execute(dbg!(request)).await?; match response.status() { status if status.is_success() => Ok(Some(response.json::<T>().await?)), StatusCode::NOT_FOUND => Ok(None), - status if status.is_client_error() => Err(ForgejoError::ApiError(status, response.json::<ErrorMessage>().await?.message)), - status => Err(ForgejoError::UnexpectedStatusCode(status)) + status if status.is_client_error() => Err(ForgejoError::ApiError( + status, + response.json::<ErrorMessage>().await?.message, + )), + status => Err(ForgejoError::UnexpectedStatusCode(status)), } } } @@ -124,14 +146,13 @@ impl Forgejo { struct ErrorMessage { message: String, // intentionally ignored, no need for now - // url: Url + // url: Url } - #[derive(serde::Deserialize, Debug, PartialEq)] pub struct Repo { pub clone_url: Url, - #[serde(with="time::serde::rfc3339")] + #[serde(with = "time::serde::rfc3339")] pub created_at: time::OffsetDateTime, pub default_branch: String, pub description: String, @@ -146,7 +167,7 @@ pub struct Repo { pub struct User { pub active: bool, pub avatar_url: Url, - #[serde(with="time::serde::rfc3339")] + #[serde(with = "time::serde::rfc3339")] pub created: time::OffsetDateTime, pub description: String, pub email: String, @@ -156,7 +177,7 @@ pub struct User { pub id: u64, pub is_admin: bool, pub language: String, - #[serde(with="time::serde::rfc3339")] + #[serde(with = "time::serde::rfc3339")] pub last_login: time::OffsetDateTime, pub location: String, pub login: String, @@ -189,7 +210,7 @@ pub struct CreateRepoOption { pub private: bool, pub readme: String, pub template: bool, - pub trust_model: TrustModel + pub trust_model: TrustModel, } #[derive(serde::Serialize, Debug, PartialEq)] @@ -199,4 +220,4 @@ pub enum TrustModel { Committer, #[serde(rename = "collaboratorcommiter")] CollaboratorCommitter, -}
\ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index d6ff42c..24dca9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,8 +26,8 @@ pub enum Command { #[derive(Subcommand, Clone, Debug)] pub enum RepoCommand { - Create { - host: String, + Create { + host: String, repo: String, // flags @@ -41,7 +41,7 @@ pub enum RepoCommand { /// Pushes the current branch to the default branch on the new repo. /// Implies `--set-upstream=origin` (setting upstream manual overrides this) #[clap(long, short)] - push: bool + push: bool, }, Info, Browse, @@ -81,9 +81,9 @@ async fn main() -> eyre::Result<()> { match args.command { Command::Repo(repo_subcommand) => match repo_subcommand { - RepoCommand::Create { - host, - repo , + RepoCommand::Create { + host, + repo, description, private, @@ -91,10 +91,12 @@ async fn main() -> eyre::Result<()> { push, } => { // let (host_domain, host_keys, repo) = keys.get_current_host_and_repo().await?; - let host_info = keys.hosts.get(&host).ok_or_else(|| eyre!("not a known host"))?; + let host_info = keys + .hosts + .get(&host) + .ok_or_else(|| eyre!("not a known host"))?; let (_, user) = host_info.get_current_user()?; - let url = Url::parse(&format!("http://{host}/"))?; - let api = Forgejo::new(&user.key, url.clone())?; + let api = Forgejo::new(&user.key, host_info.url.clone())?; let repo_spec = CreateRepoOption { auto_init: false, default_branch: "main".into(), @@ -109,41 +111,28 @@ async fn main() -> eyre::Result<()> { trust_model: forgejo_api::TrustModel::Default, }; let new_repo = api.create_repo(repo_spec).await?; - eprintln!("created new repo at {}", url.join(&format!("{}/{}", user.name, repo))?); + eprintln!( + "created new repo at {}", + host_info.url.join(&format!("{}/{}", user.name, repo))? + ); let upstream = set_upstream.as_deref().unwrap_or("origin"); - if set_upstream.is_some() || push { - let status = tokio::process::Command::new("git") - .arg("remote") - .arg("add") - .arg(upstream) - .arg(new_repo.clone_url.as_str()) - .status() - .await?; - if !status.success() { - eprintln!("origin set failed"); - } - } + let repo = git2::Repository::open(".")?; + let remote = if set_upstream.is_some() || push { + repo.remote(upstream, new_repo.clone_url.as_str())?; + } else { + repo.find_remote(upstream)?; + }; if push { - let status = tokio::process::Command::new("git") - .arg("push") - .arg("-u") - .arg(upstream) - .arg("main") - .status() - .await?; - if !status.success() { - eprintln!("push failed"); - } + remote.push(upstream)?; } } RepoCommand::Info => { - let (host_domain, host_keys, repo) = keys.get_current_host_and_repo().await?; + let (_, host_keys, repo) = keys.get_current_host_and_repo().await?; let (_, user) = host_keys.get_current_user()?; - let url = Url::parse(&format!("http://{host_domain}/"))?; - let api = Forgejo::new(&user.key, url)?; + let api = Forgejo::new(&user.key, host_keys.url.clone())?; let repo = api.get_repo(&user.name, &repo).await?; match repo { Some(repo) => { @@ -153,19 +142,27 @@ async fn main() -> eyre::Result<()> { } } RepoCommand::Browse => { - let (host_domain, host_keys, repo) = keys.get_current_host_and_repo().await?; + let (_, host_keys, repo) = keys.get_current_host_and_repo().await?; let (_, user) = host_keys.get_current_user()?; - open::that(format!("http://{host_domain}/{}/{repo}", user.name))?; + open::that( + host_keys + .url + .join(&format!("/{}/{repo}", user.name))? + .as_str(), + )?; } }, Command::User { host } => { - let (host_domain, host_keys) = match host.as_deref() { - Some(s) => (s, keys.hosts.get(s).ok_or_else(|| eyre!("not a known host"))?), + let (_, host_keys) = match host.as_deref() { + Some(s) => ( + s, + keys.hosts.get(s).ok_or_else(|| eyre!("not a known host"))?, + ), None => keys.get_current_host().await?, }; let (_, info) = host_keys.get_current_user()?; - eprintln!("currently signed in to {}@{}", info.name, host_domain); - }, + eprintln!("currently signed in to {}@{}", info.name, host_keys.url); + } Command::Auth(auth_subcommand) => match auth_subcommand { AuthCommand::Login => { todo!(); @@ -206,7 +203,10 @@ async fn main() -> eyre::Result<()> { name, key, } => { - let host_keys = keys.hosts.entry(host.clone()).or_default(); + let host_keys = keys + .hosts + .get_mut(&host) + .ok_or_else(|| eyre!("unknown host {host}"))?; let key = match key { Some(key) => key, None => readline("new key: ").await?, @@ -254,30 +254,16 @@ async fn readline(msg: &str) -> eyre::Result<String> { } async fn get_remotes() -> eyre::Result<Vec<(String, Url)>> { - let remotes = String::from_utf8( - tokio::process::Command::new("git") - .arg("remote") - .output() - .await? - .stdout, - )?; - let remotes = futures::future::try_join_all(remotes.lines().map(|name| async { - let name = name.trim(); - let url = Url::parse( - String::from_utf8( - tokio::process::Command::new("git") - .arg("remote") - .arg("get-url") - .arg(name) - .output() - .await? - .stdout, - )? - .trim(), - )?; - Ok::<_, eyre::Report>((name.to_string(), url)) - })) - .await?; + let repo = git2::Repository::open(".")?; + let remotes = repo + .remotes()? + .iter() + .filter_map(|name| { + let name = name?.to_string(); + let url = Url::parse(repo.find_remote(&name).ok()?.url()?).ok()?; + Some((name, url)) + }) + .collect::<Vec<_>>(); Ok(remotes) } @@ -295,9 +281,14 @@ async fn get_remote(remotes: &[(String, Url)]) -> eyre::Result<Url> { #[derive(serde::Serialize, serde::Deserialize, Clone, Default)] struct KeyInfo { hosts: BTreeMap<String, HostInfo>, + domain_to_name: BTreeMap<String, String>, } impl KeyInfo { + fn domain_to_name(&self, domain: &str) -> Option<&str> { + self.domain_to_name.get(domain).map(|s| &**s) + } + async fn load() -> eyre::Result<Self> { let path = directories::ProjectDirs::from("", "Cyborus", "forgejo-cli") .ok_or_else(|| eyre!("Could not find data directory"))? @@ -342,10 +333,13 @@ impl KeyInfo { } else { host_str.to_owned() }; + let name = self + .domain_to_name(&domain) + .ok_or_else(|| eyre!("unknown remote"))?; let (name, host) = self .hosts - .get_key_value(&domain) + .get_key_value(name) .ok_or_else(|| eyre!("not signed in to {domain}"))?; Ok((name, host, repo_from_url(&remote)?.into())) } @@ -381,9 +375,10 @@ fn repo_from_url(url: &Url) -> eyre::Result<&str> { Ok(repo) } -#[derive(serde::Serialize, serde::Deserialize, Clone, Default)] +#[derive(serde::Serialize, serde::Deserialize, Clone)] struct HostInfo { default: Option<String>, + url: Url, users: BTreeMap<String, UserInfo>, } @@ -393,10 +388,7 @@ impl HostInfo { let (s, k) = self.users.first_key_value().unwrap(); return Ok((s, k)); } - if let Some(default) = self - .default - .as_ref() - { + if let Some(default) = self.default.as_ref() { if let Some(default_info) = self.users.get(default) { return Ok((default, default_info)); } |