rework key lookup

Now uses the current branch's remote to find the host,
and only has one account signed in per host.
This commit is contained in:
Cyborus 2023-08-18 22:40:46 -04:00
parent 7b5dcb8d65
commit 3dbbcb75a1
No known key found for this signature in database
2 changed files with 121 additions and 145 deletions

View file

@ -5,15 +5,10 @@ use url::Url;
#[derive(serde::Serialize, serde::Deserialize, Clone, Default)] #[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
pub struct KeyInfo { pub struct KeyInfo {
pub hosts: BTreeMap<String, HostInfo>, pub hosts: BTreeMap<String, LoginInfo>,
pub domain_to_name: BTreeMap<String, String>,
} }
impl KeyInfo { 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> { pub async fn load() -> eyre::Result<Self> {
let path = directories::ProjectDirs::from("", "Cyborus", "forgejo-cli") let path = directories::ProjectDirs::from("", "Cyborus", "forgejo-cli")
.ok_or_else(|| eyre!("Could not find data directory"))? .ok_or_else(|| eyre!("Could not find data directory"))?
@ -47,37 +42,78 @@ impl KeyInfo {
Ok(()) Ok(())
} }
pub async fn get_current_host_and_repo(&self) -> eyre::Result<(&str, &HostInfo, String)> { pub fn get_current(&self) -> eyre::Result<(HostInfo<'_>, RepoInfo)> {
let remotes = get_remotes().await?; let repo = git2::Repository::open(".")?;
let remote = get_remote(&remotes).await?; let remote_url = get_remote(&repo)?;
let host_str = remote 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() .host_str()
.ok_or_else(|| eyre!("remote url does not have host"))?; .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) format!("{}:{}", host_str, port)
} else { } else {
host_str.to_owned() 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 .hosts
.get_key_value(name) .get(&domain)
.ok_or_else(|| eyre!("not signed in to {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)> { pub fn url(&self) -> &Url {
let (name, host, _) = self.get_current_host_and_repo().await?; &self.url
Ok((name, host))
} }
async fn get_current_user(&self) -> eyre::Result<(&str, &UserInfo)> { pub fn username(&self) -> &'a str {
let user = self.get_current_host().await?.1.get_current_user()?; &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) Ok(repo)
} }
#[derive(serde::Serialize, serde::Deserialize, Clone)] #[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
pub struct HostInfo { pub struct LoginInfo {
pub default: Option<String>, name: String,
pub url: Url, key: String,
pub users: BTreeMap<String, UserInfo>,
} }
impl HostInfo { impl LoginInfo {
pub fn get_current_user(&self) -> eyre::Result<(&str, &UserInfo)> { pub fn new(name: String, key: String) -> Self {
if self.users.len() == 1 { Self {
let (s, k) = self.users.first_key_value().unwrap(); name,
return Ok((s, k)); key,
}
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")) 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)] fn get_remote(repo: &git2::Repository) -> eyre::Result<Url> {
pub struct UserInfo { let head = repo.head()?;
pub name: String, let branch_name = head.name().ok_or_else(|| eyre!("branch name not UTF-8"))?;
pub key: String, 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)?;
async fn get_remotes() -> eyre::Result<Vec<(String, Url)>> { let url = Url::parse(std::str::from_utf8(remote.url_bytes())?)?;
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");
};
Ok(url) Ok(url)
} }

View file

@ -55,22 +55,12 @@ pub enum AuthCommand {
Login, Login,
Logout { Logout {
host: String, host: String,
user: String,
},
Switch {
/// The host to set the default account for.
#[clap(short, long)]
host: Option<String>,
user: String,
}, },
AddKey { AddKey {
/// The domain name of the forgejo instance. /// The domain name of the forgejo instance.
host: String, host: String,
/// The user that the key is associated with /// The user that the key is associated with
user: String, 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. /// The key to add. If not present, the key will be read in from stdin.
key: Option<String>, key: Option<String>,
}, },
@ -93,13 +83,9 @@ async fn main() -> eyre::Result<()> {
set_upstream, set_upstream,
push, push,
} => { } => {
// let (host_domain, host_keys, repo) = keys.get_current_host_and_repo().await?; let host = Url::parse(&host)?;
let host_info = keys let login = keys.get_login(&host)?;
.hosts let api = login.api_for(&host)?;
.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 repo_spec = CreateRepoOption { let repo_spec = CreateRepoOption {
auto_init: false, auto_init: false,
default_branch: "main".into(), default_branch: "main".into(),
@ -116,7 +102,7 @@ async fn main() -> eyre::Result<()> {
let new_repo = api.create_repo(repo_spec).await?; let new_repo = api.create_repo(repo_spec).await?;
eprintln!( eprintln!(
"created new repo at {}", "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"); let upstream = set_upstream.as_deref().unwrap_or("origin");
@ -133,10 +119,9 @@ async fn main() -> eyre::Result<()> {
} }
} }
RepoCommand::Info => { RepoCommand::Info => {
let (_, host_keys, repo) = keys.get_current_host_and_repo().await?; let (host, repo) = keys.get_current()?;
let (_, user) = host_keys.get_current_user()?; let api = host.api()?;
let api = Forgejo::new(&user.key, host_keys.url.clone())?; let repo = api.get_repo(repo.owner(), repo.name()).await?;
let repo = api.get_repo(&user.name, &repo).await?;
match repo { match repo {
Some(repo) => { Some(repo) => {
dbg!(repo); dbg!(repo);
@ -145,26 +130,32 @@ async fn main() -> eyre::Result<()> {
} }
} }
RepoCommand::Browse => { RepoCommand::Browse => {
let (_, host_keys, repo) = keys.get_current_host_and_repo().await?; let (host, repo) = keys.get_current()?;
let (_, user) = host_keys.get_current_user()?; let mut url = host.url().clone();
open::that( let new_path = format!("{}/{}/{}",
host_keys url.path()
.url .strip_suffix("/")
.join(&format!("/{}/{repo}", user.name))? .unwrap_or(url.path()),
.as_str(), repo.owner(),
)?; repo.name(),
);
url.set_path(&new_path);
open::that(url.as_str())?;
} }
}, },
Command::User { host } => { Command::User { host } => {
let (_, host_keys) = match host.as_deref() { let host = host.map(|host| Url::parse(&host)).transpose()?;
Some(s) => ( let (url, name) = match host {
s, Some(url) => (
keys.hosts.get(s).ok_or_else(|| eyre!("not a known host"))?, 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 {name}@{url}");
eprintln!("currently signed in to {}@{}", info.name, host_keys.url);
} }
Command::Auth(auth_subcommand) => match auth_subcommand { Command::Auth(auth_subcommand) => match auth_subcommand {
AuthCommand::Login => { AuthCommand::Login => {
@ -172,57 +163,30 @@ async fn main() -> eyre::Result<()> {
// let user = readline("username: ").await?; // let user = readline("username: ").await?;
// let pass = readline("password: ").await?; // let pass = readline("password: ").await?;
} }
AuthCommand::Logout { host, user } => { AuthCommand::Logout { host } => {
let was_signed_in = keys let info_opt = keys
.hosts .hosts
.get_mut(&host) .remove(&host);
.and_then(|host| host.users.remove(&user)) if let Some(info) = info_opt {
.is_some(); eprintln!("signed out of {}@{}", &info.username(), host);
if was_signed_in {
eprintln!("signed out of {user}@{host}");
} else { } else {
eprintln!("already not signed in"); eprintln!("already not signed in to {host}");
}
}
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 { AuthCommand::AddKey {
host, host,
user, user,
name,
key, key,
} => { } => {
let host_keys = keys
.hosts
.get_mut(&host)
.ok_or_else(|| eyre!("unknown host {host}"))?;
let key = match key { let key = match key {
Some(key) => key, Some(key) => key,
None => readline("new key: ").await?, None => readline("new key: ").await?,
}; };
if host_keys.users.get(&user).is_none() { if keys.hosts.get(&user).is_none() {
host_keys.users.insert( keys.hosts.insert(host, LoginInfo::new(user, key));
name.unwrap_or_else(|| user.clone()),
UserInfo { name: user, key },
);
} else { } else {
println!( println!(
"key {} for {} already exists (rename it?)", "key for {} already exists",
name.unwrap_or(user),
host host
); );
} }
@ -231,11 +195,8 @@ async fn main() -> eyre::Result<()> {
if keys.hosts.is_empty() { if keys.hosts.is_empty() {
println!("No logins."); println!("No logins.");
} }
for (host_url, host_info) in &keys.hosts { for (host_url, login_info) in &keys.hosts {
for (key_name, key_info) in &host_info.users { println!("{}@{}", login_info.username(), host_url);
let UserInfo { name, key: _ } = key_info;
println!("{key_name}: {name}@{host_url}");
}
} }
} }
}, },