diff --git a/CHANGELOG.md b/CHANGELOG.md index 0153a8f735..b5c02c2000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * After commit: jump back to unstaged area [[@tommady](https://github.com/tommady)] ([#2476](https://github.com/extrawurst/gitui/issues/2476)) * The default key to close the commit error message popup is now the Escape key [[@wessamfathi](https://github.com/wessamfathi)] ([#2552](https://github.com/extrawurst/gitui/issues/2552)) * use OSC52 copying in case other methods fail [[@naseschwarz](https://github.com/naseschwarz)] ([#2366](https://github.com/gitui-org/gitui/issues/2366)) +* push: respect `branch.*.merge` when push default is upstream [[@vlad-anger](https://github.com/vlad-anger)] ([#2542](https://github.com/gitui-org/gitui/pull/2542)) ## [0.27.0] - 2024-01-14 diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs index 37fba9fa91..8bd730096e 100644 --- a/asyncgit/src/error.rs +++ b/asyncgit/src/error.rs @@ -53,6 +53,10 @@ pub enum Error { #[error("git error:{0}")] Git(#[from] git2::Error), + /// + #[error("git config error: {0}")] + GitConfig(String), + /// #[error("strip prefix error: {0}")] StripPrefix(#[from] StripPrefixError), diff --git a/asyncgit/src/sync/branch/mod.rs b/asyncgit/src/sync/branch/mod.rs index 1a67390789..93185d6f45 100644 --- a/asyncgit/src/sync/branch/mod.rs +++ b/asyncgit/src/sync/branch/mod.rs @@ -243,6 +243,25 @@ pub fn get_branch_remote( } } +/// Retrieve the upstream merge of a local `branch`, +/// configured in "branch.*.merge" +/// +/// For details check git2 `branch_upstream_merge` +pub fn get_branch_upstream_merge( + repo_path: &RepoPath, + branch: &str, +) -> Result> { + let repo = repo(repo_path)?; + let branch = repo.find_branch(branch, BranchType::Local)?; + let reference = bytes2string(branch.get().name_bytes())?; + let remote_name = repo.branch_upstream_merge(&reference).ok(); + if let Some(remote_name) = remote_name { + Ok(Some(bytes2string(remote_name.as_ref())?)) + } else { + Ok(None) + } +} + /// returns whether the pull merge strategy is set to rebase pub fn config_is_pull_rebase(repo_path: &RepoPath) -> Result { let repo = repo(repo_path)?; @@ -673,6 +692,49 @@ mod tests_branches { assert!(get_branch_remote(repo_path, "foo").is_err()); } + + #[test] + fn test_branch_no_upstream_merge_config() { + let (_r, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path: &RepoPath = + &root.as_os_str().to_str().unwrap().into(); + + let upstream_merge_res = + get_branch_upstream_merge(&repo_path, "master"); + assert!( + upstream_merge_res.is_ok_and(|v| v.as_ref().is_none()) + ); + } + + #[test] + fn test_branch_with_upstream_merge_config() { + let (_r, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path: &RepoPath = + &root.as_os_str().to_str().unwrap().into(); + + let branch_name = "master"; + let upstrem_merge = "refs/heads/master"; + + let mut config = repo.config().unwrap(); + config + .set_str( + &format!("branch.{branch_name}.merge"), + &upstrem_merge, + ) + .expect("fail set branch merge config"); + + let upstream_merge_res = + get_branch_upstream_merge(&repo_path, &branch_name); + assert!(upstream_merge_res + .as_ref() + .is_ok_and(|v| v.as_ref().is_some())); + assert_eq!( + &upstream_merge_res.unwrap().unwrap(), + upstrem_merge + ); + } } #[cfg(test)] diff --git a/asyncgit/src/sync/config.rs b/asyncgit/src/sync/config.rs index 60d0b409a5..17a62b6736 100644 --- a/asyncgit/src/sync/config.rs +++ b/asyncgit/src/sync/config.rs @@ -62,6 +62,52 @@ pub fn untracked_files_config_repo( Ok(ShowUntrackedFilesConfig::All) } +// see https://git-scm.com/docs/git-config#Documentation/git-config.txt-pushdefault +/// represents `push.default` git config +#[derive(PartialEq, Eq)] +pub enum PushDefaultStrategyConfig { + Nothing, + Current, + Upstream, + Simple, + Matching, +} + +impl Default for PushDefaultStrategyConfig { + fn default() -> Self { + Self::Simple + } +} + +impl<'a> TryFrom<&'a str> for PushDefaultStrategyConfig { + type Error = crate::Error; + fn try_from( + value: &'a str, + ) -> std::result::Result { + match value { + "nothing" => Ok(Self::Nothing), + "current" => Ok(Self::Current), + "upstream" | "tracking" => Ok(Self::Upstream), + "simple" => Ok(Self::Simple), + "matching" => Ok(Self::Matching), + _ => Err(crate::Error::GitConfig(format!( + "malformed value for push.default: {value}, must be one of nothing, matching, simple, upstream or current" + ))), + } + } +} + +pub fn push_default_strategy_config_repo( + repo: &Repository, +) -> Result { + (get_config_string_repo(repo, "push.default")?).map_or_else( + || Ok(PushDefaultStrategyConfig::default()), + |entry_str| { + PushDefaultStrategyConfig::try_from(entry_str.as_str()) + }, + ) +} + /// pub fn untracked_files_config( repo_path: &RepoPath, diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 3c4b106c3e..c52a556aad 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -39,7 +39,7 @@ pub use blame::{blame_file, BlameHunk, FileBlame}; pub use branch::{ branch_compare_upstream, checkout_branch, checkout_commit, config_is_pull_rebase, create_branch, delete_branch, - get_branch_remote, get_branches_info, + get_branch_remote, get_branch_upstream_merge, get_branches_info, merge_commit::merge_upstream_commit, merge_ff::branch_merge_upstream_fastforward, merge_rebase::merge_upstream_rebase, rename::rename_branch, diff --git a/asyncgit/src/sync/remotes/push.rs b/asyncgit/src/sync/remotes/push.rs index 97b1f7fc5f..b28b8a22d3 100644 --- a/asyncgit/src/sync/remotes/push.rs +++ b/asyncgit/src/sync/remotes/push.rs @@ -3,7 +3,12 @@ use crate::{ progress::ProgressPercent, sync::{ branch::branch_set_upstream_after_push, + config::{ + push_default_strategy_config_repo, + PushDefaultStrategyConfig, + }, cred::BasicAuthCredential, + get_branch_upstream_merge, remotes::{proxy_auto, Callbacks}, repository::repo, CommitId, RepoPath, @@ -92,7 +97,7 @@ impl AsyncProgress for ProgressNotification { } /// -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PushType { /// Branch, @@ -145,6 +150,9 @@ pub fn push_raw( let repo = repo(repo_path)?; let mut remote = repo.find_remote(remote)?; + let push_default_strategy = + push_default_strategy_config_repo(&repo)?; + let mut options = PushOptions::new(); options.proxy_options(proxy_auto()); @@ -158,14 +166,28 @@ pub fn push_raw( (true, false) => "+", (false, false) => "", }; - let ref_type = match ref_type { + let git_ref_type = match ref_type { PushType::Branch => "heads", PushType::Tag => "tags", }; - let branch_name = - format!("{branch_modifier}refs/{ref_type}/{branch}"); - remote.push(&[branch_name.as_str()], Some(&mut options))?; + let mut push_ref = + format!("{branch_modifier}refs/{git_ref_type}/{branch}"); + + if !delete + && ref_type == PushType::Branch + && push_default_strategy + == PushDefaultStrategyConfig::Upstream + { + if let Ok(Some(branch_upstream_merge)) = + get_branch_upstream_merge(repo_path, branch) + { + push_ref.push_str(&format!(":{branch_upstream_merge}")); + } + } + + log::debug!("push to: {push_ref}"); + remote.push(&[push_ref], Some(&mut options))?; if let Some((reference, msg)) = callbacks.get_stats()?.push_rejected_msg