@@ -16,7 +16,9 @@ import (
16
16
"code.gitea.io/gitea/models"
17
17
"code.gitea.io/gitea/models/unit"
18
18
"code.gitea.io/gitea/modules/git"
19
+ "code.gitea.io/gitea/modules/graceful"
19
20
"code.gitea.io/gitea/modules/log"
21
+ "code.gitea.io/gitea/modules/process"
20
22
"code.gitea.io/gitea/modules/util"
21
23
22
24
"github.com/gobwas/glob"
@@ -98,74 +100,170 @@ func TestPatch(pr *models.PullRequest) error {
98
100
return nil
99
101
}
100
102
103
+ func attemptMerge (ctx context.Context , file * unmergedFile , tmpBasePath string , gitRepo * git.Repository , markConflict func (string )) error {
104
+ switch {
105
+ case file .stage1 != nil && (file .stage2 == nil || file .stage3 == nil ):
106
+ // 1. Deleted in one or both:
107
+ //
108
+ // Conflict <==> the stage1 !Equivalent to the undeleted one
109
+ if (file .stage2 != nil && ! file .stage1 .SameAs (file .stage2 )) || (file .stage3 != nil && ! file .stage1 .SameAs (file .stage3 )) {
110
+ // Conflict!
111
+ markConflict (file .stage1 .path )
112
+ return nil
113
+ }
114
+
115
+ // Not a genuine conflict and we can simply remove the file from the index
116
+ return gitRepo .RemoveFilesFromIndex (file .stage1 .path )
117
+ case file .stage1 == nil && file .stage2 != nil && (file .stage3 == nil || file .stage2 .SameAs (file .stage3 )):
118
+ // 2. Added in ours but not in theirs or identical in both
119
+ //
120
+ // Not a genuine conflict just add to the index
121
+ if err := gitRepo .AddObjectToIndex (file .stage2 .mode , git .MustIDFromString (file .stage2 .sha ), file .stage2 .path ); err != nil {
122
+ return err
123
+ }
124
+ return nil
125
+ case file .stage1 == nil && file .stage2 != nil && file .stage3 != nil && file .stage2 .sha == file .stage3 .sha && file .stage2 .mode != file .stage3 .mode :
126
+ // 3. Added in both with the same sha but the modes are different
127
+ //
128
+ // Conflict! (Not sure that this can actually happen but we should handle)
129
+ markConflict (file .stage2 .path )
130
+ return nil
131
+ case file .stage1 == nil && file .stage2 == nil && file .stage3 != nil :
132
+ // 4. Added in theirs but not ours:
133
+ //
134
+ // Not a genuine conflict just add to the index
135
+ return gitRepo .AddObjectToIndex (file .stage3 .mode , git .MustIDFromString (file .stage3 .sha ), file .stage3 .path )
136
+ case file .stage1 == nil :
137
+ // 5. Created by new in both
138
+ //
139
+ // Conflict!
140
+ markConflict (file .stage2 .path )
141
+ return nil
142
+ case file .stage2 != nil && file .stage3 != nil :
143
+ // 5. Modified in both - we should try to merge in the changes but first:
144
+ //
145
+ if file .stage2 .mode == "120000" || file .stage3 .mode == "120000" {
146
+ // 5a. Conflicting symbolic link change
147
+ markConflict (file .stage2 .path )
148
+ return nil
149
+ }
150
+ if file .stage2 .mode == "160000" || file .stage3 .mode == "160000" {
151
+ // 5b. Conflicting submodule change
152
+ markConflict (file .stage2 .path )
153
+ return nil
154
+ }
155
+ if file .stage2 .mode != file .stage3 .mode {
156
+ // 5c. Conflicting mode change
157
+ markConflict (file .stage2 .path )
158
+ return nil
159
+ }
160
+
161
+ // Need to get the objects from the object db to attempt to merge
162
+ root , err := git .NewCommandContext (ctx , "unpack-file" , file .stage1 .sha ).RunInDir (tmpBasePath )
163
+ if err != nil {
164
+ return fmt .Errorf ("unable to get root object: %s at path: %s for merging. Error: %w" , file .stage1 .sha , file .stage1 .path , err )
165
+ }
166
+ root = strings .TrimSpace (root )
167
+ defer util .Remove (root )
168
+ base , err := git .NewCommandContext (ctx , "unpack-file" , file .stage2 .sha ).RunInDir (tmpBasePath )
169
+ if err != nil {
170
+ return fmt .Errorf ("unable to get base object: %s at path: %s for merging. Error: %w" , file .stage2 .sha , file .stage2 .path , err )
171
+ }
172
+ base = strings .TrimSpace (base )
173
+ defer util .Remove (base )
174
+ head , err := git .NewCommandContext (ctx , "unpack-file" , file .stage3 .sha ).RunInDir (tmpBasePath )
175
+ if err != nil {
176
+ return fmt .Errorf ("unable to get head object:%s at path: %s for merging. Error: %w" , file .stage3 .sha , file .stage3 .path , err )
177
+ }
178
+ head = strings .TrimSpace (head )
179
+ defer util .Remove (head )
180
+
181
+ // now git merge-file annoyingly takes a different order to the merge-tree ...
182
+ _ , conflictErr := git .NewCommandContext (ctx , "merge-file" , base , root , head ).RunInDir (tmpBasePath )
183
+ if conflictErr != nil {
184
+ markConflict (file .stage2 .path )
185
+ return nil
186
+ }
187
+
188
+ // base now contains the merged data
189
+ hash , err := git .NewCommandContext (ctx , "hash-object" , "-w" , "--path" , file .stage2 .path , base ).RunInDir (tmpBasePath )
190
+ if err != nil {
191
+ return err
192
+ }
193
+ hash = strings .TrimSpace (hash )
194
+ return gitRepo .AddObjectToIndex (file .stage2 .mode , git .MustIDFromString (hash ), file .stage2 .path )
195
+ default :
196
+ if file .stage1 != nil {
197
+ markConflict (file .stage1 .path )
198
+ } else if file .stage2 != nil {
199
+ markConflict (file .stage2 .path )
200
+ } else if file .stage3 != nil {
201
+ markConflict (file .stage2 .path )
202
+ } else {
203
+ // This shouldn't happen!
204
+ }
205
+ }
206
+ return nil
207
+ }
208
+
101
209
func checkConflicts (pr * models.PullRequest , gitRepo * git.Repository , tmpBasePath string ) (bool , error ) {
102
- if _ , err := git .NewCommand ("read-tree" , "-m" , pr .MergeBase , "base" , "tracking" ).RunInDir (tmpBasePath ); err != nil {
210
+ 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 ))
211
+ defer finished ()
212
+
213
+ // First we use read-tree to do a simple three-way merge
214
+ if _ , err := git .NewCommandContext (ctx , "read-tree" , "-m" , pr .MergeBase , "base" , "tracking" ).RunInDir (tmpBasePath ); err != nil {
103
215
log .Error ("Unable to run read-tree -m! Error: %v" , err )
104
- return false , fmt .Errorf ("Unable to run read-tree -m! Error: %v" , err )
216
+ return false , fmt .Errorf ("unable to run read-tree -m! Error: %v" , err )
105
217
}
106
218
107
- diffReader , diffWriter , err := os .Pipe ()
108
- if err != nil {
109
- log .Error ("Unable to open stderr pipe: %v" , err )
110
- return false , fmt .Errorf ("Unable to open stderr pipe: %v" , err )
111
- }
219
+ // Then we use git ls-files -u to list the unmerged files and collate the triples in unmergedfiles
220
+ unmerged := make (chan * unmergedFile )
221
+ go unmergedFiles (ctx , tmpBasePath , unmerged )
222
+
112
223
defer func () {
113
- _ = diffReader .Close ()
114
- _ = diffWriter .Close ()
224
+ cancel ()
225
+ for range unmerged {
226
+ // empty the unmerged channel
227
+ }
115
228
}()
116
- stderr := & strings.Builder {}
117
229
118
- conflict := false
119
230
numberOfConflicts := 0
120
- err = git .NewCommand ("diff" , "--name-status" , "--diff-filter=U" ).
121
- RunInDirTimeoutEnvFullPipelineFunc (
122
- nil , - 1 , tmpBasePath ,
123
- diffWriter , stderr , nil ,
124
- func (ctx context.Context , cancel context.CancelFunc ) error {
125
- // Close the writer end of the pipe to begin processing
126
- _ = diffWriter .Close ()
127
- defer func () {
128
- // Close the reader on return to terminate the git command if necessary
129
- _ = diffReader .Close ()
130
- }()
131
-
132
- // Now scan the output from the command
133
- scanner := bufio .NewScanner (diffReader )
134
- for scanner .Scan () {
135
- line := scanner .Text ()
136
- split := strings .SplitN (line , "\t " , 2 )
137
- file := ""
138
- if len (split ) == 2 {
139
- file = split [1 ]
140
- }
141
-
142
- if file != "" {
143
- conflict = true
144
- if numberOfConflicts < 10 {
145
- pr .ConflictedFiles = append (pr .ConflictedFiles , file )
146
- }
147
- numberOfConflicts ++
148
- }
149
- }
231
+ conflict := false
232
+ markConflict := func (filepath string ) {
233
+ log .Trace ("Conflict: %s in PR[%d] %s/%s#%d" , filepath , pr .ID , pr .BaseRepo .OwnerName , pr .BaseRepo .Name , pr .Index )
234
+ conflict = true
235
+ if numberOfConflicts < 10 {
236
+ pr .ConflictedFiles = append (pr .ConflictedFiles , filepath )
237
+ }
238
+ numberOfConflicts ++
239
+ }
150
240
151
- return nil
152
- })
241
+ for file := range unmerged {
242
+ if file == nil {
243
+ break
244
+ }
245
+ if file .err != nil {
246
+ cancel ()
247
+ return false , file .err
248
+ }
153
249
154
- if err != nil {
155
- return false , fmt .Errorf ("git diff --name-status --filter=U: %v" , git .ConcatenateError (err , stderr .String ()))
250
+ // OK now we have the unmerged file triplet attempt to merge it
251
+ if err := attemptMerge (ctx , file , tmpBasePath , gitRepo , markConflict ); err != nil {
252
+ return false , err
253
+ }
156
254
}
157
255
158
256
if ! conflict {
159
257
return false , nil
160
258
}
161
259
162
- // OK read-tree has failed so we need to try a different thing
260
+ // OK read-tree has failed so we need to try a different thing - this might actually suceed where the above fails due to whitespace handling.
163
261
164
262
// 1. Create a plain patch from head to base
165
263
tmpPatchFile , err := os .CreateTemp ("" , "patch" )
166
264
if err != nil {
167
265
log .Error ("Unable to create temporary patch file! Error: %v" , err )
168
- return false , fmt .Errorf ("Unable to create temporary patch file! Error: %v" , err )
266
+ return false , fmt .Errorf ("unable to create temporary patch file! Error: %v" , err )
169
267
}
170
268
defer func () {
171
269
_ = util .Remove (tmpPatchFile .Name ())
@@ -174,12 +272,12 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
174
272
if err := gitRepo .GetDiffBinary (pr .MergeBase , "tracking" , tmpPatchFile ); err != nil {
175
273
tmpPatchFile .Close ()
176
274
log .Error ("Unable to get patch file from %s to %s in %s Error: %v" , pr .MergeBase , pr .HeadBranch , pr .BaseRepo .FullName (), err )
177
- 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 )
275
+ 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 )
178
276
}
179
277
stat , err := tmpPatchFile .Stat ()
180
278
if err != nil {
181
279
tmpPatchFile .Close ()
182
- return false , fmt .Errorf ("Unable to stat patch file: %v" , err )
280
+ return false , fmt .Errorf ("unable to stat patch file: %v" , err )
183
281
}
184
282
patchPath := tmpPatchFile .Name ()
185
283
tmpPatchFile .Close ()
@@ -216,6 +314,9 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
216
314
if prConfig .IgnoreWhitespaceConflicts {
217
315
args = append (args , "--ignore-whitespace" )
218
316
}
317
+ if git .CheckGitVersionAtLeast ("2.32.0" ) == nil {
318
+ args = append (args , "--3way" )
319
+ }
219
320
args = append (args , patchPath )
220
321
pr .ConflictedFiles = make ([]string , 0 , 5 )
221
322
@@ -230,7 +331,7 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
230
331
stderrReader , stderrWriter , err := os .Pipe ()
231
332
if err != nil {
232
333
log .Error ("Unable to open stderr pipe: %v" , err )
233
- return false , fmt .Errorf ("Unable to open stderr pipe: %v" , err )
334
+ return false , fmt .Errorf ("unable to open stderr pipe: %v" , err )
234
335
}
235
336
defer func () {
236
337
_ = stderrReader .Close ()
0 commit comments