@@ -11,12 +11,15 @@ import (
11
11
"fmt"
12
12
"io"
13
13
"os"
14
+ "path/filepath"
14
15
"strings"
15
16
16
17
"code.gitea.io/gitea/models"
17
18
"code.gitea.io/gitea/models/unit"
18
19
"code.gitea.io/gitea/modules/git"
20
+ "code.gitea.io/gitea/modules/graceful"
19
21
"code.gitea.io/gitea/modules/log"
22
+ "code.gitea.io/gitea/modules/process"
20
23
"code.gitea.io/gitea/modules/util"
21
24
22
25
"github.com/gobwas/glob"
@@ -98,12 +101,193 @@ func TestPatch(pr *models.PullRequest) error {
98
101
return nil
99
102
}
100
103
104
+ type errMergeConflict struct {
105
+ filename string
106
+ }
107
+
108
+ func (e * errMergeConflict ) Error () string {
109
+ return fmt .Sprintf ("conflict detected at: %s" , e .filename )
110
+ }
111
+
112
+ func attemptMerge (ctx context.Context , file * unmergedFile , tmpBasePath string , gitRepo * git.Repository ) error {
113
+ switch {
114
+ case file .stage1 != nil && (file .stage2 == nil || file .stage3 == nil ):
115
+ // 1. Deleted in one or both:
116
+ //
117
+ // Conflict <==> the stage1 !SameAs to the undeleted one
118
+ if (file .stage2 != nil && ! file .stage1 .SameAs (file .stage2 )) || (file .stage3 != nil && ! file .stage1 .SameAs (file .stage3 )) {
119
+ // Conflict!
120
+ return & errMergeConflict {file .stage1 .path }
121
+ }
122
+
123
+ // Not a genuine conflict and we can simply remove the file from the index
124
+ return gitRepo .RemoveFilesFromIndex (file .stage1 .path )
125
+ case file .stage1 == nil && file .stage2 != nil && (file .stage3 == nil || file .stage2 .SameAs (file .stage3 )):
126
+ // 2. Added in ours but not in theirs or identical in both
127
+ //
128
+ // Not a genuine conflict just add to the index
129
+ if err := gitRepo .AddObjectToIndex (file .stage2 .mode , git .MustIDFromString (file .stage2 .sha ), file .stage2 .path ); err != nil {
130
+ return err
131
+ }
132
+ return nil
133
+ case file .stage1 == nil && file .stage2 != nil && file .stage3 != nil && file .stage2 .sha == file .stage3 .sha && file .stage2 .mode != file .stage3 .mode :
134
+ // 3. Added in both with the same sha but the modes are different
135
+ //
136
+ // Conflict! (Not sure that this can actually happen but we should handle)
137
+ return & errMergeConflict {file .stage2 .path }
138
+ case file .stage1 == nil && file .stage2 == nil && file .stage3 != nil :
139
+ // 4. Added in theirs but not ours:
140
+ //
141
+ // Not a genuine conflict just add to the index
142
+ return gitRepo .AddObjectToIndex (file .stage3 .mode , git .MustIDFromString (file .stage3 .sha ), file .stage3 .path )
143
+ case file .stage1 == nil :
144
+ // 5. Created by new in both
145
+ //
146
+ // Conflict!
147
+ return & errMergeConflict {file .stage2 .path }
148
+ case file .stage2 != nil && file .stage3 != nil :
149
+ // 5. Modified in both - we should try to merge in the changes but first:
150
+ //
151
+ if file .stage2 .mode == "120000" || file .stage3 .mode == "120000" {
152
+ // 5a. Conflicting symbolic link change
153
+ return & errMergeConflict {file .stage2 .path }
154
+ }
155
+ if file .stage2 .mode == "160000" || file .stage3 .mode == "160000" {
156
+ // 5b. Conflicting submodule change
157
+ return & errMergeConflict {file .stage2 .path }
158
+ }
159
+ if file .stage2 .mode != file .stage3 .mode {
160
+ // 5c. Conflicting mode change
161
+ return & errMergeConflict {file .stage2 .path }
162
+ }
163
+
164
+ // Need to get the objects from the object db to attempt to merge
165
+ root , err := git .NewCommandContext (ctx , "unpack-file" , file .stage1 .sha ).RunInDir (tmpBasePath )
166
+ if err != nil {
167
+ return fmt .Errorf ("unable to get root object: %s at path: %s for merging. Error: %w" , file .stage1 .sha , file .stage1 .path , err )
168
+ }
169
+ root = strings .TrimSpace (root )
170
+ defer func () {
171
+ _ = util .Remove (filepath .Join (tmpBasePath , root ))
172
+ }()
173
+
174
+ base , err := git .NewCommandContext (ctx , "unpack-file" , file .stage2 .sha ).RunInDir (tmpBasePath )
175
+ if err != nil {
176
+ return fmt .Errorf ("unable to get base object: %s at path: %s for merging. Error: %w" , file .stage2 .sha , file .stage2 .path , err )
177
+ }
178
+ base = strings .TrimSpace (filepath .Join (tmpBasePath , base ))
179
+ defer func () {
180
+ _ = util .Remove (base )
181
+ }()
182
+ head , err := git .NewCommandContext (ctx , "unpack-file" , file .stage3 .sha ).RunInDir (tmpBasePath )
183
+ if err != nil {
184
+ return fmt .Errorf ("unable to get head object:%s at path: %s for merging. Error: %w" , file .stage3 .sha , file .stage3 .path , err )
185
+ }
186
+ head = strings .TrimSpace (head )
187
+ defer func () {
188
+ _ = util .Remove (filepath .Join (tmpBasePath , head ))
189
+ }()
190
+
191
+ // now git merge-file annoyingly takes a different order to the merge-tree ...
192
+ _ , conflictErr := git .NewCommandContext (ctx , "merge-file" , base , root , head ).RunInDir (tmpBasePath )
193
+ if conflictErr != nil {
194
+ return & errMergeConflict {file .stage2 .path }
195
+ }
196
+
197
+ // base now contains the merged data
198
+ hash , err := git .NewCommandContext (ctx , "hash-object" , "-w" , "--path" , file .stage2 .path , base ).RunInDir (tmpBasePath )
199
+ if err != nil {
200
+ return err
201
+ }
202
+ hash = strings .TrimSpace (hash )
203
+ return gitRepo .AddObjectToIndex (file .stage2 .mode , git .MustIDFromString (hash ), file .stage2 .path )
204
+ default :
205
+ if file .stage1 != nil {
206
+ return & errMergeConflict {file .stage1 .path }
207
+ } else if file .stage2 != nil {
208
+ return & errMergeConflict {file .stage2 .path }
209
+ } else if file .stage3 != nil {
210
+ return & errMergeConflict {file .stage3 .path }
211
+ }
212
+ }
213
+ return nil
214
+ }
215
+
101
216
func checkConflicts (pr * models.PullRequest , gitRepo * git.Repository , tmpBasePath string ) (bool , error ) {
217
+ ctx , cancel , finished := process .GetManager ().AddContext (graceful .GetManager ().HammerContext (), fmt .Sprintf ("checkConflicts: pr[%d] %s/%s#%d" , pr .ID , pr .BaseRepo .OwnerName , pr .BaseRepo .Name , pr .Index ))
218
+ defer finished ()
219
+
220
+ // First we use read-tree to do a simple three-way merge
221
+ if _ , err := git .NewCommandContext (ctx , "read-tree" , "-m" , pr .MergeBase , "base" , "tracking" ).RunInDir (tmpBasePath ); err != nil {
222
+ log .Error ("Unable to run read-tree -m! Error: %v" , err )
223
+ return false , fmt .Errorf ("unable to run read-tree -m! Error: %v" , err )
224
+ }
225
+
226
+ // Then we use git ls-files -u to list the unmerged files and collate the triples in unmergedfiles
227
+ unmerged := make (chan * unmergedFile )
228
+ go unmergedFiles (ctx , tmpBasePath , unmerged )
229
+
230
+ defer func () {
231
+ cancel ()
232
+ for range unmerged {
233
+ // empty the unmerged channel
234
+ }
235
+ }()
236
+
237
+ numberOfConflicts := 0
238
+ conflict := false
239
+
240
+ for file := range unmerged {
241
+ if file == nil {
242
+ break
243
+ }
244
+ if file .err != nil {
245
+ cancel ()
246
+ return false , file .err
247
+ }
248
+
249
+ // OK now we have the unmerged file triplet attempt to merge it
250
+ if err := attemptMerge (ctx , file , tmpBasePath , gitRepo ); err != nil {
251
+ if conflictErr , ok := err .(* errMergeConflict ); ok {
252
+ log .Trace ("Conflict: %s in PR[%d] %s/%s#%d" , conflictErr .filename , pr .ID , pr .BaseRepo .OwnerName , pr .BaseRepo .Name , pr .Index )
253
+ conflict = true
254
+ if numberOfConflicts < 10 {
255
+ pr .ConflictedFiles = append (pr .ConflictedFiles , conflictErr .filename )
256
+ }
257
+ numberOfConflicts ++
258
+ continue
259
+ }
260
+ return false , err
261
+ }
262
+ }
263
+
264
+ if ! conflict {
265
+ treeHash , err := git .NewCommandContext (ctx , "write-tree" ).RunInDir (tmpBasePath )
266
+ if err != nil {
267
+ return false , err
268
+ }
269
+ treeHash = strings .TrimSpace (treeHash )
270
+ baseTree , err := gitRepo .GetTree ("base" )
271
+ if err != nil {
272
+ return false , err
273
+ }
274
+ if treeHash == baseTree .ID .String () {
275
+ log .Debug ("PullRequest[%d]: Patch is empty - ignoring" , pr .ID )
276
+ pr .Status = models .PullRequestStatusEmpty
277
+ pr .ConflictedFiles = []string {}
278
+ pr .ChangedProtectedFiles = []string {}
279
+ }
280
+
281
+ return false , nil
282
+ }
283
+
284
+ // OK read-tree has failed so we need to try a different thing - this might actually succeed where the above fails due to whitespace handling.
285
+
102
286
// 1. Create a plain patch from head to base
103
287
tmpPatchFile , err := os .CreateTemp ("" , "patch" )
104
288
if err != nil {
105
289
log .Error ("Unable to create temporary patch file! Error: %v" , err )
106
- return false , fmt .Errorf ("Unable to create temporary patch file! Error: %v" , err )
290
+ return false , fmt .Errorf ("unable to create temporary patch file! Error: %v" , err )
107
291
}
108
292
defer func () {
109
293
_ = util .Remove (tmpPatchFile .Name ())
@@ -112,12 +296,12 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
112
296
if err := gitRepo .GetDiffBinary (pr .MergeBase , "tracking" , tmpPatchFile ); err != nil {
113
297
tmpPatchFile .Close ()
114
298
log .Error ("Unable to get patch file from %s to %s in %s Error: %v" , pr .MergeBase , pr .HeadBranch , pr .BaseRepo .FullName (), err )
115
- return false , fmt .Errorf ("Unable to get patch file from %s to %s in %s Error: %v" , pr .MergeBase , pr .HeadBranch , pr .BaseRepo .FullName (), err )
299
+ return false , fmt .Errorf ("unable to get patch file from %s to %s in %s Error: %v" , pr .MergeBase , pr .HeadBranch , pr .BaseRepo .FullName (), err )
116
300
}
117
301
stat , err := tmpPatchFile .Stat ()
118
302
if err != nil {
119
303
tmpPatchFile .Close ()
120
- return false , fmt .Errorf ("Unable to stat patch file: %v" , err )
304
+ return false , fmt .Errorf ("unable to stat patch file: %v" , err )
121
305
}
122
306
patchPath := tmpPatchFile .Name ()
123
307
tmpPatchFile .Close ()
@@ -154,6 +338,9 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
154
338
if prConfig .IgnoreWhitespaceConflicts {
155
339
args = append (args , "--ignore-whitespace" )
156
340
}
341
+ if git .CheckGitVersionAtLeast ("2.32.0" ) == nil {
342
+ args = append (args , "--3way" )
343
+ }
157
344
args = append (args , patchPath )
158
345
pr .ConflictedFiles = make ([]string , 0 , 5 )
159
346
@@ -168,15 +355,15 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
168
355
stderrReader , stderrWriter , err := os .Pipe ()
169
356
if err != nil {
170
357
log .Error ("Unable to open stderr pipe: %v" , err )
171
- return false , fmt .Errorf ("Unable to open stderr pipe: %v" , err )
358
+ return false , fmt .Errorf ("unable to open stderr pipe: %v" , err )
172
359
}
173
360
defer func () {
174
361
_ = stderrReader .Close ()
175
362
_ = stderrWriter .Close ()
176
363
}()
177
364
178
365
// 7. Run the check command
179
- conflict : = false
366
+ conflict = false
180
367
err = git .NewCommand (args ... ).
181
368
RunInDirTimeoutEnvFullPipelineFunc (
182
369
nil , - 1 , tmpBasePath ,
0 commit comments