improve host url and repo name detection

This commit is contained in:
Cyborus 2024-04-17 15:41:06 -04:00
parent a014a4e87a
commit 46cd32ebd8
No known key found for this signature in database
4 changed files with 319 additions and 135 deletions

View file

@ -1,14 +1,24 @@
use clap::Subcommand;
use eyre::eyre;
use clap::{Args, Subcommand};
use eyre::{eyre, OptionExt};
use forgejo_api::structs::{
Comment, CreateIssueCommentOption, CreateIssueOption, EditIssueOption, IssueGetCommentsQuery,
};
use forgejo_api::Forgejo;
use crate::repo::RepoInfo;
use crate::repo::{RepoInfo, RepoName};
#[derive(Args, Clone, Debug)]
pub struct IssueCommand {
#[clap(long, short = 'R')]
remote: Option<String>,
#[clap(long, short)]
repo: Option<String>,
#[clap(subcommand)]
command: IssueSubcommand,
}
#[derive(Subcommand, Clone, Debug)]
pub enum IssueCommand {
pub enum IssueSubcommand {
Create {
title: String,
#[clap(long)]
@ -86,11 +96,12 @@ pub enum ViewCommand {
}
impl IssueCommand {
pub async fn run(self, keys: &crate::KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> {
use IssueCommand::*;
let repo = RepoInfo::get_current(remote_name)?;
let api = keys.get_api(&repo.host_url())?;
match self {
pub async fn run(self, keys: &crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
use IssueSubcommand::*;
let repo = RepoInfo::get_current(host_name, self.repo.as_deref(), self.remote.as_deref())?;
let api = keys.get_api(repo.host_url())?;
let repo = repo.name().ok_or_eyre("couldn't get repo name, try specifying with --repo")?;
match self.command {
Create { title, body } => create_issue(&repo, &api, title, body).await?,
View { id, command } => match command.unwrap_or(ViewCommand::Body) {
ViewCommand::Body => view_issue(&repo, &api, id).await?,
@ -122,7 +133,7 @@ impl IssueCommand {
}
async fn create_issue(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
title: String,
body: Option<String>,
@ -163,7 +174,7 @@ async fn create_issue(
Ok(())
}
async fn view_issue(repo: &RepoInfo, api: &Forgejo, id: u64) -> eyre::Result<()> {
async fn view_issue(repo: &RepoName, api: &Forgejo, id: u64) -> eyre::Result<()> {
let issue = api.issue_get_issue(repo.owner(), repo.name(), id).await?;
let title = issue
.title
@ -186,7 +197,7 @@ async fn view_issue(repo: &RepoInfo, api: &Forgejo, id: u64) -> eyre::Result<()>
Ok(())
}
async fn view_issues(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
query_str: Option<String>,
labels: Option<String>,
@ -240,7 +251,7 @@ async fn view_issues(
Ok(())
}
async fn view_comment(repo: &RepoInfo, api: &Forgejo, id: u64, idx: usize) -> eyre::Result<()> {
async fn view_comment(repo: &RepoName, api: &Forgejo, id: u64, idx: usize) -> eyre::Result<()> {
let query = IssueGetCommentsQuery {
since: None,
before: None,
@ -255,7 +266,7 @@ async fn view_comment(repo: &RepoInfo, api: &Forgejo, id: u64, idx: usize) -> ey
Ok(())
}
async fn view_comments(repo: &RepoInfo, api: &Forgejo, id: u64) -> eyre::Result<()> {
async fn view_comments(repo: &RepoName, api: &Forgejo, id: u64) -> eyre::Result<()> {
let query = IssueGetCommentsQuery {
since: None,
before: None,
@ -294,7 +305,7 @@ fn print_comment(comment: &Comment) -> eyre::Result<()> {
Ok(())
}
async fn browse_issue(repo: &RepoInfo, api: &Forgejo, id: Option<u64>) -> eyre::Result<()> {
async fn browse_issue(repo: &RepoName, api: &Forgejo, id: Option<u64>) -> eyre::Result<()> {
match id {
Some(id) => {
let issue = api.issue_get_issue(repo.owner(), repo.name(), id).await?;
@ -317,7 +328,7 @@ async fn browse_issue(repo: &RepoInfo, api: &Forgejo, id: Option<u64>) -> eyre::
}
async fn add_comment(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
issue: u64,
body: Option<String>,
@ -344,7 +355,7 @@ async fn add_comment(
}
async fn edit_title(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
issue: u64,
new_title: Option<String>,
@ -390,7 +401,7 @@ async fn edit_title(
}
async fn edit_body(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
issue: u64,
new_body: Option<String>,
@ -430,7 +441,7 @@ async fn edit_body(
}
async fn edit_comment(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
issue: u64,
idx: usize,
@ -478,7 +489,7 @@ async fn edit_comment(
}
async fn close_issue(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
issue: u64,
message: Option<Option<String>>,

View file

@ -1,7 +1,6 @@
use clap::{Parser, Subcommand};
use eyre::eyre;
use eyre::{eyre, Context};
use tokio::io::AsyncWriteExt;
use url::Url;
mod keys;
use keys::*;
@ -13,8 +12,8 @@ mod repo;
#[derive(Parser, Debug)]
pub struct App {
#[clap(long, short = 'R')]
remote: Option<String>,
#[clap(long, short = 'H')]
host: Option<String>,
#[clap(subcommand)]
command: Command,
}
@ -23,15 +22,13 @@ pub struct App {
pub enum Command {
#[clap(subcommand)]
Repo(repo::RepoCommand),
#[clap(subcommand)]
Issue(issues::IssueCommand),
User {
#[clap(long, short)]
host: Option<String>,
remote: Option<String>,
},
#[clap(subcommand)]
Auth(auth::AuthCommand),
#[clap(subcommand)]
Release(release::ReleaseCommand),
}
@ -40,21 +37,18 @@ async fn main() -> eyre::Result<()> {
let args = App::parse();
let mut keys = KeyInfo::load().await?;
let remote_name = args.remote.as_deref();
let host_name = args.host.as_deref();
// let remote = repo::RepoInfo::get_current(host_name, remote_name)?;
match args.command {
Command::Repo(subcommand) => subcommand.run(&keys, remote_name).await?,
Command::Issue(subcommand) => subcommand.run(&keys, remote_name).await?,
Command::User { host } => {
let host = host.map(|host| Url::parse(&host)).transpose()?;
let url = match host {
Some(url) => url,
None => repo::RepoInfo::get_current(remote_name)?.url().clone(),
};
Command::Repo(subcommand) => subcommand.run(&keys, host_name).await?,
Command::Issue(subcommand) => subcommand.run(&keys, host_name).await?,
Command::User { remote } => {
let url = repo::RepoInfo::get_current(host_name, None, remote.as_deref()).wrap_err("could not find host, try specifying with --host")?.host_url().clone();
let name = keys.get_login(&url)?.username();
eprintln!("currently signed in to {name}@{url}");
}
Command::Auth(subcommand) => subcommand.run(&mut keys).await?,
Command::Release(subcommand) => subcommand.run(&mut keys, remote_name).await?,
Command::Release(subcommand) => subcommand.run(&mut keys, host_name).await?,
}
keys.save().await?;

View file

@ -1,15 +1,25 @@
use clap::Subcommand;
use eyre::{bail, eyre};
use clap::{Subcommand, Args};
use eyre::{bail, eyre, OptionExt};
use forgejo_api::{
structs::{RepoCreateReleaseAttachmentQuery, RepoListReleasesQuery},
Forgejo,
};
use tokio::io::AsyncWriteExt;
use crate::{keys::KeyInfo, repo::RepoInfo};
use crate::{keys::KeyInfo, repo::{RepoInfo, RepoName}};
#[derive(Args, Clone, Debug)]
pub struct ReleaseCommand {
#[clap(long, short = 'R')]
remote: Option<String>,
#[clap(long, short)]
repo: Option<String>,
#[clap(subcommand)]
command: ReleaseSubcommand,
}
#[derive(Subcommand, Clone, Debug)]
pub enum ReleaseCommand {
pub enum ReleaseSubcommand {
Create {
name: String,
#[clap(long, short = 'T')]
@ -103,10 +113,11 @@ pub enum AssetCommand {
impl ReleaseCommand {
pub async fn run(self, keys: &KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> {
let repo = RepoInfo::get_current(remote_name)?;
let repo = RepoInfo::get_current(remote_name, self.repo.as_deref(), self.remote.as_deref())?;
let api = keys.get_api(&repo.host_url())?;
match self {
Self::Create {
let repo = repo.name().ok_or_eyre("couldn't get repo name, try specifying with --repo")?;
match self.command {
ReleaseSubcommand::Create {
name,
create_tag,
tag,
@ -121,7 +132,7 @@ impl ReleaseCommand {
)
.await?
}
Self::Edit {
ReleaseSubcommand::Edit {
name,
rename,
tag,
@ -129,14 +140,14 @@ impl ReleaseCommand {
draft,
prerelease,
} => edit_release(&repo, &api, name, rename, tag, body, draft, prerelease).await?,
Self::Delete { name, by_tag } => delete_release(&repo, &api, name, by_tag).await?,
Self::List {
ReleaseSubcommand::Delete { name, by_tag } => delete_release(&repo, &api, name, by_tag).await?,
ReleaseSubcommand::List {
include_prerelease,
include_draft,
} => list_releases(&repo, &api, include_prerelease, include_draft).await?,
Self::View { name, by_tag } => view_release(&repo, &api, name, by_tag).await?,
Self::Browse { name } => browse_release(&repo, &api, name).await?,
Self::Asset(subcommand) => match subcommand {
ReleaseSubcommand::View { name, by_tag } => view_release(&repo, &api, name, by_tag).await?,
ReleaseSubcommand::Browse { name } => browse_release(&repo, &api, name).await?,
ReleaseSubcommand::Asset(subcommand) => match subcommand {
AssetCommand::Create {
release,
path,
@ -157,7 +168,7 @@ impl ReleaseCommand {
}
async fn create_release(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
name: String,
create_tag: Option<Option<String>>,
@ -241,7 +252,7 @@ async fn create_release(
}
async fn edit_release(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
name: String,
rename: Option<String>,
@ -280,7 +291,7 @@ async fn edit_release(
}
async fn list_releases(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
prerelease: bool,
draft: bool,
@ -321,7 +332,7 @@ async fn list_releases(
}
async fn view_release(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
name: String,
by_tag: bool,
@ -384,7 +395,7 @@ async fn view_release(
Ok(())
}
async fn browse_release(repo: &RepoInfo, api: &Forgejo, name: Option<String>) -> eyre::Result<()> {
async fn browse_release(repo: &RepoName, api: &Forgejo, name: Option<String>) -> eyre::Result<()> {
match name {
Some(name) => {
let release = find_release(repo, api, &name).await?;
@ -395,16 +406,20 @@ async fn browse_release(repo: &RepoInfo, api: &Forgejo, name: Option<String>) ->
open::that(html_url.as_str())?;
}
None => {
let mut url = repo.url().clone();
url.path_segments_mut().unwrap().push("releases");
open::that(url.as_str())?;
let repo_data = api.repo_get(repo.owner(), repo.name()).await?;
let mut html_url = repo_data
.html_url
.clone()
.ok_or_else(|| eyre::eyre!("repository does not have html_url"))?;
html_url.path_segments_mut().unwrap().push("releases");
open::that(html_url.as_str())?;
}
}
Ok(())
}
async fn create_asset(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
release: String,
file: std::path::PathBuf,
@ -441,7 +456,7 @@ async fn create_asset(
}
async fn delete_asset(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
release: String,
asset: String,
@ -467,7 +482,7 @@ async fn delete_asset(
}
async fn download_asset(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
release: String,
asset: String,
@ -526,7 +541,7 @@ async fn download_asset(
}
async fn find_release(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
name: &str,
) -> eyre::Result<forgejo_api::structs::Release> {
@ -548,7 +563,7 @@ async fn find_release(
}
async fn delete_release(
repo: &RepoInfo,
repo: &RepoName,
api: &Forgejo,
name: String,
by_tag: bool,

View file

@ -1,32 +1,228 @@
use clap::Subcommand;
use eyre::eyre;
use eyre::{eyre, OptionExt};
use forgejo_api::structs::CreateRepoOption;
use url::Url;
pub struct RepoInfo {
owner: String,
name: String,
url: Url,
name: Option<RepoName>,
}
impl RepoInfo {
pub fn get_current(remote: Option<&str>) -> eyre::Result<Self> {
let repo = git2::Repository::open(".")?;
let url = get_remote(&repo, remote)?;
pub fn get_current(host: Option<&str>, repo: Option<&str>, remote: Option<&str>) -> eyre::Result<Self> {
// l = domain/owner/name
// s = owner/name
// x = is present
// i = found locally by git
//
// | repo | host | remote | ans-host | ans-repo |
// |------|------|--------|----------|----------|
// | l | x | x | repo | repo |
// | l | x | i | repo | repo |
// | l | x | | repo | repo |
// | l | | x | repo | repo |
// | l | | i | repo | repo |
// | l | | | repo | repo |
// | s | x | x | host | repo |
// | s | x | i | host | repo |
// | s | x | | host | repo |
// | s | | x | remote | repo |
// | s | | i | remote | repo |
// | s | | | err | repo |
// | | x | x | remote | remote |
// | | x | i | host | ?remote |
// | | x | | host | none |
// | | | x | remote | remote |
// | | | i | remote | remote |
// | | | | err | remote |
//
// | repo | host | remote | ans-host | ans-repo |
// |------|------|--------|----------|----------|
// | l | x | x | repo | repo |
// | l | x | | repo | repo |
// | l | | x | repo | repo |
// | l | | | repo | repo |
// | s | x | x | host | repo |
// | s | x | | host | repo |
// | s | | x | remote | repo |
// | s | | | err | repo |
// | | x | x | remote | remote |
// | | x | | remote | remote |
// | | | x | remote | remote |
// | | | | err | remote |
let mut path = url.path_segments().ok_or_else(|| eyre!("bad path"))?;
let owner = path
.next()
.ok_or_else(|| eyre!("path does not have owner name"))?
.to_string();
let name = path
.next()
.ok_or_else(|| eyre!("path does not have repo name"))?;
let name = name.strip_suffix(".git").unwrap_or(name).to_string();
// let repo_name;
//
// let repo_url;
// let remote;
// let host;
//
// let url = if repo_url { repo_url }
// else if repo_name { host.or(remote) }
// else { remote.or_host() }
//
// let name = repo_name.or(remote)
let mut repo_url: Option<Url> = None;
let mut repo_name: Option<RepoName> = None;
let repo_info = RepoInfo { owner, name, url };
Ok(repo_info)
if let Some(repo) = repo {
let (head, name) = repo.rsplit_once("/").ok_or_eyre("repo name must contain owner and name")?;
let name = name.strip_suffix(".git").unwrap_or(name);
match head.rsplit_once("/") {
Some((url, owner)) => {
if let Ok(url) = Url::parse(url) {
repo_url = Some(url);
} else if let Ok(url) = Url::parse(&format!("https://{url}/")) {
repo_url = Some(url);
}
repo_name = Some(RepoName {
owner: owner.to_owned(),
name: name.to_owned(),
});
},
None => {
repo_name = Some(RepoName {
owner: head.to_owned(),
name: name.to_owned(),
});
},
}
}
let repo_url = repo_url;
let repo_name = repo_name;
let host_url = host.and_then(|host| Url::parse(host).ok().or_else(|| Url::parse(&format!("https://{host}/")).ok()));
let (remote_url, remote_repo_name) = {
let mut out = (None, None);
if let Ok(local_repo) = git2::Repository::open(".") {
let tmp;
let mut name = remote;
// if the user didn't specify a remote, try guessing other ways
let mut tmp2;
if name.is_none() {
let all_remotes = local_repo.remotes()?;
// if there's only one remote, use that
if all_remotes.len() == 1 {
if let Some(remote_name) = all_remotes.get(0) {
tmp2 = Some(remote_name.to_owned());
name = tmp2.as_deref();
}
// if there's a remote whose host url matches the one
// specified with `--host`, use that
//
// This is different than using `--host` itself, since this
// will include the repo name, which `--host` can't do.
} else if let Some(host_url) = &host_url {
for remote_name in all_remotes.iter() {
let Some(remote_name) = remote_name else { continue };
let remote = local_repo.find_remote(remote_name)?;
if let Some(url) = remote.url() {
let (url, _) = url_strip_repo_name(Url::parse(url)?)?;
if url.host_str() == host_url.host_str() && url.path() == host_url.path() {
tmp2 = Some(remote_name.to_owned());
name = tmp2.as_deref();
}
}
}
}
}
// if there isn't an obvious answer, guess from the current
// branch's tracking remote
if name.is_none() {
let head = local_repo.head()?;
let branch_name = head.name().ok_or_else(|| eyre!("branch name not UTF-8"))?;
tmp = local_repo.branch_upstream_remote(branch_name).ok();
name = tmp.as_ref().map(|remote| {
remote
.as_str()
.ok_or_else(|| eyre!("remote name not UTF-8"))
}).transpose()?;
}
if let Some(name) = name {
if let Ok(remote) = local_repo.find_remote(name) {
let url_s = std::str::from_utf8(remote.url_bytes())?;
let url = Url::parse(url_s)?;
let (url, name) = url_strip_repo_name(url)?;
out = (Some(url), Some(name))
}
}
} else {
eyre::ensure!(remote.is_none(), "remote specified but no git repo found");
}
out
};
let (url, name) = if repo_url.is_some() {
(repo_url, repo_name)
} else if repo_name.is_some() {
(host_url.or(remote_url), repo_name)
} else {
if remote.is_some() {
(remote_url, remote_repo_name)
} else if host_url.is_none() || remote_url == host_url {
(remote_url, remote_repo_name)
} else {
(host_url, None)
}
};
let info = match (url, name) {
(Some(url), name) => RepoInfo {
url,
name,
},
(None, Some(_)) => eyre::bail!("cannot find repo, no host specified"),
(None, None) => eyre::bail!("no repo info specified"),
};
Ok(info)
}
pub fn name(&self) -> Option<&RepoName> {
self.name.as_ref()
}
pub fn host_url(&self) -> &Url {
&self.url
}
}
fn url_strip_repo_name(mut url: Url) -> eyre::Result<(Url, RepoName)> {
let mut iter = url
.path_segments()
.ok_or_eyre("repo url cannot be a base")?
.rev();
let name = iter.next().ok_or_eyre("repo url too short")?;
let name = name.strip_suffix(".git").unwrap_or(name).to_owned();
let owner = iter.next().ok_or_eyre("repo url too short")?.to_owned();
// Remove the username and repo name from the url
url.path_segments_mut()
.map_err(|_| eyre!("repo url cannot be a base"))?
.pop()
.pop();
Ok((url, RepoName { owner, name }))
}
#[derive(Debug)]
pub struct RepoName {
owner: String,
name: String,
}
impl RepoName {
pub fn owner(&self) -> &str {
&self.owner
}
@ -34,48 +230,11 @@ impl RepoInfo {
pub fn name(&self) -> &str {
&self.name
}
pub fn url(&self) -> &Url {
&self.url
}
pub fn host_url(&self) -> Url {
let mut url = self.url.clone();
url.path_segments_mut()
.expect("invalid url: cannot be a base")
.pop()
.pop();
url
}
}
fn get_remote(repo: &git2::Repository, name: Option<&str>) -> eyre::Result<Url> {
if let Some(name) = name {
if let Ok(url) = Url::parse(name) {
return Ok(url);
}
}
let remote_name;
let remote_name = match name {
Some(name) => name,
None => {
let head = repo.head()?;
let branch_name = head.name().ok_or_else(|| eyre!("branch name not UTF-8"))?;
remote_name = repo.branch_upstream_remote(branch_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)
}
#[derive(Subcommand, Clone, Debug)]
pub enum RepoCommand {
Create {
host: String,
repo: String,
// flags
@ -91,15 +250,22 @@ pub enum RepoCommand {
#[clap(long, short)]
push: bool,
},
Info,
Browse,
Info {
name: Option<String>,
#[clap(long, short = 'R')]
remote: Option<String>,
},
Browse {
name: Option<String>,
#[clap(long, short = 'R')]
remote: Option<String>,
},
}
impl RepoCommand {
pub async fn run(self, keys: &crate::KeyInfo, remote_name: Option<&str>) -> eyre::Result<()> {
pub async fn run(self, keys: &crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
match self {
RepoCommand::Create {
host,
repo,
description,
@ -107,8 +273,8 @@ impl RepoCommand {
set_upstream,
push,
} => {
let host = Url::parse(&host)?;
let api = keys.get_api(&host)?;
let host = RepoInfo::get_current(host_name, None, None)?;
let api = keys.get_api(host.host_url())?;
let repo_spec = CreateRepoOption {
auto_init: Some(false),
default_branch: Some("main".into()),
@ -127,7 +293,7 @@ impl RepoCommand {
.full_name
.as_ref()
.ok_or_else(|| eyre::eyre!("new_repo does not have full_name"))?;
eprintln!("created new repo at {}", host.join(&full_name)?);
eprintln!("created new repo at {}", host.host_url().join(&full_name)?);
if set_upstream.is_some() || push {
let repo = git2::Repository::open(".")?;
@ -158,22 +324,20 @@ impl RepoCommand {
}
}
}
RepoCommand::Info => {
let repo = RepoInfo::get_current(remote_name)?;
RepoCommand::Info { name, remote } => {
let repo = RepoInfo::get_current(host_name, name.as_deref(), remote.as_deref())?;
let api = keys.get_api(&repo.host_url())?;
let repo = repo.name().ok_or_eyre("couldn't get repo name, please specify")?;
let repo = api.repo_get(repo.owner(), repo.name()).await?;
dbg!(repo);
}
RepoCommand::Browse => {
let repo = RepoInfo::get_current(remote_name)?;
RepoCommand::Browse { name, remote } => {
let repo = RepoInfo::get_current(host_name, name.as_deref(), remote.as_deref())?;
let mut url = repo.host_url().clone();
let new_path = format!(
"{}/{}/{}",
url.path().strip_suffix("/").unwrap_or(url.path()),
repo.owner(),
repo.name(),
);
url.set_path(&new_path);
let repo = repo.name().ok_or_eyre("couldn't get repo name, please specify")?;
url.path_segments_mut().map_err(|_| eyre!("url invalid"))?.extend([repo.owner(), repo.name()]);
open::that(url.as_str())?;
}
};