diff options
author | Cyborus <cyborus@noreply.codeberg.org> | 2024-11-05 20:02:31 +0100 |
---|---|---|
committer | Cyborus <cyborus@noreply.codeberg.org> | 2024-11-05 20:02:31 +0100 |
commit | 23137cb2ec1fa84b953fd824cf59d522e1ed29df (patch) | |
tree | 94eeb77ba35329ba9bb685fe62b7c00562b23706 | |
parent | Merge pull request 'fix ssh url parsing' (#141) from ssh-parse-mistake into main (diff) | |
parent | chore: format (diff) | |
download | forgejo-cli-23137cb2ec1fa84b953fd824cf59d522e1ed29df.tar.xz forgejo-cli-23137cb2ec1fa84b953fd824cf59d522e1ed29df.zip |
Merge pull request 'add creating prs with agit' (#138) from cyborus/agit-main into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-cli/pulls/138
-rw-r--r-- | src/prs.rs | 338 | ||||
-rw-r--r-- | src/repo.rs | 19 |
2 files changed, 256 insertions, 101 deletions
@@ -48,7 +48,7 @@ pub enum PrSubcommand { #[clap(long)] base: Option<String>, /// The branch to pull changes from. - #[clap(long)] + #[clap(long, group = "source")] head: Option<String>, /// What to name the new pull request. /// @@ -64,8 +64,11 @@ pub enum PrSubcommand { #[clap(long, short, id = "[HOST/]OWNER/REPO")] repo: Option<RepoArg>, /// Open the PR creation menu in your web browser - #[clap(short, long, group = "web-or-cmd")] + #[clap(short, long, group = "web-or-cmd", group = "web-or-agit")] web: bool, + /// Open the PR creation menu in your web browser + #[clap(short, long, group = "source", group = "web-or-agit")] + agit: bool, }, /// View the contents of a pull request View { @@ -271,9 +274,10 @@ pub enum ViewCommand { impl PrCommand { pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> { use PrSubcommand::*; - let repo = RepoInfo::get_current(host_name, self.repo(), self.remote.as_deref(), &keys)?; - let api = keys.get_api(repo.host_url()).await?; - let repo = repo.name().ok_or_else(|| self.no_repo_error())?; + let repo_info = + RepoInfo::get_current(host_name, self.repo(), self.remote.as_deref(), &keys)?; + let api = keys.get_api(repo_info.host_url()).await?; + let repo = repo_info.name().ok_or_else(|| self.no_repo_error())?; match self.command { Create { title, @@ -282,7 +286,21 @@ impl PrCommand { body, repo: _, web, - } => create_pr(repo, &api, title, base, head, body, web).await?, + agit, + } => { + create_pr( + repo, + &api, + title, + base, + head, + body, + web, + agit, + repo_info.remote_name(), + ) + .await? + } Merge { pr, method, @@ -510,7 +528,11 @@ pub async fn view_pr(repo: &RepoName, api: &Forgejo, id: Option<u64>) -> eyre::R println!( "By {white}{username}{reset} {dash} {state} {dash} {bright_green}+{additions} {bright_red}-{deletions}{reset}" ); - println!("From `{head_name}` into `{base_name}`"); + if head_name.is_empty() { + println!("Into `{base_name}`"); + } else { + println!("From `{head_name}` into `{base_name}`"); + } if let Some(body) = &pr.body { if !body.trim().is_empty() { @@ -893,11 +915,14 @@ async fn create_pr( head: Option<String>, body: Option<String>, web: bool, + agit: bool, + remote_name: Option<&str>, ) -> eyre::Result<()> { let mut repo_data = api.repo_get(repo.owner(), repo.name()).await?; let head = match head { - Some(head) => head, + _ if agit => None, + Some(head) => Some(head), None => { let local_repo = git2::Repository::open(".")?; let head = local_repo.head()?; @@ -910,9 +935,15 @@ async fn create_pr( .name() .ok_or_eyre("current branch does not have utf8 name")?; let upstream_remote = local_repo.branch_upstream_remote(branch_ref)?; - let remote_name = upstream_remote - .as_str() - .ok_or_eyre("remote does not have utf8 name")?; + + let remote_name = if let Some(remote_name) = remote_name { + remote_name + } else { + let upstream_name = upstream_remote + .as_str() + .ok_or_eyre("remote does not have utf8 name")?; + upstream_name + }; let remote = local_repo.find_remote(remote_name)?; let remote_url_s = remote.url().ok_or_eyre("remote does not have utf8 url")?; @@ -939,11 +970,13 @@ async fn create_pr( let upstream_branch = upstream_branch .as_str() .ok_or_eyre("remote branch does not have utf8 name")?; - upstream_branch - .rsplit_once("/") - .map(|(_, b)| b) - .unwrap_or(upstream_branch) - .to_owned() + Some( + upstream_branch + .rsplit_once("/") + .map(|(_, b)| b) + .unwrap_or(upstream_branch) + .to_owned(), + ) } }; @@ -979,7 +1012,7 @@ async fn create_pr( parent_owner, parent_name, parent_repo, - format!("{}:{}", repo.owner(), head), + head.map(|head| format!("{}:{}", repo.owner(), head)), ) } else { ( @@ -1000,6 +1033,8 @@ async fn create_pr( }; if web { + // --web and --agit are mutually exclusive, so this shouldn't ever fail + let head = head.unwrap(); let mut pr_create_url = base_repo .html_url .clone() @@ -1019,31 +1054,111 @@ async fn create_pr( body } }; - let pr = api - .repo_create_pull_request( - &repo_owner, - &repo_name, - CreatePullRequestOption { - assignee: None, - assignees: None, - base: Some(base.to_owned()), - body: Some(body), - due_date: None, - head: Some(head), - labels: None, - milestone: None, - title: Some(title), - }, - ) - .await?; - let number = pr - .number - .ok_or_else(|| eyre::eyre!("pr does not have number"))?; - let title = pr - .title - .as_ref() - .ok_or_else(|| eyre::eyre!("pr does not have title"))?; - println!("created pull request #{}: {}", number, title); + match head { + Some(head) => { + let pr = api + .repo_create_pull_request( + &repo_owner, + &repo_name, + CreatePullRequestOption { + assignee: None, + assignees: None, + base: Some(base.to_owned()), + body: Some(body), + due_date: None, + head: Some(head), + labels: None, + milestone: None, + title: Some(title), + }, + ) + .await?; + let number = pr + .number + .ok_or_else(|| eyre::eyre!("pr does not have number"))?; + let title = pr + .title + .as_ref() + .ok_or_else(|| eyre::eyre!("pr does not have title"))?; + println!("created pull request #{}: {}", number, title); + } + // no head means agit + None => { + let local_repo = git2::Repository::open(".")?; + let mut git_config = local_repo.config()?; + let clone_url = base_repo + .clone_url + .as_ref() + .ok_or_eyre("base repo does not have clone url")?; + + let git_auth = auth_git2::GitAuthenticator::new(); + + let mut push_options = git2::PushOptions::new(); + + let mut remote_callbacks = git2::RemoteCallbacks::new(); + remote_callbacks.credentials(git_auth.credentials(&git_config)); + push_options.remote_callbacks(remote_callbacks); + + let current_branch = git2::Branch::wrap(local_repo.head()?.resolve()?); + let current_branch_name = current_branch + .name()? + .ok_or_eyre("branch name is not utf8")?; + let topic = format!("agit-{current_branch_name}"); + + push_options.remote_push_options(&[ + &format!("topic={topic}"), + &format!("title={title}"), + &format!("description={body}"), + ]); + + let mut remote = if let Some(remote_name) = remote_name { + local_repo.find_remote(remote_name)? + } else { + local_repo.remote_anonymous(clone_url.as_str())? + }; + + remote.push(&[&format!("HEAD:refs/for/{base}")], Some(&mut push_options))?; + + // needed so the mutable reference later is valid + drop(push_options); + + println!("created new PR: \"{title}\""); + + let merge_setting_name = format!("branch.{current_branch_name}.merge"); + let remote_setting_name = format!("branch.{current_branch_name}.remote"); + let cfg_push_default = git_config.get_string("push.default").ok(); + let cfg_branch_merge = git_config.get_string(&merge_setting_name).ok(); + let cfg_branch_remote = git_config.get_string(&remote_setting_name).ok(); + + let topic_setting = format!("refs/for/{base}/{topic}"); + + let default_is_upstream = cfg_push_default.is_some_and(|s| s == "upstream"); + let branch_merge_is_agit = cfg_branch_merge.is_some_and(|s| s == topic_setting); + let branch_remote_is_agit = cfg_branch_remote.is_some_and(|s| s == topic_setting); + if !default_is_upstream || !branch_merge_is_agit || !branch_remote_is_agit { + println!("Would you like to set the needed git config"); + println!("items so that `git push` works for this pr?"); + loop { + let response = crate::readline("(y/N/?) ").await?; + match response.trim() { + "y" | "Y" | "yes" | "Yes" => { + let remote = remote_name.unwrap_or(clone_url.as_str()); + git_config.set_str("push.default", "upstream")?; + git_config.set_str(&merge_setting_name, &topic_setting)?; + git_config.set_str(&remote_setting_name, remote)?; + break; + } + "?" | "h" | "H" | "help" => { + println!("This would set the following config options:"); + println!(" push.default = upstream"); + println!(" branch.{current_branch_name}.merge = {topic_setting}"); + } + _ => break, + } + } + } + } + } } Ok(()) @@ -1490,67 +1605,94 @@ async fn guess_pr( let head = local_repo.head()?; eyre::ensure!(head.is_branch(), "head is not on branch"); let local_branch = git2::Branch::wrap(head); - let remote_branch = local_branch.upstream()?; - let remote_head_name = remote_branch - .get() - .name() - .ok_or_eyre("remote branch does not have valid name")?; - let remote_head_short = remote_head_name - .rsplit_once("/") - .map(|(_, b)| b) - .unwrap_or(remote_head_name); - let this_repo = api.repo_get(repo.owner(), repo.name()).await?; - - // check for PRs on the main branch first - let base = this_repo - .default_branch - .as_deref() - .ok_or_eyre("repo does not have default branch")?; - if let Ok(pr) = api - .repo_get_pull_request_by_base_head(repo.owner(), repo.name(), base, remote_head_short) - .await - { - return Ok(pr); - } + let local_branch_name = local_branch.name()?.ok_or_eyre("branch name is not utf8")?; + let config = local_repo.config()?; + let remote_head_name = config.get_string(&format!("branch.{local_branch_name}.merge"))?; + + let maybe_agit = remote_head_name + .strip_prefix("refs/for/") + .and_then(|s| s.split_once("/")); + + match maybe_agit { + Some((base, head)) => { + let username = api + .user_get_current() + .await? + .login + .ok_or_eyre("user does not have username")? + .to_lowercase(); + let head = format!("{username}/{head}"); + return Ok(api + .repo_get_pull_request_by_base_head(repo.owner(), repo.name(), base, &head) + .await?); + } + None => { + let remote_head_short = remote_head_name + .rsplit_once("/") + .map(|(_, b)| b) + .unwrap_or(&remote_head_name); - let this_full_name = this_repo - .full_name - .as_deref() - .ok_or_eyre("repo does not have full name")?; - let parent_remote_head_name = format!("{this_full_name}:{remote_head_short}"); + let this_repo = api.repo_get(repo.owner(), repo.name()).await?; - if let Some(parent) = this_repo.parent.as_deref() { - let (parent_owner, parent_name) = repo_name_from_repo(parent)?; - let parent_base = this_repo - .default_branch - .as_deref() - .ok_or_eyre("repo does not have default branch")?; - if let Ok(pr) = api - .repo_get_pull_request_by_base_head( - parent_owner, - parent_name, - parent_base, - &parent_remote_head_name, - ) - .await - { - return Ok(pr); - } - } + // check for PRs on the main branch first + let base = this_repo + .default_branch + .as_deref() + .ok_or_eyre("repo does not have default branch")?; + if let Ok(pr) = api + .repo_get_pull_request_by_base_head( + repo.owner(), + repo.name(), + base, + remote_head_short, + ) + .await + { + return Ok(pr); + } - // then iterate all branches - if let Some(pr) = find_pr_from_branch(repo.owner(), repo.name(), api, remote_head_short).await? - { - return Ok(pr); - } + let this_full_name = this_repo + .full_name + .as_deref() + .ok_or_eyre("repo does not have full name")?; + let parent_remote_head_name = format!("{this_full_name}:{remote_head_short}"); + + if let Some(parent) = this_repo.parent.as_deref() { + let (parent_owner, parent_name) = repo_name_from_repo(parent)?; + let parent_base = this_repo + .default_branch + .as_deref() + .ok_or_eyre("repo does not have default branch")?; + if let Ok(pr) = api + .repo_get_pull_request_by_base_head( + parent_owner, + parent_name, + parent_base, + &parent_remote_head_name, + ) + .await + { + return Ok(pr); + } + } - if let Some(parent) = this_repo.parent.as_deref() { - let (parent_owner, parent_name) = repo_name_from_repo(parent)?; + // then iterate all branches + if let Some(pr) = + find_pr_from_branch(repo.owner(), repo.name(), api, remote_head_short).await? + { + return Ok(pr); + } - if let Some(pr) = - find_pr_from_branch(parent_owner, parent_name, api, &parent_remote_head_name).await? - { - return Ok(pr); + if let Some(parent) = this_repo.parent.as_deref() { + let (parent_owner, parent_name) = repo_name_from_repo(parent)?; + + if let Some(pr) = + find_pr_from_branch(parent_owner, parent_name, api, &parent_remote_head_name) + .await? + { + return Ok(pr); + } + } } } diff --git a/src/repo.rs b/src/repo.rs index 3780642..580f3d5 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -10,6 +10,7 @@ use crate::SpecialRender; pub struct RepoInfo { url: Url, name: Option<RepoName>, + remote_name: Option<String>, } impl RepoInfo { @@ -73,6 +74,8 @@ impl RepoInfo { .map(|url| keys.deref_alias(url)) }); + let mut final_remote_name = None; + let (remote_url, remote_repo_name) = { let mut out = (None, None); if let Ok(local_repo) = git2::Repository::open(".") { @@ -143,9 +146,11 @@ impl RepoInfo { if let Ok(remote) = local_repo.find_remote(&name) { let url_s = std::str::from_utf8(remote.url_bytes())?; let url = keys.deref_alias(crate::ssh_url_parse(url_s)?); - let (url, name) = url_strip_repo_name(url)?; + let (url, repo_name) = url_strip_repo_name(url)?; + + out = (Some(url), Some(repo_name)); - out = (Some(url), Some(name)) + final_remote_name = Some(name); } } } else { @@ -177,7 +182,11 @@ impl RepoInfo { }); let info = match (url, name) { - (Some(url), name) => RepoInfo { url, name }, + (Some(url), name) => RepoInfo { + url, + name, + remote_name: final_remote_name, + }, (None, Some(_)) => eyre::bail!("cannot find repo, no host specified"), (None, None) => eyre::bail!("no repo info specified"), }; @@ -192,6 +201,10 @@ impl RepoInfo { pub fn host_url(&self) -> &Url { &self.url } + + pub fn remote_name(&self) -> Option<&str> { + self.remote_name.as_deref() + } } fn fallback_host() -> Option<Url> { |