diff --git a/src/events/git_push.rs b/src/events/git_push.rs index 39a1549..23e750e 100644 --- a/src/events/git_push.rs +++ b/src/events/git_push.rs @@ -4,10 +4,11 @@ use super::AtomicEvent; use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; use git2::{Cred, CredentialType, Repository}; +use log::debug; pub struct GitPush { pub pre_check_rules: Vec>, - pub force: bool, + pub force_with_lease: bool, pub set_upstream: bool, } @@ -18,7 +19,7 @@ impl AtomicEvent for GitPush { { GitPush { pre_check_rules: vec![], - force: false, + force_with_lease: false, set_upstream: false, } } @@ -89,21 +90,24 @@ impl AtomicEvent for GitPush { // Prepare push options with authentication let mut push_options = Self::create_push_options(); - // Check if we need to force push and validate state - if !self.force { + // Validation + if self.force_with_lease { + self.validate_force_with_lease(&repo, &head, branch_name)?; + } else { self.validate_push_safety(&repo, &head, branch_name)?; } - // Determine refspec let refspec = if self.set_upstream { format!("refs/heads/{}:refs/heads/{}", branch_name, branch_name) } else { format!("refs/heads/{}", branch_name) }; - // Perform the push - let refspecs = if self.force { - vec![format!("+{}", refspec)] + // Perform the push with force-with-lease if enabled + let refspecs = if self.force_with_lease { + let force_lease_refspec = + self.build_force_with_lease_refspec(&repo, branch_name, &refspec)?; + vec![force_lease_refspec] } else { vec![refspec] }; @@ -131,23 +135,96 @@ impl AtomicEvent for GitPush { } impl GitPush { - pub fn set_force(&mut self, force: bool) -> &mut Self { - self.force = force; + pub fn with_force_with_lease(&mut self, force_with_lease: bool) -> &mut Self { + self.force_with_lease = force_with_lease; self } - pub fn set_upstream_flag(&mut self, set_upstream: bool) -> &mut Self { + pub fn with_upstream_flag(&mut self, set_upstream: bool) -> &mut Self { self.set_upstream = set_upstream; self } + /// Validate force-with-lease conditions + fn validate_force_with_lease( + &self, + repo: &Repository, + head: &git2::Reference, + branch_name: &str, + ) -> Result<(), Box> { + let local_commit = head.peel_to_commit().map_err(|e| { + Box::new(BGitError::new( + "BGitError", + &format!("Failed to get local commit: {}", e), + BGitErrorWorkflowType::AtomicEvent, + NO_STEP, + self.get_name(), + NO_RULE, + )) + })?; + + // Check if remote branch exists and validate + if let Ok(remote_ref) = repo.find_reference(&format!("refs/remotes/origin/{}", branch_name)) + { + let remote_commit = remote_ref.peel_to_commit().map_err(|e| { + Box::new(BGitError::new( + "BGitError", + &format!("Failed to get remote commit: {}", e), + BGitErrorWorkflowType::AtomicEvent, + NO_STEP, + self.get_name(), + NO_RULE, + )) + })?; + + if local_commit.id() == remote_commit.id() { + debug!("Local branch is up to date with remote, no force-with-lease needed"); + return Ok(()); + } + } + + Ok(()) + } + + fn build_force_with_lease_refspec( + &self, + repo: &Repository, + branch_name: &str, + base_refspec: &str, + ) -> Result> { + // Force-with-lease using the current remote tracking branch as the expected value + if let Ok(remote_ref) = repo.find_reference(&format!("refs/remotes/origin/{}", branch_name)) + { + let remote_oid = remote_ref.target().ok_or_else(|| { + Box::new(BGitError::new( + "BGitError", + "Failed to get remote reference target for force-with-lease", + BGitErrorWorkflowType::AtomicEvent, + NO_STEP, + self.get_name(), + NO_RULE, + )) + })?; + + Ok(format!("+{}^{{{}}}", base_refspec, remote_oid)) + } else { + Err(Box::new(BGitError::new( + "BGitError", + "Cannot perform force-with-lease: no remote tracking branch found", + BGitErrorWorkflowType::AtomicEvent, + NO_STEP, + self.get_name(), + NO_RULE, + ))) + } + } + fn validate_push_safety( &self, repo: &Repository, head: &git2::Reference, branch_name: &str, ) -> Result<(), Box> { - // Check if we're up to date with remote if let Ok(remote_ref) = repo.find_reference(&format!("refs/remotes/origin/{}", branch_name)) { let local_commit = head.peel_to_commit().map_err(|e| { @@ -194,7 +271,7 @@ impl GitPush { if merge_base == local_commit.id() && local_commit.id() != remote_commit.id() { return Err(Box::new(BGitError::new( "BGitError", - "Local branch is behind remote. Pull changes first or use --force", + "Local branch is behind remote. Pull changes first", BGitErrorWorkflowType::AtomicEvent, NO_STEP, self.get_name(), diff --git a/src/workflows/default/action/ta09_pull_push.rs b/src/workflows/default/action/ta09_pull_push.rs index e20e42e..dcf3055 100644 --- a/src/workflows/default/action/ta09_pull_push.rs +++ b/src/workflows/default/action/ta09_pull_push.rs @@ -40,7 +40,9 @@ impl ActionStep for PullAndPush { // Pull successful, now attempt push let mut git_push = GitPush::new(); // Configure push options - you can customize these as needed - git_push.set_force(false).set_upstream_flag(false); + git_push + .with_force_with_lease(false) + .with_upstream_flag(false); match git_push.execute() { Ok(_) => {