1
0
Fork 0

convert to forgejo-api repo

This commit is contained in:
Cyborus 2023-11-09 12:34:47 -05:00
parent af89252c15
commit 285fddb2ea
No known key found for this signature in database
6 changed files with 7 additions and 1997 deletions

1587
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,15 @@
[package]
name = "fj"
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]
clap = { version = "4.3.11", features = ["derive"] }
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"
reqwest = { version = "0.11.18", features = ["json"] }
soft_assert = "0.1.1"
tokio = { version = "1.29.1", features = ["full"] }
url = "2.4.0"
[workspace]
members = ["forgejo-api"]
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"] }

View file

@ -1,15 +0,0 @@
[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"] }

View file

@ -1,175 +0,0 @@
use eyre::eyre;
use std::{collections::BTreeMap, io::ErrorKind};
use tokio::io::AsyncWriteExt;
use url::Url;
#[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
pub struct KeyInfo {
pub hosts: BTreeMap<String, LoginInfo>,
}
impl KeyInfo {
pub 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)
}
pub 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(())
}
pub fn get_current(&self) -> eyre::Result<(HostInfo<'_>, RepoInfo)> {
let repo = git2::Repository::open(".")?;
let remote_url = get_remote(&repo)?;
let login_info = self.get_login(&remote_url)?;
let mut path = remote_url
.path_segments()
.ok_or_else(|| eyre!("bad path"))?
.collect::<Vec<_>>();
let repo_name = path
.pop()
.ok_or_else(|| eyre!("path does not have repo name"))?
.to_string();
let owner = path
.pop()
.ok_or_else(|| eyre!("path does not have owner name"))?
.to_string();
let base_path = path.join("/");
let mut url = remote_url;
url.set_path(&base_path);
let host_info = HostInfo { url, login_info };
let repo_info = RepoInfo {
owner,
name: repo_name,
};
Ok((host_info, repo_info))
}
pub fn get_login(&self, url: &Url) -> eyre::Result<&LoginInfo> {
let host_str = url
.host_str()
.ok_or_else(|| eyre!("remote url does not have host"))?;
let domain = if let Some(port) = url.port() {
format!("{}:{}", host_str, port)
} else {
host_str.to_owned()
};
let login_info = self
.hosts
.get(&domain)
.ok_or_else(|| eyre!("not signed in to {domain}"))?;
Ok(login_info)
}
}
pub struct HostInfo<'a> {
url: Url,
login_info: &'a LoginInfo,
}
impl<'a> HostInfo<'a> {
pub fn api(&self) -> Result<forgejo_api::Forgejo, forgejo_api::ForgejoError> {
self.login_info.api_for(self.url())
}
pub fn url(&self) -> &Url {
&self.url
}
pub fn username(&self) -> &'a str {
&self.login_info.name
}
}
pub struct RepoInfo {
owner: String,
name: String,
}
impl RepoInfo {
pub fn owner(&self) -> &str {
&self.owner
}
pub fn name(&self) -> &str {
&self.name
}
}
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)]
pub struct LoginInfo {
name: String,
key: String,
}
impl LoginInfo {
pub fn new(name: String, key: String) -> Self {
Self { name, key }
}
pub fn username(&self) -> &str {
&self.name
}
pub fn api_for(&self, url: &Url) -> Result<forgejo_api::Forgejo, forgejo_api::ForgejoError> {
forgejo_api::Forgejo::new(&self.key, url.clone())
}
}
fn get_remote(repo: &git2::Repository) -> eyre::Result<Url> {
let head = repo.head()?;
let branch_name = head.name().ok_or_else(|| eyre!("branch name not UTF-8"))?;
let remote_name = repo.branch_upstream_remote(branch_name)?;
let remote_name = remote_name
.as_str()
.ok_or_else(|| eyre!("remote name not UTF-8"))?;
let remote = repo.find_remote(remote_name)?;
let url = Url::parse(std::str::from_utf8(remote.url_bytes())?)?;
Ok(url)
}

View file

@ -1,205 +0,0 @@
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;
mod keys;
use keys::*;
#[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: Option<String>,
/// 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,
},
Info,
Browse,
}
#[derive(Subcommand, Clone, Debug)]
pub enum AuthCommand {
Login,
Logout {
host: String,
},
AddKey {
/// The domain name of the forgejo instance.
host: String,
/// The user that the key is associated with
user: 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 = Url::parse(&host)?;
let login = keys.get_login(&host)?;
let api = login.api_for(&host)?;
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 {}",
host.join(&format!("{}/{}", login.username(), repo))?
);
let upstream = set_upstream.as_deref().unwrap_or("origin");
let repo = git2::Repository::open(".")?;
let mut remote = if set_upstream.is_some() || push {
repo.remote(upstream, new_repo.clone_url.as_str())?
} else {
repo.find_remote(upstream)?
};
if push {
remote.push::<&str>(&[], None)?;
}
}
RepoCommand::Info => {
let (host, repo) = keys.get_current()?;
let api = host.api()?;
let repo = api.get_repo(repo.owner(), repo.name()).await?;
match repo {
Some(repo) => {
dbg!(repo);
}
None => eprintln!("repo not found"),
}
}
RepoCommand::Browse => {
let (host, repo) = keys.get_current()?;
let mut url = host.url().clone();
let new_path = format!(
"{}/{}/{}",
url.path().strip_suffix("/").unwrap_or(url.path()),
repo.owner(),
repo.name(),
);
url.set_path(&new_path);
open::that(url.as_str())?;
}
},
Command::User { host } => {
let host = host.map(|host| Url::parse(&host)).transpose()?;
let (url, name) = match host {
Some(url) => (keys.get_login(&url)?.username(), url),
None => {
let (host, _) = keys.get_current()?;
(host.username(), host.url().clone())
}
};
eprintln!("currently signed in to {name}@{url}");
}
Command::Auth(auth_subcommand) => match auth_subcommand {
AuthCommand::Login => {
todo!();
// let user = readline("username: ").await?;
// let pass = readline("password: ").await?;
}
AuthCommand::Logout { host } => {
let info_opt = keys.hosts.remove(&host);
if let Some(info) = info_opt {
eprintln!("signed out of {}@{}", &info.username(), host);
} else {
eprintln!("already not signed in to {host}");
}
}
AuthCommand::AddKey { host, user, key } => {
let key = match key {
Some(key) => key,
None => readline("new key: ").await?,
};
if keys.hosts.get(&user).is_none() {
keys.hosts.insert(host, LoginInfo::new(user, key));
} else {
println!("key for {} already exists", host);
}
}
AuthCommand::List => {
if keys.hosts.is_empty() {
println!("No logins.");
}
for (host_url, login_info) in &keys.hosts {
println!("{}@{}", login_info.username(), 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?
}