Fall back when source doesn't advertise HEAD · Entire

Fall back when source doesn't advertise HEAD

8858fbb→main·

nodo·1mo ago·2 files·+148 added/-9 removed

Some HTTP v1 servers don't return HEAD in their info/refs. Previously HEAD was left at PlainInit's default refs/heads/master, which then fails the --check HEAD step on any main-default repo. pickHEAD now selects in order: server-advertised HEAD (resolved via the matched DesiredRef.TargetRef so user-supplied ref mappings are honored), refs/heads/main, refs/heads/master, then the lexicographically first branch. Tags-only conversions still leave HEAD at the PlainInit default since there's no sensible branch to point at.

Sessions

Transcript data is unavailable for this checkpoint.

Changes

2

323 unmodified lines

""
return Result{}, fmt.Errorf("write target refs: %w", err)
}

// Point HEAD at the source's symbolic HEAD if it landed in the
// converted ref set. PlainInit defaults HEAD to refs/heads/master,
// which often doesn't exist (e.g. repos using "main" as the default).
if refService.HeadTarget != "" {
if _, ok := desired[refService.HeadTarget]; ok {
head := plumbing.NewSymbolicReference(plumbing.HEAD, refService.HeadTarget)
if err := dstRepo.Storer.SetReference(head); err != nil {
return Result{}, fmt.Errorf("set HEAD: %w", err)
}
// Point HEAD at a ref that actually exists in the target. PlainInit
// defaults HEAD to refs/heads/master, which often doesn't exist
// (e.g. repos using "main"), and would then fail the --check HEAD
// step. See pickHEAD for the selection order.
if headRef := pickHEAD(refService.HeadTarget, desired); headRef != "" {
if err := dstRepo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, headRef)); err != nil {
return Result{}, fmt.Errorf("set HEAD: %w", err)
}
}

1114 unmodified lines

return nil
}

// pickHEAD chooses which target-side ref the bare repo's HEAD should
// symlink to. It returns "" when no suitable branch exists (e.g. a
// tags-only conversion), in which case the caller leaves HEAD at the
// PlainInit default.
//
// Selection order:
//  1. The source's advertised HEAD, if it landed in the converted set.
//     Resolved via the desired entry's TargetRef so a user-supplied ref
//     mapping is honored.
//  2. refs/heads/main, then refs/heads/master, if either is present in
//     the converted target refs. Some HTTP v1 servers do not advertise
//     HEAD, so we pattern-match on conventional defaults.
//  3. The lexicographically first refs/heads/* in the target set, for
//     a deterministic fallback when neither convention is present.
func pickHEAD(advertised plumbing.ReferenceName, desired map[plumbing.ReferenceName]planner.DesiredRef) plumbing.ReferenceName {
if advertised != "" {
if d, ok := desired[advertised]; ok {
return d.TargetRef
}
}
branches := make(map[plumbing.ReferenceName]struct{}, len(desired))
for _, d := range desired {
if d.TargetRef.IsBranch() {
branches[d.TargetRef] = struct{}{}
}
}
for _, candidate := range []plumbing.ReferenceName{"refs/heads/main", "refs/heads/master"} {
if _, ok := branches[candidate]; ok {
return candidate
}
}
if len(branches) == 0 {
return ""
}
names := make([]string, 0, len(branches))
for name := range branches {
names = append(names, string(name))
}
sort.Strings(names)
return plumbing.ReferenceName(names[0])
}

func writeRefs(

dst storer.Storer,
desired map[plumbing.ReferenceName]planner.DesiredRef,

Mcmd/git-sync/internal/sha256convert/sha256convert.go+49/-9

24 unmodified lines

""
26
27
28
29
30
31
32
937 unmodified lines

970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
    
24 unmodified lines

"github.com/go-git/go-git/v6/plumbing/object"
"gogitstorer "github.com/go-git/go-git/v6/plumbing/storer"
"github.com/go-git/go-git/v6/storage/filesystem"

"entire.io/entire/git-sync/internal/planner"

// TestTranslator builds a small SHA1 source repo with blobs, trees, commits,
937 unmodified lines

// will start failing in this package's tests rather than only at runtime
// against a real repo.
var _ = (*filesystem.Storage)(nil)

func TestPickHEAD(t *testing.T) {
branch := func(name string) planner.DesiredRef {
ref := plumbing.ReferenceName("refs/heads/" + name)
return planner.DesiredRef{Kind: planner.RefKindBranch, SourceRef: ref, TargetRef: ref}
}
tag := func(name string) planner.DesiredRef {
ref := plumbing.ReferenceName("refs/tags/" + name)
return planner.DesiredRef{Kind: planner.RefKindTag, SourceRef: ref, TargetRef: ref}
}
tests := []struct {
name       string
advertised plumbing.ReferenceName
desired    map[plumbing.ReferenceName]planner.DesiredRef
want       plumbing.ReferenceName
}{
{
name:       "advertised HEAD wins when present in desired",
advertised: "refs/heads/develop",
desired: map[plumbing.ReferenceName]planner.DesiredRef{
"refs/heads/main":    branch("main"),
"refs/heads/develop": branch("develop"),
},
want: "refs/heads/develop",
},
{
name:       "advertised HEAD respects ref mapping (target side)",
advertised: "refs/heads/source-name",
desired: map[plumbing.ReferenceName]planner.DesiredRef{
"refs/heads/source-name": {
Kind:      planner.RefKindBranch,
SourceRef: "refs/heads/source-name",
TargetRef: "refs/heads/target-name",
},
},
want: "refs/heads/target-name",
},
{
name:       "falls back to main when advertised HEAD missing",
advertised: "",
desired: map[plumbing.ReferenceName]planner.DesiredRef{
"refs/heads/main":   branch("main"),
"refs/heads/master": branch("master"),
},
want: "refs/heads/main",
},
{
name:       "falls back to master when no main",
advertised: "",
desired: map[plumbing.ReferenceName]planner.DesiredRef{
"refs/heads/master":  branch("master"),
"refs/heads/feature": branch("feature"),
},
want: "refs/heads/master",
},
{
name:       "falls back to first sorted branch when neither main nor master",
advertised: "",
desired: map[plumbing.ReferenceName]planner.DesiredRef{
"refs/heads/zeta":  branch("zeta"),
"refs/heads/alpha": branch("alpha"),
"refs/heads/beta":  branch("beta"),
},
want: "refs/heads/alpha",
},
{
name:       "advertised HEAD pointing outside desired falls back to convention",
advertised: "refs/heads/dropped",
desired: map[plumbing.ReferenceName]planner.DesiredRef{
"refs/heads/main": branch("main"),
},
want: "refs/heads/main",
},
{
name:       "tags-only conversion returns empty so HEAD stays at PlainInit default",
advertised: "",
desired: map[plumbing.ReferenceName]planner.DesiredRef{
"refs/tags/v1.0": tag("v1.0"),
},
want: "",
},
{
name:       "empty desired returns empty",
advertised: "",
desired:    map[plumbing.ReferenceName]planner.DesiredRef{},
want:       "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := pickHEAD(tt.advertised, tt.desired)
if got != tt.want {
t.Fatalf("pickHEAD = %q, want %q", got, tt.want)
}
})
}
}