mirror of
https://codeberg.org/Cyborus/forgejo-cli.git
synced 2025-03-15 21:45:54 +01:00
feat: creating a pr with agit
This commit is contained in:
parent
884778f401
commit
07436b556b
2 changed files with 168 additions and 44 deletions
131
src/prs.rs
131
src/prs.rs
|
@ -48,7 +48,7 @@ pub enum PrSubcommand {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
base: Option<String>,
|
base: Option<String>,
|
||||||
/// The branch to pull changes from.
|
/// The branch to pull changes from.
|
||||||
#[clap(long)]
|
#[clap(long, group = "source")]
|
||||||
head: Option<String>,
|
head: Option<String>,
|
||||||
/// What to name the new pull request.
|
/// What to name the new pull request.
|
||||||
///
|
///
|
||||||
|
@ -64,8 +64,11 @@ pub enum PrSubcommand {
|
||||||
#[clap(long, short, id = "[HOST/]OWNER/REPO")]
|
#[clap(long, short, id = "[HOST/]OWNER/REPO")]
|
||||||
repo: Option<RepoArg>,
|
repo: Option<RepoArg>,
|
||||||
/// Open the PR creation menu in your web browser
|
/// 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,
|
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 the contents of a pull request
|
||||||
View {
|
View {
|
||||||
|
@ -271,9 +274,10 @@ pub enum ViewCommand {
|
||||||
impl PrCommand {
|
impl PrCommand {
|
||||||
pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
|
pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> {
|
||||||
use PrSubcommand::*;
|
use PrSubcommand::*;
|
||||||
let repo = RepoInfo::get_current(host_name, self.repo(), self.remote.as_deref(), &keys)?;
|
let repo_info =
|
||||||
let api = keys.get_api(repo.host_url()).await?;
|
RepoInfo::get_current(host_name, self.repo(), self.remote.as_deref(), &keys)?;
|
||||||
let repo = repo.name().ok_or_else(|| self.no_repo_error())?;
|
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 {
|
match self.command {
|
||||||
Create {
|
Create {
|
||||||
title,
|
title,
|
||||||
|
@ -282,7 +286,21 @@ impl PrCommand {
|
||||||
body,
|
body,
|
||||||
repo: _,
|
repo: _,
|
||||||
web,
|
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 {
|
Merge {
|
||||||
pr,
|
pr,
|
||||||
method,
|
method,
|
||||||
|
@ -893,11 +911,14 @@ async fn create_pr(
|
||||||
head: Option<String>,
|
head: Option<String>,
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
web: bool,
|
web: bool,
|
||||||
|
agit: bool,
|
||||||
|
remote_name: Option<&str>,
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
let mut repo_data = api.repo_get(repo.owner(), repo.name()).await?;
|
let mut repo_data = api.repo_get(repo.owner(), repo.name()).await?;
|
||||||
|
|
||||||
let head = match head {
|
let head = match head {
|
||||||
Some(head) => head,
|
_ if agit => None,
|
||||||
|
Some(head) => Some(head),
|
||||||
None => {
|
None => {
|
||||||
let local_repo = git2::Repository::open(".")?;
|
let local_repo = git2::Repository::open(".")?;
|
||||||
let head = local_repo.head()?;
|
let head = local_repo.head()?;
|
||||||
|
@ -910,9 +931,15 @@ async fn create_pr(
|
||||||
.name()
|
.name()
|
||||||
.ok_or_eyre("current branch does not have utf8 name")?;
|
.ok_or_eyre("current branch does not have utf8 name")?;
|
||||||
let upstream_remote = local_repo.branch_upstream_remote(branch_ref)?;
|
let upstream_remote = local_repo.branch_upstream_remote(branch_ref)?;
|
||||||
let remote_name = upstream_remote
|
|
||||||
|
let remote_name = if let Some(remote_name) = remote_name {
|
||||||
|
remote_name
|
||||||
|
} else {
|
||||||
|
let upstream_name = upstream_remote
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_eyre("remote does not have utf8 name")?;
|
.ok_or_eyre("remote does not have utf8 name")?;
|
||||||
|
upstream_name
|
||||||
|
};
|
||||||
|
|
||||||
let remote = local_repo.find_remote(remote_name)?;
|
let remote = local_repo.find_remote(remote_name)?;
|
||||||
let remote_url_s = remote.url().ok_or_eyre("remote does not have utf8 url")?;
|
let remote_url_s = remote.url().ok_or_eyre("remote does not have utf8 url")?;
|
||||||
|
@ -939,11 +966,13 @@ async fn create_pr(
|
||||||
let upstream_branch = upstream_branch
|
let upstream_branch = upstream_branch
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_eyre("remote branch does not have utf8 name")?;
|
.ok_or_eyre("remote branch does not have utf8 name")?;
|
||||||
|
Some(
|
||||||
upstream_branch
|
upstream_branch
|
||||||
.rsplit_once("/")
|
.rsplit_once("/")
|
||||||
.map(|(_, b)| b)
|
.map(|(_, b)| b)
|
||||||
.unwrap_or(upstream_branch)
|
.unwrap_or(upstream_branch)
|
||||||
.to_owned()
|
.to_owned(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -979,7 +1008,7 @@ async fn create_pr(
|
||||||
parent_owner,
|
parent_owner,
|
||||||
parent_name,
|
parent_name,
|
||||||
parent_repo,
|
parent_repo,
|
||||||
format!("{}:{}", repo.owner(), head),
|
head.map(|head| format!("{}:{}", repo.owner(), head)),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
|
@ -1000,6 +1029,8 @@ async fn create_pr(
|
||||||
};
|
};
|
||||||
|
|
||||||
if web {
|
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
|
let mut pr_create_url = base_repo
|
||||||
.html_url
|
.html_url
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -1019,6 +1050,8 @@ async fn create_pr(
|
||||||
body
|
body
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
match head {
|
||||||
|
Some(head) => {
|
||||||
let pr = api
|
let pr = api
|
||||||
.repo_create_pull_request(
|
.repo_create_pull_request(
|
||||||
&repo_owner,
|
&repo_owner,
|
||||||
|
@ -1045,6 +1078,84 @@ async fn create_pr(
|
||||||
.ok_or_else(|| eyre::eyre!("pr does not have title"))?;
|
.ok_or_else(|| eyre::eyre!("pr does not have title"))?;
|
||||||
println!("created pull request #{}: {}", number, 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
19
src/repo.rs
19
src/repo.rs
|
@ -10,6 +10,7 @@ use crate::SpecialRender;
|
||||||
pub struct RepoInfo {
|
pub struct RepoInfo {
|
||||||
url: Url,
|
url: Url,
|
||||||
name: Option<RepoName>,
|
name: Option<RepoName>,
|
||||||
|
remote_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepoInfo {
|
impl RepoInfo {
|
||||||
|
@ -73,6 +74,8 @@ impl RepoInfo {
|
||||||
.map(|url| keys.deref_alias(url))
|
.map(|url| keys.deref_alias(url))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut final_remote_name = None;
|
||||||
|
|
||||||
let (remote_url, remote_repo_name) = {
|
let (remote_url, remote_repo_name) = {
|
||||||
let mut out = (None, None);
|
let mut out = (None, None);
|
||||||
if let Ok(local_repo) = git2::Repository::open(".") {
|
if let Ok(local_repo) = git2::Repository::open(".") {
|
||||||
|
@ -143,9 +146,11 @@ impl RepoInfo {
|
||||||
if let Ok(remote) = local_repo.find_remote(&name) {
|
if let Ok(remote) = local_repo.find_remote(&name) {
|
||||||
let url_s = std::str::from_utf8(remote.url_bytes())?;
|
let url_s = std::str::from_utf8(remote.url_bytes())?;
|
||||||
let url = keys.deref_alias(crate::ssh_url_parse(url_s)?);
|
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(name))
|
out = (Some(url), Some(repo_name));
|
||||||
|
|
||||||
|
final_remote_name = Some(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -177,7 +182,11 @@ impl RepoInfo {
|
||||||
});
|
});
|
||||||
|
|
||||||
let info = match (url, name) {
|
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, Some(_)) => eyre::bail!("cannot find repo, no host specified"),
|
||||||
(None, None) => eyre::bail!("no repo info specified"),
|
(None, None) => eyre::bail!("no repo info specified"),
|
||||||
};
|
};
|
||||||
|
@ -192,6 +201,10 @@ impl RepoInfo {
|
||||||
pub fn host_url(&self) -> &Url {
|
pub fn host_url(&self) -> &Url {
|
||||||
&self.url
|
&self.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remote_name(&self) -> Option<&str> {
|
||||||
|
self.remote_name.as_deref()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fallback_host() -> Option<Url> {
|
fn fallback_host() -> Option<Url> {
|
||||||
|
|
Loading…
Add table
Reference in a new issue