-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathgit_smart_fetch.go
More file actions
172 lines (148 loc) · 4.88 KB
/
Copy pathgit_smart_fetch.go
File metadata and controls
172 lines (148 loc) · 4.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// SPDX-License-Identifier: Apache-2.0
package git
import (
"context"
"errors"
"fmt"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/config"
"github.com/go-git/go-git/v6/plumbing"
gitclient "github.com/go-git/go-git/v6/plumbing/client"
"github.com/go-git/go-git/v6/plumbing/transport"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// SmartFetch performs a network sync and returns the best available LOCAL branch reference.
// It prioritizes the target branch but always fetches the default branch as a safety net.
//
// Return values (example with target="refs/heads/feature"):
// - "refs/heads/feature", nil: Target found on remote, fetched, ready to checkout.
// - "refs/heads/main", nil: Target missing on remote, fell back to default branch.
// - "", nil: No valid branches found (empty repo).
func SmartFetch(
ctx context.Context,
repo *git.Repository,
target plumbing.ReferenceName, // e.g. "refs/heads/feature" or "HEAD"
auth []gitclient.Option,
) (plumbing.ReferenceName, error) {
remoteName := "origin"
remote, err := repo.Remote(remoteName)
if err != nil {
return "", fmt.Errorf("failed to get remote %s: %w", remoteName, err)
}
// 1. Audit: List refs
refs, err := listRemoteRefs(remote, auth)
if err != nil {
return "", err
}
if len(refs) == 0 {
return "", nil
}
// 2. Analyze: Find default branch and check target existence
defaultFull, defaultShort, targetExists := analyzeRemoteRefs(ctx, refs, target.String())
// 3. Plan: Build RefSpecs based on analysis
refSpecs := buildSmartRefSpecs(remoteName, defaultFull, defaultShort, target, targetExists)
// Determine Result (The return value)
var result plumbing.ReferenceName
switch {
case targetExists:
result = target
case defaultFull != "":
result = plumbing.ReferenceName(defaultFull)
default:
return "", nil
}
// 4. Execute: Fetch
if len(refSpecs) > 0 {
err = repo.Fetch(&git.FetchOptions{
RemoteName: remoteName,
ClientOptions: auth,
RefSpecs: refSpecs,
Depth: 1,
Force: true,
Prune: true,
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return "", fmt.Errorf("smart fetch failed: %w", err)
}
}
// 5. Repair: Fix local symbolic HEAD
repairRemoteSymbolicHead(repo, remoteName, defaultShort)
return result, nil
}
func listRemoteRefs(remote *git.Remote, auth []gitclient.Option) ([]*plumbing.Reference, error) {
refs, err := remote.List(&git.ListOptions{ClientOptions: auth})
if errors.Is(err, transport.ErrEmptyRemoteRepository) {
return nil, nil // Valid state, not an error
}
if err != nil {
return nil, fmt.Errorf("failed to list remote refs: %w", err)
}
return refs, nil
}
// analyzeRemoteRefs scans the reference list to find the default branch and check if the target exists.
func analyzeRemoteRefs(ctx context.Context, refs []*plumbing.Reference, targetFullStr string) (string, string, bool) {
logger := log.FromContext(ctx)
var defaultFull, defaultShort string
var targetExists bool
// Map existing refs for O(1) lookup validation
existingRefs := make(map[string]bool, len(refs))
for _, ref := range refs {
existingRefs[ref.Name().String()] = true
}
for _, ref := range refs {
name := ref.Name().String()
// Check for Default Branch (HEAD)
if name == "HEAD" && ref.Type() == plumbing.SymbolicReference {
target := ref.Target().String()
if existingRefs[target] {
defaultFull = target
defaultShort = cleanBranchName(ref.Target().Short())
} else {
logger.Info("Remote HEAD is broken (points to missing ref)", "target", target)
}
}
// Check for Target
if name == targetFullStr {
targetExists = true
}
}
return defaultFull, defaultShort, targetExists
}
func buildSmartRefSpecs(
remoteName, defaultFull, defaultShort string,
target plumbing.ReferenceName,
targetExists bool,
) []config.RefSpec {
var refSpecs []config.RefSpec
// A. Always fetch Default (Safety Net)
if defaultFull != "" {
spec := config.RefSpec(fmt.Sprintf("+%s:refs/remotes/%s/%s", defaultFull, remoteName, defaultShort))
refSpecs = append(refSpecs, spec)
}
// B. Fetch Target (If valid and different)
if targetExists {
targetFullStr := target.String()
if defaultFull != targetFullStr {
spec := config.RefSpec(fmt.Sprintf("+%s:refs/remotes/%s/%s", targetFullStr, remoteName, target.Short()))
refSpecs = append(refSpecs, spec)
}
}
return refSpecs
}
func repairRemoteSymbolicHead(repo *git.Repository, remoteName, defaultShort string) {
if defaultShort == "" {
return
}
symRef := plumbing.NewSymbolicReference(
plumbing.NewRemoteReferenceName(remoteName, "HEAD"),
plumbing.NewRemoteReferenceName(remoteName, defaultShort),
)
_ = repo.Storer.SetReference(symRef)
}
// cleanBranchName handles the edge case where .Short() returns "origin/main" instead of "main".
func cleanBranchName(name string) string {
if len(name) > 7 && name[:7] == "origin/" {
return name[7:]
}
return name
}