@@ -11,12 +11,15 @@ import (
1111 "fmt"
1212 "io"
1313 "os"
14+ "path/filepath"
1415 "strings"
1516
1617 "code.gitea.io/gitea/models"
1718 "code.gitea.io/gitea/models/unit"
1819 "code.gitea.io/gitea/modules/git"
20+ "code.gitea.io/gitea/modules/graceful"
1921 "code.gitea.io/gitea/modules/log"
22+ "code.gitea.io/gitea/modules/process"
2023 "code.gitea.io/gitea/modules/util"
2124
2225 "github.com/gobwas/glob"
@@ -98,12 +101,193 @@ func TestPatch(pr *models.PullRequest) error {
98101 return nil
99102}
100103
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+
101216func 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+
102286 // 1. Create a plain patch from head to base
103287 tmpPatchFile , err := os .CreateTemp ("" , "patch" )
104288 if err != nil {
105289 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 )
107291 }
108292 defer func () {
109293 _ = util .Remove (tmpPatchFile .Name ())
@@ -112,12 +296,12 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
112296 if err := gitRepo .GetDiffBinary (pr .MergeBase , "tracking" , tmpPatchFile ); err != nil {
113297 tmpPatchFile .Close ()
114298 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 )
116300 }
117301 stat , err := tmpPatchFile .Stat ()
118302 if err != nil {
119303 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 )
121305 }
122306 patchPath := tmpPatchFile .Name ()
123307 tmpPatchFile .Close ()
@@ -154,6 +338,9 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
154338 if prConfig .IgnoreWhitespaceConflicts {
155339 args = append (args , "--ignore-whitespace" )
156340 }
341+ if git .CheckGitVersionAtLeast ("2.32.0" ) == nil {
342+ args = append (args , "--3way" )
343+ }
157344 args = append (args , patchPath )
158345 pr .ConflictedFiles = make ([]string , 0 , 5 )
159346
@@ -168,15 +355,15 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
168355 stderrReader , stderrWriter , err := os .Pipe ()
169356 if err != nil {
170357 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 )
172359 }
173360 defer func () {
174361 _ = stderrReader .Close ()
175362 _ = stderrWriter .Close ()
176363 }()
177364
178365 // 7. Run the check command
179- conflict : = false
366+ conflict = false
180367 err = git .NewCommand (args ... ).
181368 RunInDirTimeoutEnvFullPipelineFunc (
182369 nil , - 1 , tmpBasePath ,
0 commit comments