Skip to content

Commit 740796b

Browse files
authored
Merge branch 'main' into fix-18020
2 parents 893a32c + f1e8562 commit 740796b

File tree

7 files changed

+395
-6
lines changed

7 files changed

+395
-6
lines changed

modules/git/repo_compare.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,11 @@ func (repo *Repository) GetDiff(base, head string, w io.Writer) error {
237237

238238
// GetDiffBinary generates and returns patch data between given revisions, including binary diffs.
239239
func (repo *Repository) GetDiffBinary(base, head string, w io.Writer) error {
240-
return NewCommandContext(repo.Ctx, "diff", "-p", "--binary", base, head).
240+
if CheckGitVersionAtLeast("1.7.7") == nil {
241+
return NewCommandContext(repo.Ctx, "diff", "-p", "--binary", "--histogram", base, head).
242+
RunInDirPipeline(repo.Path, w, nil)
243+
}
244+
return NewCommandContext(repo.Ctx, "diff", "-p", "--binary", "--patience", base, head).
241245
RunInDirPipeline(repo.Path, w, nil)
242246
}
243247

options/gitignore/B4X

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**/Objects
2+
**/AutoBackups
3+
*.meta

options/gitignore/Go

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
14
# Binaries for programs and plugins
25
*.exe
36
*.exe~

options/gitignore/Gradle

+6
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ gradle-app.setting
1010

1111
# Cache of project
1212
.gradletasknamecache
13+
14+
# Eclipse Gradle plugin generated files
15+
# Eclipse Core
16+
.project
17+
# JDT-specific (Eclipse Java Development Tools)
18+
.classpath

options/gitignore/Maven

+6
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@ buildNumber.properties
99
.mvn/timing.properties
1010
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
1111
.mvn/wrapper/maven-wrapper.jar
12+
13+
# Eclipse m2e generated files
14+
# Eclipse Core
15+
.project
16+
# JDT-specific (Eclipse Java Development Tools)
17+
.classpath

services/pull/patch.go

+192-5
Original file line numberDiff line numberDiff line change
@@ -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+
101216
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+
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

Comments
 (0)