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:
authorPatrick Steinhardt <psteinhardt@gitlab.com>2020-09-22 12:11:21 +0300
committerPatrick Steinhardt <psteinhardt@gitlab.com>2020-10-12 11:00:24 +0300
commit6d9126524f23fe27fafc1d9a5c0b24a6f4a97de5 (patch)
tree5af04767a28e3702cb25c90d01c74787153cd936 /internal/git2go
parent5c7314644639343d067fcd47c8b1ece8e500232e (diff)
git2go: Implement command to list merge conflicts
In order to get a list of all conflicting files when merging two commits, this commit implements a new command `gitaly-git2go conflicts`. As input, it gets the repository as well as the two commits which ought to be merged with each other. It'll then return the serialized list of conflicts via its standard output, containing for each conflict the three paths which are impacted as well as the conflicting hunks.
Diffstat (limited to 'internal/git2go')
-rw-r--r--internal/git2go/conflicts.go94
-rw-r--r--internal/git2go/conflicts_test.go128
2 files changed, 222 insertions, 0 deletions
diff --git a/internal/git2go/conflicts.go b/internal/git2go/conflicts.go
new file mode 100644
index 000000000..101c260ae
--- /dev/null
+++ b/internal/git2go/conflicts.go
@@ -0,0 +1,94 @@
+package git2go
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+
+ "gitlab.com/gitlab-org/gitaly/internal/gitaly/config"
+)
+
+// ConflictsCommand contains parameters to perform a merge and return its conflicts.
+type ConflictsCommand struct {
+ // Repository is the path to execute merge in.
+ Repository string `json:"repository"`
+ // Ours is the commit that is to be merged into theirs.
+ Ours string `json:"ours"`
+ // Theirs is the commit into which ours is to be merged.
+ Theirs string `json:"theirs"`
+}
+
+// Conflict represents a merge conflict for a single file.
+type Conflict struct {
+ // AncestorPath is the path of the ancestor.
+ AncestorPath string `json:"ancestor_path"`
+ // OurPath is the path of ours.
+ OurPath string `json:"our_path"`
+ // TheirPath is the path of theirs.
+ TheirPath string `json:"their_path"`
+ // Content contains the conflicting merge results.
+ Content string `json:"content"`
+}
+
+// ConflictsResult contains all conflicts resulting from a merge.
+type ConflictsResult struct {
+ // Conflicts
+ Conflicts []Conflict `json:"conflicts"`
+}
+
+// ConflictsCommandFromSerialized constructs a ConflictsCommand from its serialized representation.
+func ConflictsCommandFromSerialized(serialized string) (ConflictsCommand, error) {
+ var request ConflictsCommand
+ if err := deserialize(serialized, &request); err != nil {
+ return ConflictsCommand{}, err
+ }
+
+ if err := request.verify(); err != nil {
+ return ConflictsCommand{}, fmt.Errorf("conflicts: %w: %s", ErrInvalidArgument, err.Error())
+ }
+
+ return request, nil
+}
+
+// Serialize serializes the conflicts result.
+func (m ConflictsResult) SerializeTo(writer io.Writer) error {
+ return serializeTo(writer, m)
+}
+
+// Run performs a merge via gitaly-git2go and returns all resulting conflicts.
+func (c ConflictsCommand) Run(ctx context.Context, cfg config.Cfg) (ConflictsResult, error) {
+ if err := c.verify(); err != nil {
+ return ConflictsResult{}, fmt.Errorf("conflicts: %w: %s", ErrInvalidArgument, err.Error())
+ }
+
+ serialized, err := serialize(c)
+ if err != nil {
+ return ConflictsResult{}, err
+ }
+
+ stdout, err := run(ctx, cfg, "conflicts", serialized)
+ if err != nil {
+ return ConflictsResult{}, err
+ }
+
+ var response ConflictsResult
+ if err := deserialize(stdout, &response); err != nil {
+ return ConflictsResult{}, err
+ }
+
+ return response, nil
+}
+
+func (c ConflictsCommand) verify() error {
+ if c.Repository == "" {
+ return errors.New("missing repository")
+ }
+ if c.Ours == "" {
+ return errors.New("missing ours")
+ }
+ if c.Theirs == "" {
+ return errors.New("missing theirs")
+ }
+ return nil
+}
diff --git a/internal/git2go/conflicts_test.go b/internal/git2go/conflicts_test.go
new file mode 100644
index 000000000..6aeeb2903
--- /dev/null
+++ b/internal/git2go/conflicts_test.go
@@ -0,0 +1,128 @@
+package git2go
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestConflictsCommand_Serialization(t *testing.T) {
+ testcases := []struct {
+ desc string
+ cmd ConflictsCommand
+ err string
+ }{
+ {
+ desc: "missing repository",
+ cmd: ConflictsCommand{},
+ err: "missing repository",
+ },
+ {
+ desc: "missing theirs",
+ cmd: ConflictsCommand{
+ Repository: "foo",
+ Ours: "refs/heads/master",
+ },
+ err: "missing theirs",
+ },
+ {
+ desc: "missing ours",
+ cmd: ConflictsCommand{
+ Repository: "foo",
+ Theirs: "refs/heads/master",
+ },
+ err: "missing ours",
+ },
+ {
+ desc: "valid command",
+ cmd: ConflictsCommand{
+ Repository: "foo",
+ Ours: "refs/heads/master",
+ Theirs: "refs/heads/foo",
+ },
+ },
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.desc, func(t *testing.T) {
+ serialized, err := serialize(tc.cmd)
+ require.NoError(t, err)
+
+ deserialized, err := ConflictsCommandFromSerialized(serialized)
+
+ if tc.err != "" {
+ require.Error(t, err)
+ require.Contains(t, err.Error(), tc.err)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, deserialized, tc.cmd)
+ }
+ })
+ }
+}
+
+func TestConflictsResult_Serialization(t *testing.T) {
+ serializeResult := func(t *testing.T, result ConflictsResult) string {
+ var buf bytes.Buffer
+ err := result.SerializeTo(&buf)
+ require.NoError(t, err)
+ return buf.String()
+ }
+
+ testcases := []struct {
+ desc string
+ serialized string
+ expected ConflictsResult
+ err string
+ }{
+ {
+ desc: "empty merge result",
+ serialized: serializeResult(t, ConflictsResult{}),
+ expected: ConflictsResult{},
+ },
+ {
+ desc: "merge result with commit",
+ serialized: serializeResult(t, ConflictsResult{
+ Conflicts: []Conflict{
+ {
+ AncestorPath: "dir/ancestor",
+ OurPath: "dir/our",
+ TheirPath: "dir/their",
+ Content: "content",
+ },
+ },
+ }),
+ expected: ConflictsResult{
+ Conflicts: []Conflict{
+ {
+ AncestorPath: "dir/ancestor",
+ OurPath: "dir/our",
+ TheirPath: "dir/their",
+ Content: "content",
+ },
+ },
+ },
+ },
+ {
+ desc: "invalid serialized representation",
+ serialized: "xvlc",
+ err: "invalid character",
+ },
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.desc, func(t *testing.T) {
+ var deserialized ConflictsResult
+ err := deserialize(tc.serialized, &deserialized)
+
+ if tc.err != "" {
+ require.Error(t, err)
+ require.Contains(t, err.Error(), tc.err)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, deserialized, tc.expected)
+ }
+ })
+ }
+}