summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCyborus <cyborus@noreply.codeberg.org>2024-11-05 20:02:31 +0100
committerCyborus <cyborus@noreply.codeberg.org>2024-11-05 20:02:31 +0100
commit23137cb2ec1fa84b953fd824cf59d522e1ed29df (patch)
tree94eeb77ba35329ba9bb685fe62b7c00562b23706
parentMerge pull request 'fix ssh url parsing' (#141) from ssh-parse-mistake into main (diff)
parentchore: format (diff)
downloadforgejo-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.rs338
-rw-r--r--src/repo.rs19
2 files changed, 256 insertions, 101 deletions
diff --git a/src/prs.rs b/src/prs.rs
index 98d54cd..43bfbf5 100644
--- a/src/prs.rs
+++ b/src/prs.rs
@@ -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> {