From 3dbbcb75a14523cb07bc3b23ad54f654d13da3a9 Mon Sep 17 00:00:00 2001 From: Cyborus <87248184+Cyborus04@users.noreply.github.com> Date: Fri, 18 Aug 2023 22:40:46 -0400 Subject: [PATCH] rework key lookup Now uses the current branch's remote to find the host, and only has one account signed in per host. --- src/keys.rs | 149 +++++++++++++++++++++++++++++----------------------- src/main.rs | 117 ++++++++++++++--------------------------- 2 files changed, 121 insertions(+), 145 deletions(-) diff --git a/src/keys.rs b/src/keys.rs index 047eba1..bc57cc4 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -5,15 +5,10 @@ use url::Url; #[derive(serde::Serialize, serde::Deserialize, Clone, Default)] pub struct KeyInfo { - pub hosts: BTreeMap, - pub domain_to_name: BTreeMap, + pub hosts: BTreeMap, } 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 { 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::>(); + 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 { + 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, - pub url: Url, - pub users: BTreeMap, +#[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::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> { - 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::>(); - Ok(remotes) -} - -async fn get_remote(remotes: &[(String, Url)]) -> eyre::Result { - 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 { + 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) } diff --git a/src/main.rs b/src/main.rs index 7e881ee..5255abc 100644 --- a/src/main.rs +++ b/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, - 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, /// The key to add. If not present, the key will be read in from stdin. key: Option, }, @@ -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); } } },