mirror of
https://codeberg.org/Cyborus/forgejo-cli.git
synced 2024-11-23 10:21:48 +01:00
initial commit
This commit is contained in:
commit
6b2f7628d6
6 changed files with 2116 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
1482
Cargo.lock
generated
Normal file
1482
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "fj"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.3.11", features = ["derive"] }
|
||||||
|
directories = "5.0.1"
|
||||||
|
eyre = "0.6.8"
|
||||||
|
forgejo-api = { path = "./forgejo-api" }
|
||||||
|
futures = "0.3.28"
|
||||||
|
serde = { version = "1.0.170", features = ["derive"] }
|
||||||
|
serde_json = "1.0.100"
|
||||||
|
soft_assert = "0.1.1"
|
||||||
|
tokio = { version = "1.29.1", features = ["full"] }
|
||||||
|
url = "2.4.0"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["forgejo-api"]
|
15
forgejo-api/Cargo.toml
Normal file
15
forgejo-api/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "forgejo-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
reqwest = { version = "0.11.18", features = ["json"] }
|
||||||
|
soft_assert = "0.1.1"
|
||||||
|
thiserror = "1.0.43"
|
||||||
|
tokio = { version = "1.29.1", features = ["net"] }
|
||||||
|
url = { version = "2.4.0", features = ["serde"] }
|
||||||
|
serde = { version = "1.0.168", features = ["derive"] }
|
||||||
|
time = { version = "0.3.22", features = ["parsing", "serde"] }
|
191
forgejo-api/src/lib.rs
Normal file
191
forgejo-api/src/lib.rs
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
use soft_assert::*;
|
||||||
|
use reqwest::{Client, StatusCode, Request};
|
||||||
|
|
||||||
|
pub struct Forgejo {
|
||||||
|
url: Url,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ForgejoError {
|
||||||
|
#[error("url must have a host")]
|
||||||
|
HostRequired,
|
||||||
|
#[error("scheme must be http or https")]
|
||||||
|
HttpRequired,
|
||||||
|
#[error("{0}")] // for some reason, you can't use `source` and `transparent` together
|
||||||
|
ReqwestError(#[source] reqwest::Error),
|
||||||
|
#[error("API key should be ascii")]
|
||||||
|
KeyNotAscii,
|
||||||
|
#[error("the response from forgejo was not properly structured")]
|
||||||
|
BadStructure,
|
||||||
|
#[error("unexpected status code {} {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""))]
|
||||||
|
UnexpectedStatusCode(StatusCode),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for ForgejoError {
|
||||||
|
fn from(e: reqwest::Error) -> Self {
|
||||||
|
if e.is_decode() {
|
||||||
|
ForgejoError::BadStructure
|
||||||
|
} else {
|
||||||
|
ForgejoError::ReqwestError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Forgejo {
|
||||||
|
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));
|
||||||
|
|
||||||
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
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()?;
|
||||||
|
dbg!(&client);
|
||||||
|
Ok(Self {
|
||||||
|
url,
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_repo(&self, user: &str, repo: &str) -> Result<Option<Repo>, ForgejoError> {
|
||||||
|
self.get_opt(&format!("repos/{user}/{repo}/")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_repo(&self, repo: CreateRepoOption) -> Result<Repo, ForgejoError> {
|
||||||
|
self.post("user/repos", &repo).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns user info about the authorized user.
|
||||||
|
pub async fn myself(&self) -> Result<User, ForgejoError> {
|
||||||
|
self.get("user").await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user(&self, user: &str) -> Result<Option<User>, ForgejoError> {
|
||||||
|
self.get_opt(&format!("users/{user}/")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_followers(&self, user: &str) -> Result<Option<Vec<User>>, ForgejoError> {
|
||||||
|
self.get_opt(&format!("users/{user}/followers/")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_following(&self, user: &str) -> Result<Option<Vec<User>>, ForgejoError> {
|
||||||
|
self.get_opt(&format!("users/{user}/following/")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, ForgejoError> {
|
||||||
|
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||||
|
let request = self.client.get(url).build()?;
|
||||||
|
self.execute(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_opt<T: DeserializeOwned>(&self, path: &str) -> Result<Option<T>, ForgejoError> {
|
||||||
|
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||||
|
let request = self.client.get(url).build()?;
|
||||||
|
self.execute_opt(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => 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> {
|
||||||
|
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 => Err(ForgejoError::UnexpectedStatusCode(status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Repo {
|
||||||
|
pub clone_url: Url,
|
||||||
|
#[serde(with="time::serde::rfc3339")]
|
||||||
|
pub created_at: time::OffsetDateTime,
|
||||||
|
pub default_branch: String,
|
||||||
|
pub description: String,
|
||||||
|
pub fork: bool,
|
||||||
|
pub forks_count: u64,
|
||||||
|
pub full_name: String,
|
||||||
|
|
||||||
|
pub owner: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct User {
|
||||||
|
pub active: bool,
|
||||||
|
pub avatar_url: Url,
|
||||||
|
#[serde(with="time::serde::rfc3339")]
|
||||||
|
pub created: time::OffsetDateTime,
|
||||||
|
pub description: String,
|
||||||
|
pub email: String,
|
||||||
|
pub followers_count: u64,
|
||||||
|
pub following_count: u64,
|
||||||
|
pub full_name: String,
|
||||||
|
pub id: u64,
|
||||||
|
pub is_admin: bool,
|
||||||
|
pub language: String,
|
||||||
|
#[serde(with="time::serde::rfc3339")]
|
||||||
|
pub last_login: time::OffsetDateTime,
|
||||||
|
pub location: String,
|
||||||
|
pub login: String,
|
||||||
|
pub login_name: String,
|
||||||
|
pub prohibit_login: bool,
|
||||||
|
pub restricted: bool,
|
||||||
|
pub starred_repos_count: u64,
|
||||||
|
pub website: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
|
pub enum UserVisibility {
|
||||||
|
#[serde(rename = "public")]
|
||||||
|
Public,
|
||||||
|
#[serde(rename = "limited")]
|
||||||
|
Limited,
|
||||||
|
#[serde(rename = "private")]
|
||||||
|
Private,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Debug, PartialEq)]
|
||||||
|
pub struct CreateRepoOption {
|
||||||
|
pub auto_init: bool,
|
||||||
|
pub default_branch: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub gitignores: String,
|
||||||
|
pub issue_labels: String,
|
||||||
|
pub license: String,
|
||||||
|
pub name: String,
|
||||||
|
pub private: bool,
|
||||||
|
pub readme: String,
|
||||||
|
pub template: bool,
|
||||||
|
pub trust_model: TrustModel
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Debug, PartialEq)]
|
||||||
|
pub enum TrustModel {
|
||||||
|
Default,
|
||||||
|
Collaborator,
|
||||||
|
Committer,
|
||||||
|
#[serde(rename = "collaboratorcommiter")]
|
||||||
|
CollaboratorCommitter,
|
||||||
|
}
|
406
src/main.rs
Normal file
406
src/main.rs
Normal file
|
@ -0,0 +1,406 @@
|
||||||
|
use std::{collections::BTreeMap, io::ErrorKind};
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use eyre::{bail, eyre};
|
||||||
|
use forgejo_api::{CreateRepoOption, Forgejo};
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct App {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
|
pub enum Command {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
Repo(RepoCommand),
|
||||||
|
User {
|
||||||
|
#[clap(long, short)]
|
||||||
|
host: Option<String>,
|
||||||
|
},
|
||||||
|
#[clap(subcommand)]
|
||||||
|
Auth(AuthCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
|
pub enum RepoCommand {
|
||||||
|
Create {
|
||||||
|
host: String,
|
||||||
|
repo: String,
|
||||||
|
|
||||||
|
// flags
|
||||||
|
#[clap(long, short)]
|
||||||
|
description: Option<String>,
|
||||||
|
#[clap(long, short)]
|
||||||
|
private: bool,
|
||||||
|
/// Sets the new repo to be the `origin` remote of the current local repo.
|
||||||
|
#[clap(long, short)]
|
||||||
|
set_upstream: bool,
|
||||||
|
/// Pushes the current branch to the default branch on the new repo.
|
||||||
|
/// Implies `--set-upstream`
|
||||||
|
#[clap(long, short)]
|
||||||
|
push: bool
|
||||||
|
},
|
||||||
|
Info,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
|
pub enum AuthCommand {
|
||||||
|
Login,
|
||||||
|
Logout {
|
||||||
|
host: String,
|
||||||
|
user: String,
|
||||||
|
},
|
||||||
|
Switch {
|
||||||
|
/// The host to set the default account for.
|
||||||
|
#[clap(short, long)]
|
||||||
|
host: Option<String>,
|
||||||
|
user: String,
|
||||||
|
},
|
||||||
|
AddKey {
|
||||||
|
/// The domain name of the forgejo instance.
|
||||||
|
host: String,
|
||||||
|
/// The user that the key is associated with
|
||||||
|
user: String,
|
||||||
|
/// The name of the key. If not present, defaults to the username.
|
||||||
|
#[clap(short, long)]
|
||||||
|
name: Option<String>,
|
||||||
|
/// The key to add. If not present, the key will be read in from stdin.
|
||||||
|
key: Option<String>,
|
||||||
|
},
|
||||||
|
List,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> eyre::Result<()> {
|
||||||
|
let args = App::parse();
|
||||||
|
let mut keys = KeyInfo::load().await?;
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Command::Repo(repo_subcommand) => match repo_subcommand {
|
||||||
|
RepoCommand::Create {
|
||||||
|
host,
|
||||||
|
repo ,
|
||||||
|
|
||||||
|
description,
|
||||||
|
private,
|
||||||
|
set_upstream,
|
||||||
|
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 (_, user) = host_info.get_current_user()?;
|
||||||
|
let url = Url::parse(&format!("http://{host}/"))?;
|
||||||
|
let api = Forgejo::new(&user.key, url.clone())?;
|
||||||
|
let repo_spec = CreateRepoOption {
|
||||||
|
auto_init: false,
|
||||||
|
default_branch: "main".into(),
|
||||||
|
description,
|
||||||
|
gitignores: String::new(),
|
||||||
|
issue_labels: String::new(),
|
||||||
|
license: String::new(),
|
||||||
|
name: repo.clone(),
|
||||||
|
private,
|
||||||
|
readme: String::new(),
|
||||||
|
template: false,
|
||||||
|
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))?);
|
||||||
|
|
||||||
|
if set_upstream || push {
|
||||||
|
let status = tokio::process::Command::new("git")
|
||||||
|
.arg("remote")
|
||||||
|
.arg("add")
|
||||||
|
.arg("origin")
|
||||||
|
.arg(new_repo.clone_url.as_str())
|
||||||
|
.status()
|
||||||
|
.await?;
|
||||||
|
if !status.success() {
|
||||||
|
eprintln!("origin set failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if push {
|
||||||
|
let status = tokio::process::Command::new("git")
|
||||||
|
.arg("push")
|
||||||
|
.arg("-u")
|
||||||
|
.arg("origin")
|
||||||
|
.arg("main")
|
||||||
|
.arg(new_repo.clone_url.as_str())
|
||||||
|
.status()
|
||||||
|
.await?;
|
||||||
|
if !status.success() {
|
||||||
|
eprintln!("push failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RepoCommand::Info => {
|
||||||
|
let (host_domain, 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 repo = api.get_repo(&user.name, &repo).await?;
|
||||||
|
match repo {
|
||||||
|
Some(repo) => {
|
||||||
|
dbg!(repo);
|
||||||
|
}
|
||||||
|
None => eprintln!("repo not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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"))?),
|
||||||
|
None => keys.get_current_host().await?,
|
||||||
|
};
|
||||||
|
let (_, info) = host_keys.get_current_user()?;
|
||||||
|
eprintln!("currently signed in to {}@{}", info.name, host_domain);
|
||||||
|
},
|
||||||
|
Command::Auth(auth_subcommand) => match auth_subcommand {
|
||||||
|
AuthCommand::Login => {
|
||||||
|
todo!();
|
||||||
|
// let user = readline("username: ").await?;
|
||||||
|
// let pass = readline("password: ").await?;
|
||||||
|
}
|
||||||
|
AuthCommand::Logout { host, user } => {
|
||||||
|
let was_signed_in = keys
|
||||||
|
.hosts
|
||||||
|
.get_mut(&host)
|
||||||
|
.and_then(|host| host.users.remove(&user))
|
||||||
|
.is_some();
|
||||||
|
if was_signed_in {
|
||||||
|
eprintln!("signed out of {user}@{host}");
|
||||||
|
} else {
|
||||||
|
eprintln!("already not signed in");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AuthCommand::Switch { host, user } => {
|
||||||
|
let host = host.unwrap_or(keys.get_current_host().await?.0.to_string());
|
||||||
|
let host_info = keys
|
||||||
|
.hosts
|
||||||
|
.get_mut(&host)
|
||||||
|
.ok_or_else(|| eyre!("not a known host"))?;
|
||||||
|
if !host_info.users.contains_key(&user) {
|
||||||
|
bail!("could not switch user: not signed into {host} as {user}");
|
||||||
|
}
|
||||||
|
let previous = host_info.default.replace(user.clone());
|
||||||
|
print!("set current user for {host} to {user}");
|
||||||
|
match previous {
|
||||||
|
Some(prev) => println!(" (previously {prev})"),
|
||||||
|
None => println!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AuthCommand::AddKey {
|
||||||
|
host,
|
||||||
|
user,
|
||||||
|
name,
|
||||||
|
key,
|
||||||
|
} => {
|
||||||
|
let host_keys = keys.hosts.entry(host.clone()).or_default();
|
||||||
|
let key = match key {
|
||||||
|
Some(key) => key,
|
||||||
|
None => readline("new key: ").await?,
|
||||||
|
};
|
||||||
|
if host_keys.users.get(&user).is_none() {
|
||||||
|
host_keys.users.insert(
|
||||||
|
name.unwrap_or_else(|| user.clone()),
|
||||||
|
UserInfo { name: user, key },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"key {} for {} already exists (rename it?)",
|
||||||
|
name.unwrap_or(user),
|
||||||
|
host
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AuthCommand::List => {
|
||||||
|
if keys.hosts.is_empty() {
|
||||||
|
println!("No logins.");
|
||||||
|
}
|
||||||
|
for (host_url, host_info) in &keys.hosts {
|
||||||
|
for (key_name, key_info) in &host_info.users {
|
||||||
|
let UserInfo { name, key: _ } = key_info;
|
||||||
|
println!("{key_name}: {name}@{host_url}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.save().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn readline(msg: &str) -> eyre::Result<String> {
|
||||||
|
print!("{msg}");
|
||||||
|
tokio::io::stdout().flush().await?;
|
||||||
|
tokio::task::spawn_blocking(|| {
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input)?;
|
||||||
|
Ok(input)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
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?;
|
||||||
|
Ok(remotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_remote(remotes: &[(String, Url)]) -> eyre::Result<Url> {
|
||||||
|
let url = if remotes.len() == 1 {
|
||||||
|
remotes[0].1.clone()
|
||||||
|
} else if let Some((_, url)) = remotes.iter().find(|(name, _)| *name == "origin") {
|
||||||
|
url.clone()
|
||||||
|
} else {
|
||||||
|
bail!("could not find remote");
|
||||||
|
};
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
|
||||||
|
struct KeyInfo {
|
||||||
|
hosts: BTreeMap<String, HostInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyInfo {
|
||||||
|
async fn load() -> eyre::Result<Self> {
|
||||||
|
let path = directories::ProjectDirs::from("", "Cyborus", "forgejo-cli")
|
||||||
|
.ok_or_else(|| eyre!("Could not find data directory"))?
|
||||||
|
.data_dir()
|
||||||
|
.join("keys.json");
|
||||||
|
let json = tokio::fs::read(path).await;
|
||||||
|
let this = match json {
|
||||||
|
Ok(x) => serde_json::from_slice::<Self>(&x)?,
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||||
|
eprintln!("keys file not found, creating");
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self) -> eyre::Result<()> {
|
||||||
|
let json = serde_json::to_vec_pretty(self)?;
|
||||||
|
let dirs = directories::ProjectDirs::from("", "Cyborus", "forgejo-cli")
|
||||||
|
.ok_or_else(|| eyre!("Could not find data directory"))?;
|
||||||
|
let path = dirs.data_dir();
|
||||||
|
|
||||||
|
tokio::fs::create_dir_all(path).await?;
|
||||||
|
|
||||||
|
tokio::fs::File::create(path.join("keys.json"))
|
||||||
|
.await?
|
||||||
|
.write_all(&json)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_current_host_and_repo(&self) -> eyre::Result<(&str, &HostInfo, String)> {
|
||||||
|
let remotes = get_remotes().await?;
|
||||||
|
let remote = get_remote(&remotes).await?;
|
||||||
|
let host_str = remote
|
||||||
|
.host_str()
|
||||||
|
.ok_or_else(|| eyre!("remote url does not have host"))?;
|
||||||
|
let domain = if let Some(port) = remote.port() {
|
||||||
|
format!("{}:{}", host_str, port)
|
||||||
|
} else {
|
||||||
|
host_str.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (name, host) = self
|
||||||
|
.hosts
|
||||||
|
.get_key_value(&domain)
|
||||||
|
.ok_or_else(|| eyre!("not signed in to {domain}"))?;
|
||||||
|
Ok((name, host, repo_from_url(&remote)?.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_current_host(&self) -> eyre::Result<(&str, &HostInfo)> {
|
||||||
|
let (name, host, _) = self.get_current_host_and_repo().await?;
|
||||||
|
Ok((name, host))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_current_user(&self) -> eyre::Result<(&str, &UserInfo)> {
|
||||||
|
let user = self.get_current_host().await?.1.get_current_user()?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repo_from_url(url: &Url) -> eyre::Result<&str> {
|
||||||
|
let mut iter = url
|
||||||
|
.path_segments()
|
||||||
|
.ok_or_else(|| eyre!("failed to get path from url"))?;
|
||||||
|
soft_assert::soft_assert!(
|
||||||
|
matches!(iter.next(), Some(_)),
|
||||||
|
Err(eyre!("path should have 2 segments, has none"))
|
||||||
|
);
|
||||||
|
let repo = iter
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| eyre!("path should have 2 segments, has only 1"))?;
|
||||||
|
let repo = repo.strip_suffix(".git").unwrap_or(repo);
|
||||||
|
soft_assert::soft_assert!(
|
||||||
|
matches!(iter.next(), None),
|
||||||
|
Err(eyre!("path should have 2 segments, has more"))
|
||||||
|
);
|
||||||
|
Ok(repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
|
||||||
|
struct HostInfo {
|
||||||
|
default: Option<String>,
|
||||||
|
users: BTreeMap<String, UserInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostInfo {
|
||||||
|
fn get_current_user(&self) -> eyre::Result<(&str, &UserInfo)> {
|
||||||
|
if self.users.len() == 1 {
|
||||||
|
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_info) = self.users.get(default) {
|
||||||
|
return Ok((default, default_info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(eyre!("could not find user"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
|
||||||
|
struct UserInfo {
|
||||||
|
name: String,
|
||||||
|
key: String,
|
||||||
|
}
|
Loading…
Reference in a new issue