@@ -679,6 +679,21 @@ func (r *runner) waitForJournal(ctx context.Context) error {
679679 r .printStageEndIfNeeded )
680680}
681681
682+ // bestBranchFromCandidates selects the best branch for HEAD from a set of
683+ // candidate branch names (prefer main > master > alphabetically first).
684+ func bestBranchFromCandidates (best plumbing.ReferenceName , candidate plumbing.ReferenceName ) plumbing.ReferenceName {
685+ switch {
686+ case candidate == "refs/heads/main" :
687+ return candidate
688+ case candidate == "refs/heads/master" && best != "refs/heads/main" :
689+ return candidate
690+ case best == "" || (best != "refs/heads/main" && best != "refs/heads/master" && candidate < best ):
691+ return candidate
692+ default :
693+ return best
694+ }
695+ }
696+
682697// handleList: From https://git-scm.com/docs/git-remote-helpers
683698//
684699// Lists the refs, one per line, in the format "<value> <name> [<attr>
@@ -705,9 +720,18 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) {
705720 if err != nil {
706721 return err
707722 }
723+ defer refs .Close ()
708724
709- var symRefs []string
725+ type symRefInfo struct {
726+ name plumbing.ReferenceName
727+ target plumbing.ReferenceName
728+ }
729+ var symRefs []symRefInfo
730+ hashRefNames := make (map [plumbing.ReferenceName ]bool )
710731 hashesSeen := false
732+ // Track the best fallback branch for HEAD in case its target
733+ // doesn't exist (prefer main > master > alphabetically first).
734+ var bestBranch plumbing.ReferenceName
711735 for {
712736 ref , err := refs .Next ()
713737 if errors .Cause (err ) == io .EOF {
@@ -722,6 +746,11 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) {
722746 case plumbing .HashReference :
723747 value = ref .Hash ().String ()
724748 hashesSeen = true
749+ hashRefNames [ref .Name ()] = true
750+ // Track best branch for fallback HEAD.
751+ if strings .HasPrefix (ref .Name ().String (), "refs/heads/" ) {
752+ bestBranch = bestBranchFromCandidates (bestBranch , ref .Name ())
753+ }
725754 case plumbing .SymbolicReference :
726755 value = "@" + ref .Target ().String ()
727756 default :
@@ -734,7 +763,10 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) {
734763 // cloning an empty repo will result in an error because
735764 // the HEAD symbolic ref points to a ref that doesn't
736765 // exist.
737- symRefs = append (symRefs , refStr )
766+ symRefs = append (symRefs , symRefInfo {
767+ name : ref .Name (),
768+ target : ref .Target (),
769+ })
738770 continue
739771 }
740772 r .log .CDebugf (ctx , "Listing ref %s" , refStr )
@@ -745,7 +777,30 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) {
745777 }
746778
747779 if hashesSeen && ! forPush {
748- for _ , refStr := range symRefs {
780+ for _ , sr := range symRefs {
781+ target := sr .target
782+ // If the symref target doesn't exist among hash refs,
783+ // rewrite it to point to the best available branch,
784+ // but only for HEAD. Other symrefs are emitted as-is.
785+ if ! hashRefNames [target ] {
786+ if sr .name == plumbing .HEAD {
787+ if bestBranch == "" {
788+ r .log .CDebugf (ctx ,
789+ "Skipping HEAD symref %s -> %s (no branches available)" ,
790+ sr .name , target )
791+ continue
792+ }
793+ r .log .CDebugf (ctx ,
794+ "Rewriting HEAD symref from %s to %s" ,
795+ target , bestBranch )
796+ target = bestBranch
797+ } else {
798+ r .log .CDebugf (ctx ,
799+ "Emitting non-HEAD symref %s -> %s with unknown target" ,
800+ sr .name , target )
801+ }
802+ }
803+ refStr := "@" + target .String () + " " + sr .name .String () + "\n "
749804 r .log .CDebugf (ctx , "Listing symbolic ref %s" , refStr )
750805 _ , err = r .output .Write ([]byte (refStr ))
751806 if err != nil {
@@ -1827,6 +1882,53 @@ func (r *runner) handlePushBatch(ctx context.Context, args [][]string) (
18271882 return nil , err
18281883 }
18291884
1885+ // If HEAD points to a nonexistent ref, update it to point to the
1886+ // best available branch in the repo (prefer main > master > alphabetical).
1887+ // We intentionally repair HEAD even when the target was explicitly
1888+ // deleted in this batch, because a broken HEAD causes clone failures.
1889+ // This must happen before waitForJournal so the HEAD update is
1890+ // included in the same flush.
1891+ head , headErr := repo .Storer .Reference (plumbing .HEAD )
1892+ if headErr == nil && head .Type () == plumbing .SymbolicReference {
1893+ _ , targetErr := repo .Storer .Reference (head .Target ())
1894+ var bestBranch plumbing.ReferenceName
1895+ if targetErr == plumbing .ErrReferenceNotFound {
1896+ allRefs , refsErr := repo .References ()
1897+ if refsErr == nil {
1898+ defer allRefs .Close ()
1899+ for {
1900+ ref , nextErr := allRefs .Next ()
1901+ if errors .Cause (nextErr ) == io .EOF {
1902+ break
1903+ }
1904+ if nextErr != nil {
1905+ break
1906+ }
1907+ if ref .Type () != plumbing .HashReference {
1908+ continue
1909+ }
1910+ if ! strings .HasPrefix (ref .Name ().String (), "refs/heads/" ) {
1911+ continue
1912+ }
1913+ bestBranch = bestBranchFromCandidates (bestBranch , ref .Name ())
1914+ }
1915+ }
1916+ }
1917+ if bestBranch != "" {
1918+ newHead := plumbing .NewSymbolicReference (
1919+ plumbing .HEAD , bestBranch )
1920+ if setErr := repo .Storer .SetReference (
1921+ newHead ); setErr != nil {
1922+ r .log .CDebugf (ctx ,
1923+ "Error updating HEAD to %s: %+v" ,
1924+ bestBranch , setErr )
1925+ } else {
1926+ r .log .CDebugf (ctx ,
1927+ "Updated HEAD to point to %s" , bestBranch )
1928+ }
1929+ }
1930+ }
1931+
18301932 err = r .waitForJournal (ctx )
18311933 if err != nil {
18321934 return nil , err
0 commit comments