Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-04 03:06:03 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-04 03:06:03 +0300
commitb0838ee4f37f6f53b92ddbe08c0862f44ad9f8d3 (patch)
treef30629b2e4ea0aff4b47a2d1b6d4c6a4fc05a8e3
parentf59b942e9476914194331bb088b9ddfd78aa234f (diff)
parentaa4297c302139f2b27c4210b451d97c0f350186b (diff)
Automatic merge of gitlab-org/gitaly master
-rw-r--r--internal/git/localrepo/merge.go83
-rw-r--r--internal/git/localrepo/merge_test.go855
-rw-r--r--internal/gitaly/service/operations/merge.go2
3 files changed, 685 insertions, 255 deletions
diff --git a/internal/git/localrepo/merge.go b/internal/git/localrepo/merge.go
index 79f3bd60b..7fd38b22b 100644
--- a/internal/git/localrepo/merge.go
+++ b/internal/git/localrepo/merge.go
@@ -27,6 +27,11 @@ const (
MergeStageTheirs = MergeStage(3)
)
+// ErrMergeTreeUnrelatedHistory is used to denote the error when trying to merge two
+// trees without unrelated history. This occurs when we don't use set the
+// `allowUnrelatedHistories` option in the config.
+var ErrMergeTreeUnrelatedHistory = errors.New("unrelated histories")
+
type mergeTreeConfig struct {
allowUnrelatedHistories bool
conflictingFileNamesOnly bool
@@ -65,6 +70,7 @@ func (repo *Repo) MergeTree(
}
flags := []git.Option{
+ git.Flag{Name: "-z"},
git.Flag{Name: "--write-tree"},
}
@@ -100,9 +106,7 @@ func (repo *Repo) MergeTree(
if exitCode > 1 {
if text.ChompBytes(stderr.Bytes()) == "fatal: refusing to merge unrelated histories" {
- return "", &MergeTreeError{
- InfoMessage: "unrelated histories",
- }
+ return "", ErrMergeTreeUnrelatedHistory
}
return "", fmt.Errorf("merge-tree: %w", err)
}
@@ -110,7 +114,7 @@ func (repo *Repo) MergeTree(
return parseMergeTreeError(objectHash, config, stdout.String())
}
- oid, err := objectHash.FromHex(text.ChompBytes(stdout.Bytes()))
+ oid, err := objectHash.FromHex(strings.Split(stdout.String(), "\x00")[0])
if err != nil {
return "", fmt.Errorf("hex to oid: %w", err)
}
@@ -122,35 +126,32 @@ func (repo *Repo) MergeTree(
// a MergeTreeResult struct. The format for the output can be found at
// https://git-scm.com/docs/git-merge-tree#OUTPUT.
func parseMergeTreeError(objectHash git.ObjectHash, cfg mergeTreeConfig, output string) (git.ObjectID, error) {
- var mergeTreeError MergeTreeError
-
- lines := strings.SplitN(output, "\n\n", 2)
+ var mergeTreeConflictError MergeTreeConflictError
- // When the output is of unexpected length
- if len(lines) < 2 {
- return "", errors.New("error parsing merge tree result")
+ oidAndConflictsBuf, infoMsg, ok := strings.Cut(output, "\x00\x00")
+ if !ok {
+ return "", fmt.Errorf("couldn't parse merge tree output: %s", output)
}
- mergeTreeError.InfoMessage = strings.TrimSuffix(lines[1], "\n")
- oidAndConflicts := strings.Split(lines[0], "\n")
-
+ oidAndConflicts := strings.Split(oidAndConflictsBuf, "\x00")
+ // When the output is of unexpected length
if len(oidAndConflicts) < 2 {
- return "", &mergeTreeError
+ return "", errors.New("couldn't split oid and file info")
}
- oid, err := objectHash.FromHex(text.ChompBytes([]byte(oidAndConflicts[0])))
+ oid, err := objectHash.FromHex(oidAndConflicts[0])
if err != nil {
return "", fmt.Errorf("hex to oid: %w", err)
}
- mergeTreeError.ConflictingFileInfo = make([]ConflictingFileInfo, len(oidAndConflicts[1:]))
+ mergeTreeConflictError.ConflictingFileInfo = make([]ConflictingFileInfo, len(oidAndConflicts[1:]))
// From git-merge-tree(1), the information is of the format `<mode> <object> <stage> <filename>`
// unless the `--name-only` option is used, in which case only the filename is output.
// Note: that there is \t before the filename (https://gitlab.com/gitlab-org/git/blob/v2.40.0/builtin/merge-tree.c#L481)
for i, infoLine := range oidAndConflicts[1:] {
if cfg.conflictingFileNamesOnly {
- mergeTreeError.ConflictingFileInfo[i].FileName = infoLine
+ mergeTreeConflictError.ConflictingFileInfo[i].FileName = infoLine
} else {
infoAndFilename := strings.Split(infoLine, "\t")
if len(infoAndFilename) != 2 {
@@ -162,7 +163,7 @@ func parseMergeTreeError(objectHash git.ObjectHash, cfg mergeTreeConfig, output
return "", fmt.Errorf("parsing conflicting file info: %s", infoLine)
}
- mergeTreeError.ConflictingFileInfo[i].OID, err = objectHash.FromHex(info[1])
+ mergeTreeConflictError.ConflictingFileInfo[i].OID, err = objectHash.FromHex(info[1])
if err != nil {
return "", fmt.Errorf("hex to oid: %w", err)
}
@@ -176,12 +177,37 @@ func parseMergeTreeError(objectHash git.ObjectHash, cfg mergeTreeConfig, output
return "", fmt.Errorf("invalid value for stage: %d", stage)
}
- mergeTreeError.ConflictingFileInfo[i].Stage = MergeStage(stage)
- mergeTreeError.ConflictingFileInfo[i].FileName = infoAndFilename[1]
+ mergeTreeConflictError.ConflictingFileInfo[i].Stage = MergeStage(stage)
+ mergeTreeConflictError.ConflictingFileInfo[i].FileName = infoAndFilename[1]
}
}
- return oid, &mergeTreeError
+ fields := strings.Split(infoMsg, "\x00")
+ // The git output contains a null characted at the end, which creates a stray empty field.
+ fields = fields[:len(fields)-1]
+
+ for i := 0; i < len(fields); {
+ c := ConflictInfoMessage{}
+
+ numOfPaths, err := strconv.Atoi(fields[i])
+ if err != nil {
+ return "", fmt.Errorf("converting stage to int: %w", err)
+ }
+
+ if i+numOfPaths+2 > len(fields) {
+ return "", fmt.Errorf("incorrect number of fields: %s", infoMsg)
+ }
+
+ c.Paths = fields[i+1 : i+numOfPaths+1]
+ c.Type = fields[i+numOfPaths+1]
+ c.Message = fields[i+numOfPaths+2]
+
+ mergeTreeConflictError.ConflictInfoMessage = append(mergeTreeConflictError.ConflictInfoMessage, c)
+
+ i = i + numOfPaths + 3
+ }
+
+ return oid, &mergeTreeConflictError
}
// ConflictingFileInfo holds the conflicting file info output from git-merge-tree(1).
@@ -191,15 +217,22 @@ type ConflictingFileInfo struct {
Stage MergeStage
}
-// MergeTreeError encapsulates any conflicting file info and messages that occur
+// ConflictInfoMessage holds the information message output from git-merge-tree(1).
+type ConflictInfoMessage struct {
+ Paths []string
+ Type string
+ Message string
+}
+
+// MergeTreeConflictError encapsulates any conflicting file info and messages that occur
// when a merge-tree(1) command fails.
-type MergeTreeError struct {
+type MergeTreeConflictError struct {
ConflictingFileInfo []ConflictingFileInfo
- InfoMessage string
+ ConflictInfoMessage []ConflictInfoMessage
}
// Error returns the error string for a conflict error.
-func (c *MergeTreeError) Error() string {
+func (c *MergeTreeConflictError) Error() string {
// TODO: for now, it's better that this error matches the git2go
// error but once we deprecate the git2go code path in
// merges, we can change this error to print out the conflicting files
diff --git a/internal/git/localrepo/merge_test.go b/internal/git/localrepo/merge_test.go
index 39426f229..4f00d7e74 100644
--- a/internal/git/localrepo/merge_test.go
+++ b/internal/git/localrepo/merge_test.go
@@ -1,10 +1,10 @@
package localrepo
import (
- "context"
"errors"
"fmt"
"strconv"
+ "strings"
"testing"
"github.com/stretchr/testify/require"
@@ -15,16 +15,29 @@ import (
)
func TestMergeTree(t *testing.T) {
+ t.Parallel()
+
cfg := testcfg.Build(t)
+ type setupData struct {
+ ours git.ObjectID
+ theirs git.ObjectID
+
+ expectedTreeEntries []gittest.TreeEntry
+ expectedErr error
+ }
+
testCases := []struct {
- desc string
- expectedErr error
- setupFunc func(t *testing.T, ctx context.Context, repoPath string) (ours, theirs git.ObjectID, expectedTreeEntries []gittest.TreeEntry)
+ desc string
+ mergeTreeOptions []MergeTreeOption
+ setup func(t *testing.T, repoPath string) setupData
}{
{
desc: "normal merge",
- setupFunc: func(t *testing.T, ctx context.Context, repoPath string) (git.ObjectID, git.ObjectID, []gittest.TreeEntry) {
+ mergeTreeOptions: []MergeTreeOption{
+ WithConflictingFileNamesOnly(),
+ },
+ setup: func(t *testing.T, repoPath string) setupData {
tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
Mode: "100644",
@@ -32,11 +45,7 @@ func TestMergeTree(t *testing.T) {
Content: "foo",
},
})
- baseCommit := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithCommitterName("Andy"),
- gittest.WithAuthorName("Andy"),
- gittest.WithTree(tree1),
- )
+ baseCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree1))
tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
Mode: "100644",
@@ -61,44 +70,65 @@ func TestMergeTree(t *testing.T) {
Content: "bar",
},
})
- ours := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithTree(tree2),
- gittest.WithParents(baseCommit),
- gittest.WithAuthorName("Woody"),
- gittest.WithCommitterName("Woody"),
- )
- theirs := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithTree(tree3),
- gittest.WithParents(baseCommit),
- gittest.WithAuthorName("Buzz"),
- gittest.WithCommitterName("Buzz"),
- )
+ ours := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree2), gittest.WithParents(baseCommit))
+ theirs := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree3), gittest.WithParents(baseCommit))
- return ours, theirs, []gittest.TreeEntry{
+ return setupData{
+ ours: ours,
+ theirs: theirs,
+ expectedTreeEntries: []gittest.TreeEntry{
+ {
+ Mode: "100644",
+ Path: "file1",
+ Content: "foo",
+ },
+ {
+ Mode: "100644",
+ Path: "file2",
+ Content: "baz",
+ },
+ {
+ Mode: "100644",
+ Path: "file3",
+ Content: "bar",
+ },
+ },
+ }
+ },
+ },
+ {
+ desc: "no shared ancestors",
+ mergeTreeOptions: []MergeTreeOption{
+ WithConflictingFileNamesOnly(),
+ },
+ setup: func(t *testing.T, repoPath string) setupData {
+ tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
Mode: "100644",
Path: "file1",
Content: "foo",
},
+ })
+ tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
Mode: "100644",
Path: "file2",
Content: "baz",
},
- {
- Mode: "100644",
- Path: "file3",
- Content: "bar",
- },
+ })
+ ours := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree1))
+ theirs := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree2))
+
+ return setupData{
+ ours: ours,
+ theirs: theirs,
+ expectedErr: ErrMergeTreeUnrelatedHistory,
}
},
},
{
- desc: "no shared ancestors",
- expectedErr: &MergeTreeError{
- InfoMessage: "unrelated histories",
- },
- setupFunc: func(t *testing.T, ctx context.Context, repoPath string) (git.ObjectID, git.ObjectID, []gittest.TreeEntry) {
+ desc: "with single file conflict",
+ setup: func(t *testing.T, repoPath string) setupData {
tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
Mode: "100644",
@@ -106,40 +136,152 @@ func TestMergeTree(t *testing.T) {
Content: "foo",
},
})
+ baseCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree1))
+ blob1 := gittest.WriteBlob(t, cfg, repoPath, []byte("baz"))
tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
Mode: "100644",
- Path: "file2",
- Content: "baz",
+ Path: "file1",
+ Content: "foo",
+ },
+ {
+ OID: blob1,
+ Mode: "100644",
+ Path: "file2",
},
})
- ours := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithTree(tree1),
- gittest.WithAuthorName("Woody"),
- gittest.WithCommitterName("Woody"),
- )
- theirs := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithTree(tree2),
- gittest.WithAuthorName("Buzz"),
- gittest.WithCommitterName("Buzz"),
- )
+ blob2 := gittest.WriteBlob(t, cfg, repoPath, []byte("bar"))
+ tree3 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ Mode: "100644",
+ Path: "file1",
+ Content: "foo",
+ },
+ {
+ OID: blob2,
+ Mode: "100644",
+ Path: "file2",
+ },
+ })
+ ours := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree2), gittest.WithParents(baseCommit))
+ theirs := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree3), gittest.WithParents(baseCommit))
- return ours, theirs, nil
+ return setupData{
+ ours: ours,
+ theirs: theirs,
+ expectedErr: &MergeTreeConflictError{
+ ConflictingFileInfo: []ConflictingFileInfo{
+ {
+ FileName: "file2",
+ OID: blob1,
+ Stage: MergeStageOurs,
+ },
+ {
+ FileName: "file2",
+ OID: blob2,
+ Stage: MergeStageTheirs,
+ },
+ },
+ ConflictInfoMessage: []ConflictInfoMessage{
+ {
+ Paths: []string{"file2"},
+ Type: "Auto-merging",
+ Message: "Auto-merging file2\n",
+ },
+ {
+ Paths: []string{"file2"},
+ Type: "CONFLICT (contents)",
+ Message: "CONFLICT (add/add): Merge conflict in file2\n",
+ },
+ },
+ },
+ }
},
},
{
- desc: "with conflicts",
- expectedErr: &MergeTreeError{
- ConflictingFileInfo: []ConflictingFileInfo{
+ desc: "with single file conflict with ancestor",
+ setup: func(t *testing.T, repoPath string) setupData {
+ blob1 := gittest.WriteBlob(t, cfg, repoPath, []byte("foo"))
+ tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob1,
+ Mode: "100644",
+ Path: "file1",
+ },
+ })
+ baseCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree1))
+
+ blob2 := gittest.WriteBlob(t, cfg, repoPath, []byte("baz"))
+ tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob2,
+ Mode: "100644",
+ Path: "file1",
+ },
+ {
+ Mode: "100644",
+ Path: "file2",
+ Content: "goo",
+ },
+ })
+
+ blob3 := gittest.WriteBlob(t, cfg, repoPath, []byte("bar"))
+ tree3 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob3,
+ Mode: "100644",
+ Path: "file1",
+ },
{
- FileName: "file2",
- OID: "",
- Stage: 0,
+ Mode: "100644",
+ Path: "file2",
+ Content: "goo",
+ },
+ })
+
+ ours := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree2), gittest.WithParents(baseCommit))
+ theirs := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree3), gittest.WithParents(baseCommit))
+
+ return setupData{
+ ours: ours,
+ theirs: theirs,
+ expectedErr: &MergeTreeConflictError{
+ ConflictingFileInfo: []ConflictingFileInfo{
+ {
+ FileName: "file1",
+ OID: blob1,
+ Stage: MergeStageAncestor,
+ },
+ {
+ FileName: "file1",
+ OID: blob2,
+ Stage: MergeStageOurs,
+ },
+ {
+ FileName: "file1",
+ OID: blob3,
+ Stage: MergeStageTheirs,
+ },
+ },
+ ConflictInfoMessage: []ConflictInfoMessage{
+ {
+ Paths: []string{"file1"},
+ Type: "Auto-merging",
+ Message: "Auto-merging file1\n",
+ },
+ {
+ Paths: []string{"file1"},
+ Type: "CONFLICT (contents)",
+ Message: "CONFLICT (content): Merge conflict in file1\n",
+ },
+ },
},
- },
- InfoMessage: "Auto-merging file2\nCONFLICT (add/add): Merge conflict in file2",
+ }
},
- setupFunc: func(t *testing.T, ctx context.Context, repoPath string) (git.ObjectID, git.ObjectID, []gittest.TreeEntry) {
+ },
+ {
+ desc: "with single file conflict due to rename",
+ setup: func(t *testing.T, repoPath string) setupData {
tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
Mode: "100644",
@@ -147,55 +289,393 @@ func TestMergeTree(t *testing.T) {
Content: "foo",
},
})
- baseCommit := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithCommitterName("Andy"),
- gittest.WithAuthorName("Andy"),
- gittest.WithTree(tree1),
- )
+ baseCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree1))
+
+ blob2 := gittest.WriteBlob(t, cfg, repoPath, []byte("foo"))
tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
+ OID: blob2,
+ Mode: "100644",
+ Path: "file2",
+ },
+ })
+
+ blob3 := gittest.WriteBlob(t, cfg, repoPath, []byte("bar"))
+ tree3 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob3,
+ Mode: "100644",
+ Path: "file3",
+ },
+ })
+
+ ours := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree2), gittest.WithParents(baseCommit))
+ theirs := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree3), gittest.WithParents(baseCommit))
+
+ return setupData{
+ ours: ours,
+ theirs: theirs,
+ expectedErr: &MergeTreeConflictError{
+ ConflictingFileInfo: []ConflictingFileInfo{
+ {
+ FileName: "file2",
+ OID: blob2,
+ Stage: MergeStageAncestor,
+ },
+ {
+ FileName: "file2",
+ OID: blob2,
+ Stage: MergeStageOurs,
+ },
+ },
+ ConflictInfoMessage: []ConflictInfoMessage{
+ {
+ Paths: []string{"file2", "file1"},
+ Type: "CONFLICT (rename/delete)",
+ Message: fmt.Sprintf("CONFLICT (rename/delete): file1 renamed to file2 in %s, but deleted in %s.\n", ours, theirs),
+ },
+ },
+ },
+ }
+ },
+ },
+ {
+ desc: "with multiple file conflicts",
+ setup: func(t *testing.T, repoPath string) setupData {
+ tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
Mode: "100644",
Path: "file1",
Content: "foo",
},
+ })
+ baseCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree1))
+
+ blob2a := gittest.WriteBlob(t, cfg, repoPath, []byte("baz"))
+ blob2b := gittest.WriteBlob(t, cfg, repoPath, []byte("xyz"))
+ tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
- Mode: "100644",
- Path: "file2",
- Content: "baz",
+ OID: blob2a,
+ Mode: "100644",
+ Path: "file2",
+ },
+ {
+ OID: blob2b,
+ Mode: "100644",
+ Path: "file3",
},
})
+
+ blob3a := gittest.WriteBlob(t, cfg, repoPath, []byte("bar"))
+ blob3b := gittest.WriteBlob(t, cfg, repoPath, []byte("mno"))
tree3 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
+ OID: blob3a,
+ Mode: "100644",
+ Path: "file2",
+ },
+ {
+ OID: blob3b,
+ Mode: "100644",
+ Path: "file3",
+ },
+ })
+
+ ours := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree2), gittest.WithParents(baseCommit))
+ theirs := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree3), gittest.WithParents(baseCommit))
+
+ return setupData{
+ ours: ours,
+ theirs: theirs,
+ expectedErr: &MergeTreeConflictError{
+ ConflictingFileInfo: []ConflictingFileInfo{
+ {
+ FileName: "file2",
+ OID: blob2a,
+ Stage: MergeStageOurs,
+ },
+ {
+ FileName: "file2",
+ OID: blob3a,
+ Stage: MergeStageTheirs,
+ },
+ {
+ FileName: "file3",
+ OID: blob2b,
+ Stage: MergeStageOurs,
+ },
+ {
+ FileName: "file3",
+ OID: blob3b,
+ Stage: MergeStageTheirs,
+ },
+ },
+ ConflictInfoMessage: []ConflictInfoMessage{
+ {
+ Paths: []string{"file2"},
+ Type: "Auto-merging",
+ Message: "Auto-merging file2\n",
+ },
+ {
+ Paths: []string{"file2"},
+ Type: "CONFLICT (contents)",
+ Message: "CONFLICT (add/add): Merge conflict in file2\n",
+ },
+ {
+ Paths: []string{"file3"},
+ Type: "Auto-merging",
+ Message: "Auto-merging file3\n",
+ },
+ {
+ Paths: []string{"file3"},
+ Type: "CONFLICT (contents)",
+ Message: "CONFLICT (add/add): Merge conflict in file3\n",
+ },
+ },
+ },
+ }
+ },
+ },
+ {
+ desc: "with multiple file conflicts and common ancestor",
+ setup: func(t *testing.T, repoPath string) setupData {
+ blob := gittest.WriteBlob(t, cfg, repoPath, []byte("foo"))
+ tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob,
+ Mode: "100644",
+ Path: "file1",
+ },
+ })
+ baseCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree1))
+
+ blob2a := gittest.WriteBlob(t, cfg, repoPath, []byte("baz"))
+ blob2b := gittest.WriteBlob(t, cfg, repoPath, []byte("xyz"))
+ tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob2a,
+ Mode: "100644",
+ Path: "file1",
+ },
+ {
+ OID: blob2b,
+ Mode: "100644",
+ Path: "file2",
+ },
+ })
+
+ blob3a := gittest.WriteBlob(t, cfg, repoPath, []byte("bar"))
+ blob3b := gittest.WriteBlob(t, cfg, repoPath, []byte("mno"))
+ tree3 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob3a,
+ Mode: "100644",
+ Path: "file1",
+ },
+ {
+ OID: blob3b,
+ Mode: "100644",
+ Path: "file2",
+ },
+ })
+
+ ours := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree2), gittest.WithParents(baseCommit))
+ theirs := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree3), gittest.WithParents(baseCommit))
+
+ return setupData{
+ ours: ours,
+ theirs: theirs,
+ expectedErr: &MergeTreeConflictError{
+ ConflictingFileInfo: []ConflictingFileInfo{
+ {
+ FileName: "file1",
+ OID: blob,
+ Stage: MergeStageAncestor,
+ },
+ {
+ FileName: "file1",
+ OID: blob2a,
+ Stage: MergeStageOurs,
+ },
+ {
+ FileName: "file1",
+ OID: blob3a,
+ Stage: MergeStageTheirs,
+ },
+ {
+ FileName: "file2",
+ OID: blob2b,
+ Stage: MergeStageOurs,
+ },
+ {
+ FileName: "file2",
+ OID: blob3b,
+ Stage: MergeStageTheirs,
+ },
+ },
+ ConflictInfoMessage: []ConflictInfoMessage{
+ {
+ Paths: []string{"file1"},
+ Type: "Auto-merging",
+ Message: "Auto-merging file1\n",
+ },
+ {
+ Paths: []string{"file1"},
+ Type: "CONFLICT (contents)",
+ Message: "CONFLICT (content): Merge conflict in file1\n",
+ },
+ {
+ Paths: []string{"file2"},
+ Type: "Auto-merging",
+ Message: "Auto-merging file2\n",
+ },
+ {
+ Paths: []string{"file2"},
+ Type: "CONFLICT (contents)",
+ Message: "CONFLICT (add/add): Merge conflict in file2\n",
+ },
+ },
+ },
+ }
+ },
+ },
+ {
+ desc: "with multiple file conflicts due to file deletion",
+ setup: func(t *testing.T, repoPath string) setupData {
+ blob := gittest.WriteBlob(t, cfg, repoPath, []byte("foo"))
+ tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob,
+ Mode: "100644",
+ Path: "file1",
+ },
+ })
+ baseCommit := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree1))
+
+ blob2b := gittest.WriteBlob(t, cfg, repoPath, []byte("xyz"))
+ tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob2b,
+ Mode: "100644",
+ Path: "file2",
+ },
+ })
+
+ blob3a := gittest.WriteBlob(t, cfg, repoPath, []byte("bar"))
+ blob3b := gittest.WriteBlob(t, cfg, repoPath, []byte("mno"))
+ tree3 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
+ OID: blob3a,
+ Mode: "100644",
+ Path: "file1",
+ },
+ {
+ OID: blob3b,
+ Mode: "100644",
+ Path: "file2",
+ },
+ })
+
+ ours := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree2), gittest.WithParents(baseCommit))
+ theirs := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree3), gittest.WithParents(baseCommit))
+
+ return setupData{
+ ours: ours,
+ theirs: theirs,
+ expectedErr: &MergeTreeConflictError{
+ ConflictingFileInfo: []ConflictingFileInfo{
+ {
+ FileName: "file1",
+ OID: blob,
+ Stage: MergeStageAncestor,
+ },
+ {
+ FileName: "file1",
+ OID: blob3a,
+ Stage: MergeStageTheirs,
+ },
+ {
+ FileName: "file2",
+ OID: blob2b,
+ Stage: MergeStageOurs,
+ },
+ {
+ FileName: "file2",
+ OID: blob3b,
+ Stage: MergeStageTheirs,
+ },
+ },
+ ConflictInfoMessage: []ConflictInfoMessage{
+ {
+ Paths: []string{"file1"},
+ Type: "CONFLICT (modify/delete)",
+ Message: fmt.Sprintf("CONFLICT (modify/delete): file1 deleted in %s and modified in %s. Version %s of file1 left in tree.\n", ours, theirs, theirs),
+ },
+ {
+ Paths: []string{"file2"},
+ Type: "Auto-merging",
+ Message: "Auto-merging file2\n",
+ },
+ {
+ Paths: []string{"file2"},
+ Type: "CONFLICT (contents)",
+ Message: "CONFLICT (add/add): Merge conflict in file2\n",
+ },
+ },
+ },
+ }
+ },
+ },
+ {
+ desc: "allow unrelated histories",
+ mergeTreeOptions: []MergeTreeOption{
+ WithConflictingFileNamesOnly(),
+ WithAllowUnrelatedHistories(),
+ },
+ setup: func(t *testing.T, repoPath string) setupData {
+ tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {
Mode: "100644",
Path: "file1",
Content: "foo",
},
+ })
+ tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
{
Mode: "100644",
Path: "file2",
- Content: "bar",
+ Content: "baz",
},
})
- ours := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithTree(tree2),
- gittest.WithParents(baseCommit),
- gittest.WithAuthorName("Woody"),
- gittest.WithCommitterName("Woody"),
- )
- theirs := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithTree(tree3),
- gittest.WithParents(baseCommit),
- gittest.WithAuthorName("Buzz"),
- gittest.WithCommitterName("Buzz"),
- )
+ ours := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree1))
+ theirs := gittest.WriteCommit(t, cfg, repoPath, gittest.WithTree(tree2))
- return ours, theirs, nil
+ return setupData{
+ ours: ours,
+ theirs: theirs,
+ expectedTreeEntries: []gittest.TreeEntry{
+ {
+ Mode: "100644",
+ Path: "file1",
+ Content: "foo",
+ },
+ {
+ Mode: "100644",
+ Path: "file2",
+ Content: "baz",
+ },
+ },
+ }
},
},
}
for _, tc := range testCases {
+ tc := tc
+
t.Run(tc.desc, func(t *testing.T) {
+ t.Parallel()
+
ctx := testhelper.Context(t)
repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{
@@ -203,83 +683,22 @@ func TestMergeTree(t *testing.T) {
})
repo := NewTestRepo(t, cfg, repoProto)
- ours, theirs, treeEntries := tc.setupFunc(t, ctx, repoPath)
+ data := tc.setup(t, repoPath)
- mergeTreeResult, err := repo.MergeTree(ctx, string(ours), string(theirs), WithConflictingFileNamesOnly())
+ mergeTreeResult, err := repo.MergeTree(ctx, string(data.ours), string(data.theirs), tc.mergeTreeOptions...)
- require.Equal(t, tc.expectedErr, err)
- if tc.expectedErr == nil {
+ require.Equal(t, data.expectedErr, err)
+ if data.expectedErr == nil {
gittest.RequireTree(
t,
cfg,
repoPath,
string(mergeTreeResult),
- treeEntries,
+ data.expectedTreeEntries,
)
}
})
}
-
- t.Run("allow unrelated histories", func(t *testing.T) {
- ctx := testhelper.Context(t)
-
- repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{
- SkipCreationViaService: true,
- })
- repo := NewTestRepo(t, cfg, repoProto)
-
- tree1 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {
- Mode: "100644",
- Path: "file1",
- Content: "foo",
- },
- })
- tree2 := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {
- Mode: "100644",
- Path: "file2",
- Content: "baz",
- },
- })
- ours := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithTree(tree1),
- gittest.WithAuthorName("Woody"),
- gittest.WithCommitterName("Woody"),
- )
- theirs := gittest.WriteCommit(t, cfg, repoPath,
- gittest.WithTree(tree2),
- gittest.WithAuthorName("Buzz"),
- gittest.WithCommitterName("Buzz"),
- )
-
- mergeTreeResult, err := repo.MergeTree(
- ctx,
- string(ours),
- string(theirs),
- WithAllowUnrelatedHistories(),
- )
- require.NoError(t, err)
-
- gittest.RequireTree(
- t,
- cfg,
- repoPath,
- string(mergeTreeResult),
- []gittest.TreeEntry{
- {
- Mode: "100644",
- Path: "file1",
- Content: "foo",
- },
- {
- Mode: "100644",
- Path: "file2",
- Content: "baz",
- },
- },
- )
- })
}
func TestParseResult(t *testing.T) {
@@ -290,102 +709,74 @@ func TestParseResult(t *testing.T) {
expectedErr error
}{
{
- desc: "single file conflict",
- output: fmt.Sprintf(`%s
-100644 %s 2%sfile
-100644 %s 3%sfile
-
-Auto-merging file
-CONFLICT (content): Merge conflict in file
-`, gittest.DefaultObjectHash.EmptyTreeOID, gittest.DefaultObjectHash.EmptyTreeOID, "\t", gittest.DefaultObjectHash.EmptyTreeOID, "\t"),
- oid: gittest.DefaultObjectHash.EmptyTreeOID,
- expectedErr: &MergeTreeError{
- ConflictingFileInfo: []ConflictingFileInfo{
- {
- FileName: "file",
- OID: gittest.DefaultObjectHash.EmptyTreeOID,
- Stage: MergeStageOurs,
- },
- {
- FileName: "file",
- OID: gittest.DefaultObjectHash.EmptyTreeOID,
- Stage: MergeStageTheirs,
- },
- },
- InfoMessage: "Auto-merging file\nCONFLICT (content): Merge conflict in file",
- },
- },
- {
- desc: "multiple files conflict",
- output: fmt.Sprintf(`%s
-100644 %s 2%sfile1
-100644 %s 3%sfile2
-
-Auto-merging file
-CONFLICT (content): Merge conflict in file1
-`, gittest.DefaultObjectHash.EmptyTreeOID, gittest.DefaultObjectHash.EmptyTreeOID, "\t", gittest.DefaultObjectHash.EmptyTreeOID, "\t"),
- oid: gittest.DefaultObjectHash.EmptyTreeOID,
- expectedErr: &MergeTreeError{
- ConflictingFileInfo: []ConflictingFileInfo{
- {
- FileName: "file1",
- OID: gittest.DefaultObjectHash.EmptyTreeOID,
- Stage: MergeStageOurs,
- },
- {
- FileName: "file2",
- OID: gittest.DefaultObjectHash.EmptyTreeOID,
- Stage: MergeStageTheirs,
- },
- },
- InfoMessage: "Auto-merging file\nCONFLICT (content): Merge conflict in file1",
- },
- },
- {
desc: "no tab in conflicting file info",
- output: fmt.Sprintf(`%s
-100644 %s 2 file1
-
-Auto-merging file
-CONFLICT (content): Merge conflict in file1
-`, gittest.DefaultObjectHash.EmptyTreeOID, gittest.DefaultObjectHash.EmptyTreeOID),
+ output: strings.Join([]string{
+ gittest.DefaultObjectHash.EmptyTreeOID.String(),
+ fmt.Sprintf("100644 %s 1a", gittest.DefaultObjectHash.EmptyTreeOID),
+ fmt.Sprintf("100644 %s 2\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ fmt.Sprintf("100644 %s 3\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ "",
+ "1",
+ "a",
+ "Auto-merging",
+ "Auto-merging a\n",
+ "",
+ }, "\x00"),
oid: gittest.DefaultObjectHash.EmptyTreeOID,
- expectedErr: fmt.Errorf("parsing conflicting file info: 100644 %s 2 file1", gittest.DefaultObjectHash.EmptyTreeOID),
+ expectedErr: fmt.Errorf("parsing conflicting file info: 100644 %s 1a", gittest.DefaultObjectHash.EmptyTreeOID),
},
{
desc: "incorrect number of fields in conflicting file info",
- output: fmt.Sprintf(`%s
-%s 2%sfile1
-
-Auto-merging file
-CONFLICT (content): Merge conflict in file1
-`, gittest.DefaultObjectHash.EmptyTreeOID, gittest.DefaultObjectHash.EmptyTreeOID, "\t"),
+ output: strings.Join([]string{
+ gittest.DefaultObjectHash.EmptyTreeOID.String(),
+ fmt.Sprintf("100644 %s 1\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ fmt.Sprintf("%s 2\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ fmt.Sprintf("100644 %s 3\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ "",
+ "1",
+ "a",
+ "Auto-merging",
+ "Auto-merging a\n",
+ "",
+ }, "\x00"),
oid: gittest.DefaultObjectHash.EmptyTreeOID,
- expectedErr: fmt.Errorf("parsing conflicting file info: %s 2\tfile1", gittest.DefaultObjectHash.EmptyTreeOID),
+ expectedErr: fmt.Errorf("parsing conflicting file info: %s 2\ta", gittest.DefaultObjectHash.EmptyTreeOID),
},
{
desc: "invalid OID in conflicting file info",
- output: fmt.Sprintf(`%s
-100644 23 2%sfile1
-
-Auto-merging file
-CONFLICT (content): Merge conflict in file1
-`, gittest.DefaultObjectHash.EmptyTreeOID, "\t"),
+ output: strings.Join([]string{
+ gittest.DefaultObjectHash.EmptyTreeOID.String(),
+ fmt.Sprintf("100644 %s 1\ta", "$$$"),
+ fmt.Sprintf("100644 %s 2\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ fmt.Sprintf("100644 %s 3\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ "",
+ "1",
+ "a",
+ "Auto-merging",
+ "Auto-merging a\n",
+ "",
+ }, "\x00"),
oid: gittest.DefaultObjectHash.EmptyTreeOID,
expectedErr: fmt.Errorf("hex to oid: %w", git.InvalidObjectIDLengthError{
- OID: "23",
+ OID: "$$$",
CorrectLength: gittest.DefaultObjectHash.EncodedLen(),
- Length: 2,
+ Length: 3,
}),
},
{
desc: "invalid stage type in conflicting file info",
- output: fmt.Sprintf(`%s
-100644 %s foo%sfile1
-
-Auto-merging file
-CONFLICT (content): Merge conflict in file1
-`, gittest.DefaultObjectHash.EmptyTreeOID, gittest.DefaultObjectHash.EmptyTreeOID, "\t"),
+ output: strings.Join([]string{
+ gittest.DefaultObjectHash.EmptyTreeOID.String(),
+ fmt.Sprintf("100644 %s foo\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ fmt.Sprintf("100644 %s 2\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ fmt.Sprintf("100644 %s 3\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ "",
+ "1",
+ "a",
+ "Auto-merging",
+ "Auto-merging a\n",
+ "",
+ }, "\x00"),
oid: gittest.DefaultObjectHash.EmptyTreeOID,
expectedErr: fmt.Errorf("converting stage to int: %w", &strconv.NumError{
Func: "Atoi",
@@ -395,12 +786,18 @@ CONFLICT (content): Merge conflict in file1
},
{
desc: "invalid stage value in conflicting file info",
- output: fmt.Sprintf(`%s
-100644 %s 5%sfile1
-
-Auto-merging file
-CONFLICT (content): Merge conflict in file1
-`, gittest.DefaultObjectHash.EmptyTreeOID, gittest.DefaultObjectHash.EmptyTreeOID, "\t"),
+ output: strings.Join([]string{
+ gittest.DefaultObjectHash.EmptyTreeOID.String(),
+ fmt.Sprintf("100644 %s 5\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ fmt.Sprintf("100644 %s 2\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ fmt.Sprintf("100644 %s 3\ta", gittest.DefaultObjectHash.EmptyTreeOID),
+ "",
+ "1",
+ "a",
+ "Auto-merging",
+ "Auto-merging a\n",
+ "",
+ }, "\x00"),
oid: gittest.DefaultObjectHash.EmptyTreeOID,
expectedErr: fmt.Errorf("invalid value for stage: 5"),
},
diff --git a/internal/gitaly/service/operations/merge.go b/internal/gitaly/service/operations/merge.go
index a35392b9a..4b0340eb6 100644
--- a/internal/gitaly/service/operations/merge.go
+++ b/internal/gitaly/service/operations/merge.go
@@ -154,7 +154,7 @@ func (s *Server) UserMergeBranch(stream gitalypb.OperationService_UserMergeBranc
firstRequest.CommitId)
if mergeErr != nil {
- var conflictErr *localrepo.MergeTreeError
+ var conflictErr *localrepo.MergeTreeConflictError
if errors.As(mergeErr, &conflictErr) {
conflictingFiles := make([][]byte, 0, len(conflictErr.ConflictingFileInfo))
for _, conflictingFileInfo := range conflictErr.ConflictingFileInfo {