Skip to content

Commit 789a04e

Browse files
authored
Merge pull request #1786 from helixml/feature/001289-force-pushing-on-a-spec
Force pushing on a spec task branch doesn't work. It fail...
2 parents 14ec76a + 42d07ec commit 789a04e

File tree

1 file changed

+42
-15
lines changed

1 file changed

+42
-15
lines changed

api/pkg/services/git_http_server.go

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)