Merge pull request 'rework key lookup' (#9) from rework-key-lookup into main
This commit is contained in:
		
						commit
						9a37f3bcd1
					
				
					 2 changed files with 121 additions and 145 deletions
				
			
		
							
								
								
									
										149
									
								
								src/keys.rs
									
										
									
									
									
								
							
							
						
						
									
										149
									
								
								src/keys.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -5,15 +5,10 @@ use url::Url;
 | 
			
		|||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
 | 
			
		||||
pub struct KeyInfo {
 | 
			
		||||
    pub hosts: BTreeMap<String, HostInfo>,
 | 
			
		||||
    pub domain_to_name: BTreeMap<String, String>,
 | 
			
		||||
    pub hosts: BTreeMap<String, LoginInfo>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl KeyInfo {
 | 
			
		||||
    fn domain_to_name(&self, domain: &str) -> Option<&str> {
 | 
			
		||||
        self.domain_to_name.get(domain).map(|s| &**s)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn load() -> eyre::Result<Self> {
 | 
			
		||||
        let path = directories::ProjectDirs::from("", "Cyborus", "forgejo-cli")
 | 
			
		||||
            .ok_or_else(|| eyre!("Could not find data directory"))?
 | 
			
		||||
| 
						 | 
				
			
			@ -47,37 +42,78 @@ impl KeyInfo {
 | 
			
		|||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub 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
 | 
			
		||||
    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) = remote.port() {
 | 
			
		||||
        let domain = if let Some(port) = url.port() {
 | 
			
		||||
            format!("{}:{}", host_str, port)
 | 
			
		||||
        } else {
 | 
			
		||||
            host_str.to_owned()
 | 
			
		||||
        };
 | 
			
		||||
        let name = self
 | 
			
		||||
            .domain_to_name(&domain)
 | 
			
		||||
            .ok_or_else(|| eyre!("unknown remote"))?;
 | 
			
		||||
 | 
			
		||||
        let (name, host) = self
 | 
			
		||||
        let login_info = self
 | 
			
		||||
            .hosts
 | 
			
		||||
            .get_key_value(name)
 | 
			
		||||
            .get(&domain)
 | 
			
		||||
            .ok_or_else(|| eyre!("not signed in to {domain}"))?;
 | 
			
		||||
        Ok((name, host, repo_from_url(&remote)?.into()))
 | 
			
		||||
        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 async fn get_current_host(&self) -> eyre::Result<(&str, &HostInfo)> {
 | 
			
		||||
        let (name, host, _) = self.get_current_host_and_repo().await?;
 | 
			
		||||
        Ok((name, host))
 | 
			
		||||
    pub fn url(&self) -> &Url {
 | 
			
		||||
        &self.url
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn get_current_user(&self) -> eyre::Result<(&str, &UserInfo)> {
 | 
			
		||||
        let user = self.get_current_host().await?.1.get_current_user()?;
 | 
			
		||||
    pub fn username(&self) -> &'a str {
 | 
			
		||||
        &self.login_info.name
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
        Ok(user)
 | 
			
		||||
pub struct RepoInfo {
 | 
			
		||||
    owner: String,
 | 
			
		||||
    name: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl RepoInfo {
 | 
			
		||||
    pub fn owner(&self) -> &str {
 | 
			
		||||
        &self.owner
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    pub fn name(&self) -> &str {
 | 
			
		||||
        &self.name
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -100,57 +136,36 @@ fn repo_from_url(url: &Url) -> eyre::Result<&str> {
 | 
			
		|||
    Ok(repo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
 | 
			
		||||
pub struct HostInfo {
 | 
			
		||||
    pub default: Option<String>,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
    pub users: BTreeMap<String, UserInfo>,
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
 | 
			
		||||
pub struct LoginInfo {
 | 
			
		||||
    name: String,
 | 
			
		||||
    key: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl HostInfo {
 | 
			
		||||
    pub 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));
 | 
			
		||||
            }
 | 
			
		||||
impl LoginInfo {
 | 
			
		||||
    pub fn new(name: String, key: String) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            name,
 | 
			
		||||
            key,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        Err(eyre!("could not find user"))
 | 
			
		||||
    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())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
 | 
			
		||||
pub struct UserInfo {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub key: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn get_remotes() -> eyre::Result<Vec<(String, Url)>> {
 | 
			
		||||
    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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
        eyre::bail!("could not find remote");
 | 
			
		||||
    };
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										117
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										117
									
								
								src/main.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -55,22 +55,12 @@ 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>,
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -93,13 +83,9 @@ async fn main() -> eyre::Result<()> {
 | 
			
		|||
                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 api = Forgejo::new(&user.key, host_info.url.clone())?;
 | 
			
		||||
                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(),
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +102,7 @@ async fn main() -> eyre::Result<()> {
 | 
			
		|||
                let new_repo = api.create_repo(repo_spec).await?;
 | 
			
		||||
                eprintln!(
 | 
			
		||||
                    "created new repo at {}",
 | 
			
		||||
                    host_info.url.join(&format!("{}/{}", user.name, repo))?
 | 
			
		||||
                    host.join(&format!("{}/{}", login.username(), repo))?
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                let upstream = set_upstream.as_deref().unwrap_or("origin");
 | 
			
		||||
| 
						 | 
				
			
			@ -133,10 +119,9 @@ async fn main() -> eyre::Result<()> {
 | 
			
		|||
                }
 | 
			
		||||
            }
 | 
			
		||||
            RepoCommand::Info => {
 | 
			
		||||
                let (_, host_keys, repo) = keys.get_current_host_and_repo().await?;
 | 
			
		||||
                let (_, user) = host_keys.get_current_user()?;
 | 
			
		||||
                let api = Forgejo::new(&user.key, host_keys.url.clone())?;
 | 
			
		||||
                let repo = api.get_repo(&user.name, &repo).await?;
 | 
			
		||||
                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);
 | 
			
		||||
| 
						 | 
				
			
			@ -145,26 +130,32 @@ async fn main() -> eyre::Result<()> {
 | 
			
		|||
                }
 | 
			
		||||
            }
 | 
			
		||||
            RepoCommand::Browse => {
 | 
			
		||||
                let (_, host_keys, repo) = keys.get_current_host_and_repo().await?;
 | 
			
		||||
                let (_, user) = host_keys.get_current_user()?;
 | 
			
		||||
                open::that(
 | 
			
		||||
                    host_keys
 | 
			
		||||
                        .url
 | 
			
		||||
                        .join(&format!("/{}/{repo}", user.name))?
 | 
			
		||||
                        .as_str(),
 | 
			
		||||
                )?;
 | 
			
		||||
                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_keys) = match host.as_deref() {
 | 
			
		||||
                Some(s) => (
 | 
			
		||||
                    s,
 | 
			
		||||
                    keys.hosts.get(s).ok_or_else(|| eyre!("not a known host"))?,
 | 
			
		||||
            let host = host.map(|host| Url::parse(&host)).transpose()?;
 | 
			
		||||
            let (url, name) = match host {
 | 
			
		||||
                Some(url) => (
 | 
			
		||||
                    keys.get_login(&url)?.username(),
 | 
			
		||||
                    url,
 | 
			
		||||
                ),
 | 
			
		||||
                None => keys.get_current_host().await?,
 | 
			
		||||
                None => {
 | 
			
		||||
                    let (host, _) = keys.get_current()?;
 | 
			
		||||
                    (host.username(), host.url().clone())
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            let (_, info) = host_keys.get_current_user()?;
 | 
			
		||||
            eprintln!("currently signed in to {}@{}", info.name, host_keys.url);
 | 
			
		||||
            eprintln!("currently signed in to {name}@{url}");
 | 
			
		||||
        }
 | 
			
		||||
        Command::Auth(auth_subcommand) => match auth_subcommand {
 | 
			
		||||
            AuthCommand::Login => {
 | 
			
		||||
| 
						 | 
				
			
			@ -172,57 +163,30 @@ async fn main() -> eyre::Result<()> {
 | 
			
		|||
                // let user = readline("username: ").await?;
 | 
			
		||||
                // let pass = readline("password: ").await?;
 | 
			
		||||
            }
 | 
			
		||||
            AuthCommand::Logout { host, user } => {
 | 
			
		||||
                let was_signed_in = keys
 | 
			
		||||
            AuthCommand::Logout { host } => {
 | 
			
		||||
                let info_opt = keys
 | 
			
		||||
                    .hosts
 | 
			
		||||
                    .get_mut(&host)
 | 
			
		||||
                    .and_then(|host| host.users.remove(&user))
 | 
			
		||||
                    .is_some();
 | 
			
		||||
                if was_signed_in {
 | 
			
		||||
                    eprintln!("signed out of {user}@{host}");
 | 
			
		||||
                    .remove(&host);
 | 
			
		||||
                if let Some(info) = info_opt {
 | 
			
		||||
                    eprintln!("signed out of {}@{}", &info.username(), 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!(),
 | 
			
		||||
                    eprintln!("already not signed in to {host}");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            AuthCommand::AddKey {
 | 
			
		||||
                host,
 | 
			
		||||
                user,
 | 
			
		||||
                name,
 | 
			
		||||
                key,
 | 
			
		||||
            } => {
 | 
			
		||||
                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?,
 | 
			
		||||
                };
 | 
			
		||||
                if host_keys.users.get(&user).is_none() {
 | 
			
		||||
                    host_keys.users.insert(
 | 
			
		||||
                        name.unwrap_or_else(|| user.clone()),
 | 
			
		||||
                        UserInfo { name: user, key },
 | 
			
		||||
                    );
 | 
			
		||||
                if keys.hosts.get(&user).is_none() {
 | 
			
		||||
                    keys.hosts.insert(host, LoginInfo::new(user, key));
 | 
			
		||||
                } else {
 | 
			
		||||
                    println!(
 | 
			
		||||
                        "key {} for {} already exists (rename it?)",
 | 
			
		||||
                        name.unwrap_or(user),
 | 
			
		||||
                        "key for {} already exists",
 | 
			
		||||
                        host
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -231,11 +195,8 @@ async fn main() -> eyre::Result<()> {
 | 
			
		|||
                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}");
 | 
			
		||||
                    }
 | 
			
		||||
                for (host_url, login_info) in &keys.hosts {
 | 
			
		||||
                    println!("{}@{}", login_info.username(), host_url);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue