@@ -17,130 +17,220 @@ limitations under the License.
17
17
package helpers
18
18
19
19
import (
20
- "errors"
20
+ "bufio"
21
+ "bytes"
21
22
"io/fs"
23
+ log "log/slog"
22
24
"os"
23
25
"os/exec"
24
26
"path/filepath"
27
+ "sort"
25
28
"strings"
26
29
)
27
30
28
- var (
29
- errFoundConflict = errors .New ("found-conflict" )
30
- errGoConflict = errors .New ("go-conflict" )
31
- )
32
-
33
31
type ConflictSummary struct {
34
32
Makefile bool // Makefile or makefile conflicted
35
33
API bool // anything under api/ or apis/ conflicted
36
34
AnyGo bool // any *.go file anywhere conflicted
37
35
}
38
36
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
39
100
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
44
109
}
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
45
125
}
46
126
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" : {},
51
134
}
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 (">>>>>>> " ),
57
141
}
142
+ markerExact := []byte ("=======" )
143
+
144
+ var conflicts []string
58
145
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
64
156
}
65
- b , err := os .ReadFile (p )
157
+
158
+ // Quick size check
159
+ fi , err := d .Info ()
66
160
if err != nil {
67
- return false
161
+ return nil
162
+ }
163
+ if fi .Size () > maxBytes {
164
+ return nil
68
165
}
69
- s := string (b )
70
- return strings .Contains (s , "<<<<<<<" ) &&
71
- strings .Contains (s , "=======" ) &&
72
- strings .Contains (s , ">>>>>>>" )
73
- }
74
166
75
- for _ , root := range paths {
76
- info , err := os .Stat (root )
167
+ f , err := os .Open (path )
77
168
if err != nil {
78
- continue
169
+ return nil
79
170
}
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 )
83
174
}
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 )
86
182
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
+ }
90
191
}
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
94
195
}
95
- if hasMarkers ( p ) {
96
- return errFoundConflict
196
+ if found {
197
+ break
97
198
}
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 ] {
101
214
return true
102
215
}
103
216
}
104
217
return false
105
218
}
106
219
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
+ }
113
226
}
114
227
}
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" ) {
144
234
return true
145
235
}
146
236
}
0 commit comments