@@ -17,130 +17,220 @@ limitations under the License.
1717package helpers
1818
1919import (
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-
3331type 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
39100func 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