convert to forgejo-api repo
This commit is contained in:
		
							parent
							
								
									af89252c15
								
							
						
					
					
						commit
						285fddb2ea
					
				
					 6 changed files with 7 additions and 1997 deletions
				
			
		
							
								
								
									
										1587
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1587
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										22
									
								
								Cargo.toml
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								Cargo.toml
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -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"] }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"] }
 | 
			
		||||
							
								
								
									
										175
									
								
								src/keys.rs
									
										
									
									
									
								
							
							
						
						
									
										175
									
								
								src/keys.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										205
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										205
									
								
								src/main.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -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?
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue