@@ -579,8 +579,15 @@ func (s *GitHTTPServer) handleReceivePack(w http.ResponseWriter, r *http.Request
579579 }
580580
581581 // Detect pushed branches by comparing before/after
582+ // Returns map[branch]isForce where isForce=true means force push detected
582583 branchesAfter := s .getBranchHashes (repoPath )
583- pushedBranches := s .detectChangedBranches (branchesBefore , branchesAfter )
584+ pushedBranchesMap := s .detectChangedBranches (repoPath , branchesBefore , branchesAfter )
585+
586+ // Extract branch names for logging
587+ pushedBranches := make ([]string , 0 , len (pushedBranchesMap ))
588+ for branch := range pushedBranchesMap {
589+ pushedBranches = append (pushedBranches , branch )
590+ }
584591
585592 log .Info ().Str ("repo_id" , repoID ).Strs ("pushed_branches" , pushedBranches ).Msg ("Receive-pack completed" )
586593
@@ -594,24 +601,24 @@ func (s *GitHTTPServer) handleReceivePack(w http.ResponseWriter, r *http.Request
594601 // headers have already been sent, so we cannot signal upstream push failures to the
595602 // client - they will see success regardless. If upstream push fails, we rollback
596603 // locally but the agent won't know. This is a known architectural limitation.
597- if len (pushedBranches ) > 0 && repo != nil && repo .ExternalURL != "" {
598- log .Debug ().Str ("repo_id" , repoID ).Int ("branch_count" , len (pushedBranches )).Msg ("Starting external push with detached context" )
604+ if len (pushedBranchesMap ) > 0 && repo != nil && repo .ExternalURL != "" {
605+ log .Debug ().Str ("repo_id" , repoID ).Int ("branch_count" , len (pushedBranchesMap )).Msg ("Starting external push with detached context" )
599606
600607 upstreamPushFailed := false
601- for _ , branch := range pushedBranches {
608+ for branch , isForce := range pushedBranchesMap {
602609 // Create per-branch timeout so later branches don't get starved
603610 branchCtx , branchCancel := context .WithTimeout (context .Background (), 90 * time .Second )
604611
605- log .Info ().Str ("repo_id" , repoID ).Str ("branch" , branch ).Msg ("Pushing branch to upstream" )
606- err := s .gitRepoService .PushBranchToRemote (branchCtx , repoID , branch , false )
612+ log .Info ().Str ("repo_id" , repoID ).Str ("branch" , branch ).Bool ( "force" , isForce ). Msg ("Pushing branch to upstream" )
613+ err := s .gitRepoService .PushBranchToRemote (branchCtx , repoID , branch , isForce )
607614 branchCancel ()
608615
609616 if err != nil {
610- log .Error ().Err (err ).Str ("repo_id" , repoID ).Str ("branch" , branch ).Msg ("Failed to push branch to upstream - rolling back" )
617+ log .Error ().Err (err ).Str ("repo_id" , repoID ).Str ("branch" , branch ).Bool ( "force" , isForce ). Msg ("Failed to push branch to upstream - rolling back" )
611618 upstreamPushFailed = true
612619 break
613620 }
614- log .Info ().Str ("repo_id" , repoID ).Str ("branch" , branch ).Msg ("Successfully pushed branch to upstream" )
621+ log .Info ().Str ("repo_id" , repoID ).Str ("branch" , branch ).Bool ( "force" , isForce ). Msg ("Successfully pushed branch to upstream" )
615622 }
616623
617624 if upstreamPushFailed {
@@ -670,15 +677,35 @@ func (s *GitHTTPServer) getBranchHashes(repoPath string) map[string]string {
670677 return result
671678}
672679
673- // detectChangedBranches compares before/after branch hashes to find changed branches
674- func (s * GitHTTPServer ) detectChangedBranches (before , after map [string ]string ) []string {
675- var changed []string
676- for branch , hash := range after {
677- if beforeHash , exists := before [branch ]; ! exists || beforeHash != hash {
678- changed = append (changed , branch )
680+ // detectChangedBranches compares before/after branch hashes to find changed branches.
681+ // Returns a map of branch name -> isForce (true if force push detected).
682+ // A force push is detected when the old commit is NOT an ancestor of the new commit.
683+ func (s * GitHTTPServer ) detectChangedBranches (repoPath string , before , after map [string ]string ) map [string ]bool {
684+ result := make (map [string ]bool )
685+ for branch , newHash := range after {
686+ oldHash , existed := before [branch ]
687+ if ! existed || oldHash != newHash {
688+ isForce := false
689+ if existed && oldHash != "" {
690+ // Check if old commit is ancestor of new commit (fast-forward)
691+ // If NOT ancestor, this is a force push
692+ _ , _ , err := gitcmd .NewCommand ("merge-base" , "--is-ancestor" ).
693+ AddDynamicArguments (oldHash , newHash ).
694+ RunStdString (context .Background (), & gitcmd.RunOpts {Dir : repoPath })
695+ if err != nil {
696+ // merge-base --is-ancestor returns non-zero if not ancestor
697+ isForce = true
698+ log .Info ().
699+ Str ("branch" , branch ).
700+ Str ("old_hash" , oldHash ).
701+ Str ("new_hash" , newHash ).
702+ Msg ("Force push detected: old commit is not ancestor of new commit" )
703+ }
704+ }
705+ result [branch ] = isForce
679706 }
680707 }
681- return changed
708+ return result
682709}
683710
684711// rollbackBranchRefs restores branch refs to their previous state using native git
0 commit comments