Skip to content

Commit ce8ea50

Browse files
refactor: (alpha update) optimize diff generation and add unified conflict detection
Consolidate duplicate conflict scanning, improve Kubebuilder file patterns, and add comprehensive test coverage. Assisted-by: Cursor
1 parent 61455d0 commit ce8ea50

File tree

5 files changed

+656
-211
lines changed

5 files changed

+656
-211
lines changed

pkg/cli/alpha/internal/update/helpers/conflict.go

Lines changed: 175 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -17,130 +17,220 @@ limitations under the License.
1717
package helpers
1818

1919
import (
20-
"errors"
20+
"bufio"
21+
"bytes"
2122
"io/fs"
23+
log "log/slog"
2224
"os"
2325
"os/exec"
2426
"path/filepath"
27+
"sort"
2528
"strings"
2629
)
2730

28-
var (
29-
errFoundConflict = errors.New("found-conflict")
30-
errGoConflict = errors.New("go-conflict")
31-
)
32-
3331
type ConflictSummary struct {
3432
Makefile bool // Makefile or makefile conflicted
3533
API bool // anything under api/ or apis/ conflicted
3634
AnyGo bool // any *.go file anywhere conflicted
3735
}
3836

37+
// ConflictResult provides detailed conflict information for multiple use cases
38+
type ConflictResult struct {
39+
Summary ConflictSummary
40+
SourceFiles []string // conflicted source files
41+
GeneratedFiles []string // conflicted generated files
42+
}
43+
44+
// isGeneratedKB returns true for Kubebuilder-generated artifacts.
45+
// Moved from open_gh_issue.go to avoid duplication
46+
func isGeneratedKB(path string) bool {
47+
return strings.Contains(path, "/zz_generated.") ||
48+
strings.HasPrefix(path, "config/crd/bases/") ||
49+
strings.HasPrefix(path, "config/rbac/") ||
50+
path == "dist/install.yaml" ||
51+
// Generated deepcopy files
52+
strings.HasSuffix(path, "_deepcopy.go")
53+
}
54+
55+
// FindConflictFiles performs unified conflict detection for both conflict handling and GitHub issue generation
56+
func FindConflictFiles() ConflictResult {
57+
result := ConflictResult{
58+
SourceFiles: []string{},
59+
GeneratedFiles: []string{},
60+
}
61+
62+
// Use git index for fast conflict detection first
63+
gitConflicts := getGitIndexConflicts()
64+
65+
// Filesystem scan for conflict markers
66+
fsConflicts := scanFilesystemForConflicts()
67+
68+
// Combine results and categorize
69+
allConflicts := make(map[string]bool)
70+
for _, f := range gitConflicts {
71+
allConflicts[f] = true
72+
}
73+
for _, f := range fsConflicts {
74+
allConflicts[f] = true
75+
}
76+
77+
// Categorize into source vs generated
78+
for file := range allConflicts {
79+
if isGeneratedKB(file) {
80+
result.GeneratedFiles = append(result.GeneratedFiles, file)
81+
} else {
82+
result.SourceFiles = append(result.SourceFiles, file)
83+
}
84+
}
85+
86+
sort.Strings(result.SourceFiles)
87+
sort.Strings(result.GeneratedFiles)
88+
89+
// Build summary for existing conflict.go usage
90+
result.Summary = ConflictSummary{
91+
Makefile: hasConflictInFiles(allConflicts, "Makefile", "makefile"),
92+
API: hasConflictInPaths(allConflicts, "api", "apis"),
93+
AnyGo: hasGoConflictInFiles(allConflicts),
94+
}
95+
96+
return result
97+
}
98+
99+
// DetectConflicts maintains backward compatibility
39100
func DetectConflicts() ConflictSummary {
40-
return ConflictSummary{
41-
Makefile: hasConflict("Makefile", "makefile"),
42-
API: hasConflict("api", "apis"),
43-
AnyGo: hasGoConflicts(), // checks all *.go in repo (index fast path + FS scan)
101+
return FindConflictFiles().Summary
102+
}
103+
104+
// getGitIndexConflicts uses git ls-files to quickly find unmerged entries
105+
func getGitIndexConflicts() []string {
106+
out, err := exec.Command("git", "ls-files", "-u").Output()
107+
if err != nil {
108+
return nil
44109
}
110+
111+
conflicts := make(map[string]bool)
112+
for _, line := range strings.Split(string(out), "\n") {
113+
fields := strings.Fields(line)
114+
if len(fields) >= 4 {
115+
file := strings.Join(fields[3:], " ")
116+
conflicts[file] = true
117+
}
118+
}
119+
120+
result := make([]string, 0, len(conflicts))
121+
for file := range conflicts {
122+
result = append(result, file)
123+
}
124+
return result
45125
}
46126

47-
// hasConflict: file/dir conflicts via index fast path + marker scan.
48-
func hasConflict(paths ...string) bool {
49-
if len(paths) == 0 {
50-
return false
127+
// scanFilesystemForConflicts scans the working directory for conflict markers
128+
func scanFilesystemForConflicts() []string {
129+
type void struct{}
130+
skipDir := map[string]void{
131+
".git": {},
132+
"vendor": {},
133+
"bin": {},
51134
}
52-
// Fast path: any unmerged entry under these pathspecs?
53-
args := append([]string{"ls-files", "-u", "--"}, paths...)
54-
out, err := exec.Command("git", args...).Output()
55-
if err == nil && len(strings.TrimSpace(string(out))) > 0 {
56-
return true
135+
136+
const maxBytes = 2 << 20 // 2 MiB per file
137+
138+
markersPrefix := [][]byte{
139+
[]byte("<<<<<<< "),
140+
[]byte(">>>>>>> "),
57141
}
142+
markerExact := []byte("=======")
143+
144+
var conflicts []string
58145

59-
// Fallback: scan for conflict markers.
60-
hasMarkers := func(p string) bool {
61-
// Best-effort, skip large likely-binaries.
62-
if fi, err := os.Stat(p); err == nil && fi.Size() > 1<<20 {
63-
return false
146+
_ = filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
147+
if err != nil {
148+
return nil // best-effort
149+
}
150+
// Skip unwanted directories
151+
if d.IsDir() {
152+
if _, ok := skipDir[d.Name()]; ok {
153+
return filepath.SkipDir
154+
}
155+
return nil
64156
}
65-
b, err := os.ReadFile(p)
157+
158+
// Quick size check
159+
fi, err := d.Info()
66160
if err != nil {
67-
return false
161+
return nil
162+
}
163+
if fi.Size() > maxBytes {
164+
return nil
68165
}
69-
s := string(b)
70-
return strings.Contains(s, "<<<<<<<") &&
71-
strings.Contains(s, "=======") &&
72-
strings.Contains(s, ">>>>>>>")
73-
}
74166

75-
for _, root := range paths {
76-
info, err := os.Stat(root)
167+
f, err := os.Open(path)
77168
if err != nil {
78-
continue
169+
return nil
79170
}
80-
if !info.IsDir() {
81-
if hasMarkers(root) {
82-
return true
171+
defer func() {
172+
if cerr := f.Close(); cerr != nil {
173+
log.Warn("failed to close file", "path", path, "error", cerr)
83174
}
84-
continue
85-
}
175+
}()
176+
177+
found := false
178+
sc := bufio.NewScanner(f)
179+
// allow long lines (YAML/JSON)
180+
buf := make([]byte, 0, 1024*1024)
181+
sc.Buffer(buf, 4<<20)
86182

87-
werr := filepath.WalkDir(root, func(p string, d fs.DirEntry, walkErr error) error {
88-
if walkErr != nil || d.IsDir() {
89-
return nil
183+
for sc.Scan() {
184+
b := sc.Bytes()
185+
// starts with conflict markers
186+
for _, p := range markersPrefix {
187+
if bytes.HasPrefix(b, p) {
188+
found = true
189+
break
190+
}
90191
}
91-
// Skip obvious noise dirs.
92-
if d.Name() == ".git" || strings.Contains(p, string(filepath.Separator)+".git"+string(filepath.Separator)) {
93-
return nil
192+
// exact middle marker line
193+
if !found && bytes.Equal(b, markerExact) {
194+
found = true
94195
}
95-
if hasMarkers(p) {
96-
return errFoundConflict
196+
if found {
197+
break
97198
}
98-
return nil
99-
})
100-
if errors.Is(werr, errFoundConflict) {
199+
}
200+
201+
if found {
202+
conflicts = append(conflicts, path)
203+
}
204+
return nil
205+
})
206+
207+
return conflicts
208+
}
209+
210+
// Helper functions for backward compatibility
211+
func hasConflictInFiles(conflicts map[string]bool, paths ...string) bool {
212+
for _, path := range paths {
213+
if conflicts[path] {
101214
return true
102215
}
103216
}
104217
return false
105218
}
106219

107-
// hasGoConflicts: any *.go file conflicted (repo-wide).
108-
func hasGoConflicts(roots ...string) bool {
109-
// Fast path: any unmerged *.go anywhere?
110-
if out, err := exec.Command("git", "ls-files", "-u", "--", "*.go").Output(); err == nil {
111-
if len(strings.TrimSpace(string(out))) > 0 {
112-
return true
220+
func hasConflictInPaths(conflicts map[string]bool, pathPrefixes ...string) bool {
221+
for file := range conflicts {
222+
for _, prefix := range pathPrefixes {
223+
if strings.HasPrefix(file, prefix+"/") || file == prefix {
224+
return true
225+
}
113226
}
114227
}
115-
// Fallback: filesystem scan (repo-wide or limited to roots if provided).
116-
if len(roots) == 0 {
117-
roots = []string{"."}
118-
}
119-
for _, root := range roots {
120-
werr := filepath.WalkDir(root, func(p string, d fs.DirEntry, walkErr error) error {
121-
if walkErr != nil || d.IsDir() || !strings.HasSuffix(p, ".go") {
122-
return nil
123-
}
124-
// Skip .git and large files.
125-
if strings.Contains(p, string(filepath.Separator)+".git"+string(filepath.Separator)) {
126-
return nil
127-
}
128-
if fi, err := os.Stat(p); err == nil && fi.Size() > 1<<20 {
129-
return nil
130-
}
131-
b, err := os.ReadFile(p)
132-
if err != nil {
133-
return nil
134-
}
135-
s := string(b)
136-
if strings.Contains(s, "<<<<<<<") &&
137-
strings.Contains(s, "=======") &&
138-
strings.Contains(s, ">>>>>>>") {
139-
return errGoConflict
140-
}
141-
return nil
142-
})
143-
if errors.Is(werr, errGoConflict) {
228+
return false
229+
}
230+
231+
func hasGoConflictInFiles(conflicts map[string]bool) bool {
232+
for file := range conflicts {
233+
if strings.HasSuffix(file, ".go") {
144234
return true
145235
}
146236
}

0 commit comments

Comments
 (0)