summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock68
-rw-r--r--Cargo.toml1
-rw-r--r--forgejo-api/src/lib.rs73
-rw-r--r--src/main.rs136
4 files changed, 180 insertions, 98 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4ea41ee..cc8b9fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index c430c0c..291646c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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));
}