Get default push remote from configuration (#2156)

This commit is contained in:
Christoph Rüßler 2024-04-14 12:12:09 +02:00 committed by GitHub
parent fe05f93a74
commit b08eddb45b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 229 additions and 29 deletions

View file

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* support `core.commitChar` filtering [[@concelare](https://github.com/concelare)] ([#2136](https://github.com/extrawurst/gitui/issues/2136))
* allow reset in branch popup ([#2170](https://github.com/extrawurst/gitui/issues/2170))
* support ssh commit signing (when `user.signingKey` and `gpg.format = ssh` of gitconfig are set; ssh-agent isn't yet supported) [[@yanganto](https://github.com/yanganto)] ([#1149](https://github.com/extrawurst/gitui/issues/1149))
* respect configuration for remote when pushing [[@cruessler](https://github.com/cruessler)]
### Changed
* Make info and error message popups scrollable [[@MichaelAug](https://github.com/MichaelAug)] ([#1138](https://github.com/extrawurst/gitui/issues/1138))

View file

@ -5,13 +5,13 @@ pub mod merge_ff;
pub mod merge_rebase;
pub mod rename;
use super::{
remotes::get_default_remote_in_repo, utils::bytes2string,
RepoPath,
};
use super::{utils::bytes2string, RepoPath};
use crate::{
error::{Error, Result},
sync::{repository::repo, utils::get_head_repo, CommitId},
sync::{
remotes::get_default_remote_for_push_in_repo,
repository::repo, utils::get_head_repo, CommitId,
},
};
use git2::{Branch, BranchType, Repository};
use scopetime::scope_time;
@ -209,7 +209,7 @@ pub struct BranchCompare {
}
///
pub(crate) fn branch_set_upstream(
pub(crate) fn branch_set_upstream_after_push(
repo: &Repository,
branch_name: &str,
) -> Result<()> {
@ -219,7 +219,7 @@ pub(crate) fn branch_set_upstream(
repo.find_branch(branch_name, BranchType::Local)?;
if branch.upstream().is_err() {
let remote = get_default_remote_in_repo(repo)?;
let remote = get_default_remote_for_push_in_repo(repo)?;
let upstream_name = format!("{remote}/{branch_name}");
branch.set_upstream(Some(upstream_name.as_str()))?;
}

View file

@ -1,7 +1,12 @@
//! credentials git helper
use super::{
remotes::get_default_remote_in_repo, repository::repo, RepoPath,
remotes::{
get_default_remote_for_push_in_repo,
get_default_remote_in_repo,
},
repository::repo,
RepoPath,
};
use crate::error::{Error, Result};
use git2::CredentialHelper;
@ -43,6 +48,22 @@ pub fn need_username_password(repo_path: &RepoPath) -> Result<bool> {
Ok(is_http)
}
/// know if username and password are needed for this url
pub fn need_username_password_for_push(
repo_path: &RepoPath,
) -> Result<bool> {
let repo = repo(repo_path)?;
let remote = repo
.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?;
let url = remote
.pushurl()
.or_else(|| remote.url())
.ok_or(Error::UnknownRemote)?
.to_owned();
let is_http = url.starts_with("http");
Ok(is_http)
}
/// extract username and password
pub fn extract_username_password(
repo_path: &RepoPath,
@ -71,6 +92,34 @@ pub fn extract_username_password(
})
}
/// extract username and password
pub fn extract_username_password_for_push(
repo_path: &RepoPath,
) -> Result<BasicAuthCredential> {
let repo = repo(repo_path)?;
let url = repo
.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?
.url()
.ok_or(Error::UnknownRemote)?
.to_owned();
let mut helper = CredentialHelper::new(&url);
//TODO: look at Cred::credential_helper,
//if the username is in the url we need to set it here,
//I dont think `config` will pick it up
if let Ok(config) = repo.config() {
helper.config(&config);
}
Ok(match helper.execute() {
Some((username, password)) => {
BasicAuthCredential::new(Some(username), Some(password))
}
None => extract_cred_from_url(&url),
})
}
/// extract credentials from url
pub fn extract_cred_from_url(url: &str) -> BasicAuthCredential {
url::Url::parse(url).map_or_else(

View file

@ -79,8 +79,8 @@ pub use merge::{
};
pub use rebase::rebase_branch;
pub use remotes::{
get_default_remote, get_remotes, push::AsyncProgress,
tags::PushTagsProgress,
get_default_remote, get_default_remote_for_push, get_remotes,
push::AsyncProgress, tags::PushTagsProgress,
};
pub(crate) use repository::repo;
pub use repository::{RepoPath, RepoPathRef};

View file

@ -51,6 +51,81 @@ pub fn get_default_remote(repo_path: &RepoPath) -> Result<String> {
get_default_remote_in_repo(&repo)
}
/// Gets the current branch the user is on.
/// Returns none if they are not on a branch
/// and Err if there was a problem finding the branch
fn get_current_branch(
repo: &Repository,
) -> Result<Option<git2::Branch>> {
for b in repo.branches(None)? {
let branch = b?.0;
if branch.is_head() {
return Ok(Some(branch));
}
}
Ok(None)
}
/// Tries to find the default repo to push to based on configuration.
///
/// > remote.pushDefault
/// >
/// > The remote to push to by default. Overrides `branch.<name>.remote` for all branches, and is
/// > overridden by `branch.<name>.pushRemote` for specific branches.
///
/// > branch.<name>.remote
/// >
/// > When on branch `<name>`, it tells `git fetch` and `git push` which remote to fetch from or
/// > push to. The remote to push to may be overridden with `remote.pushDefault` (for all
/// > branches). The remote to push to, for the current branch, may be further overridden by
/// > `branch.<name>.pushRemote`. If no remote is configured, or if you are not on any branch and
/// > there is more than one remote defined in the repository, it defaults to `origin` for fetching
/// > and `remote.pushDefault` for pushing.
///
/// [git-config-remote-push-default]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-remotepushDefault
/// [git-config-branch-name-remote]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote
///
/// Falls back to `get_default_remote_in_repo`.
pub fn get_default_remote_for_push(
repo_path: &RepoPath,
) -> Result<String> {
let repo = repo(repo_path)?;
get_default_remote_for_push_in_repo(&repo)
}
pub(crate) fn get_default_remote_for_push_in_repo(
repo: &Repository,
) -> Result<String> {
scope_time!("get_default_remote_for_push_in_repo");
let config = repo.config()?;
let branch = get_current_branch(repo)?;
if let Some(branch) = branch {
let remote_name = bytes2string(branch.name_bytes()?)?;
let entry_name =
format!("branch.{}.pushRemote", &remote_name);
if let Ok(entry) = config.get_entry(&entry_name) {
return bytes2string(entry.value_bytes());
}
if let Ok(entry) = config.get_entry("remote.pushDefault") {
return bytes2string(entry.value_bytes());
}
let entry_name = format!("branch.{}.remote", &remote_name);
if let Ok(entry) = config.get_entry(&entry_name) {
return bytes2string(entry.value_bytes());
}
}
get_default_remote_in_repo(repo)
}
/// see `get_default_remote`
pub(crate) fn get_default_remote_in_repo(
repo: &Repository,
@ -299,9 +374,68 @@ mod tests {
]
);
let res =
let default_remote =
get_default_remote_in_repo(&repo(repo_path).unwrap());
assert_eq!(res.is_err(), true);
assert!(matches!(res, Err(Error::NoDefaultRemoteFound)));
assert!(matches!(
default_remote,
Err(Error::NoDefaultRemoteFound)
));
}
#[test]
fn test_default_remote_for_push() {
let (remote_dir, _remote) = repo_init().unwrap();
let remote_path = remote_dir.path().to_str().unwrap();
let (repo_dir, repo) = repo_clone(remote_path).unwrap();
let repo_path: &RepoPath = &repo_dir
.into_path()
.as_os_str()
.to_str()
.unwrap()
.into();
debug_cmd_print(
repo_path,
"git remote rename origin alternate",
);
debug_cmd_print(
repo_path,
&format!("git remote add someremote {remote_path}")[..],
);
let mut config = repo.config().unwrap();
config
.set_str("branch.master.remote", "branchremote")
.unwrap();
let default_push_remote =
get_default_remote_for_push_in_repo(&repo);
assert!(
matches!(default_push_remote, Ok(remote_name) if remote_name == "branchremote")
);
config.set_str("remote.pushDefault", "pushdefault").unwrap();
let default_push_remote =
get_default_remote_for_push_in_repo(&repo);
assert!(
matches!(default_push_remote, Ok(remote_name) if remote_name == "pushdefault")
);
config
.set_str("branch.master.pushRemote", "branchpushremote")
.unwrap();
let default_push_remote =
get_default_remote_for_push_in_repo(&repo);
assert!(
matches!(default_push_remote, Ok(remote_name) if remote_name == "branchpushremote")
);
}
}

View file

@ -2,7 +2,7 @@ use crate::{
error::{Error, Result},
progress::ProgressPercent,
sync::{
branch::branch_set_upstream,
branch::branch_set_upstream_after_push,
cred::BasicAuthCredential,
remotes::{proxy_auto, Callbacks},
repository::repo,
@ -176,7 +176,7 @@ pub fn push_raw(
}
if !delete {
branch_set_upstream(&repo, branch)?;
branch_set_upstream_after_push(&repo, branch)?;
}
Ok(())

View file

@ -13,10 +13,12 @@ use anyhow::Result;
use asyncgit::{
sync::{
cred::{
extract_username_password, need_username_password,
BasicAuthCredential,
extract_username_password_for_push,
need_username_password_for_push, BasicAuthCredential,
},
get_branch_remote, get_default_remote, RepoPathRef,
get_branch_remote,
remotes::get_default_remote_for_push,
RepoPathRef,
},
AsyncGitNotification, AsyncPush, PushRequest, PushType,
RemoteProgress, RemoteProgressState,
@ -104,11 +106,11 @@ impl PushPopup {
self.show()?;
if need_username_password(&self.repo.borrow())? {
let cred = extract_username_password(&self.repo.borrow())
.unwrap_or_else(|_| {
BasicAuthCredential::new(None, None)
});
if need_username_password_for_push(&self.repo.borrow())? {
let cred = extract_username_password_for_push(
&self.repo.borrow(),
)
.unwrap_or_else(|_| BasicAuthCredential::new(None, None));
if cred.is_complete() {
self.push_to_remote(Some(cred), force)
} else {
@ -132,7 +134,8 @@ impl PushPopup {
remote
} else {
log::info!("push: branch '{}' has no upstream - looking up default remote",self.branch);
let remote = get_default_remote(&self.repo.borrow())?;
let remote =
get_default_remote_for_push(&self.repo.borrow())?;
log::info!(
"push: branch '{}' to remote '{}'",
self.branch,

View file

@ -57,6 +57,11 @@ enum DiffTarget {
WorkingDir,
}
struct RemoteStatus {
has_remotes: bool,
has_remote_for_push: bool,
}
pub struct Status {
repo: RepoPathRef,
visible: bool,
@ -65,8 +70,8 @@ pub struct Status {
index: ChangesComponent,
index_wd: ChangesComponent,
diff: DiffComponent,
remotes: RemoteStatus,
git_diff: AsyncDiff,
has_remotes: bool,
git_state: RepoState,
git_status_workdir: AsyncStatus,
git_status_stage: AsyncStatus,
@ -155,7 +160,10 @@ impl Status {
Self {
queue: env.queue.clone(),
visible: true,
has_remotes: false,
remotes: RemoteStatus {
has_remotes: false,
has_remote_for_push: false,
},
git_state: RepoState::Clean,
focus: Focus::WorkDir,
diff_target: DiffTarget::WorkingDir,
@ -407,9 +415,14 @@ impl Status {
}
fn check_remotes(&mut self) {
self.has_remotes =
self.remotes.has_remotes =
sync::get_default_remote(&self.repo.borrow().clone())
.is_ok();
self.remotes.has_remote_for_push =
sync::get_default_remote_for_push(
&self.repo.borrow().clone(),
)
.is_ok();
}
///
@ -600,11 +613,11 @@ impl Status {
.as_ref()
.map_or(true, |state| state.ahead > 0);
is_ahead && self.has_remotes
is_ahead && self.remotes.has_remote_for_push
}
const fn can_pull(&self) -> bool {
self.has_remotes && self.git_branch_state.is_some()
self.remotes.has_remotes && self.git_branch_state.is_some()
}
fn can_abort_merge(&self) -> bool {