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:
authorZeger-Jan van de Weg <git@zjvandeweg.nl>2020-01-08 18:55:29 +0300
committerZeger-Jan van de Weg <git@zjvandeweg.nl>2020-01-08 18:55:29 +0300
commit4fb323c51c38d1ef26a4e40bd49f3442794232c8 (patch)
tree856780b0dfc9f0d39d73dc668723a28e034a4a00
parentb1c0371664f6c65f7645180bfa5fbef55d58a346 (diff)
parent617a6eec741d955562c27511d232639c46924678 (diff)
Merge branch 'jc-add-hook-rpcs' into 'master'
Add hook rpcs See merge request gitlab-org/gitaly!1686
-rw-r--r--changelogs/unreleased/jc-add-hook-rpcs.yml5
-rw-r--r--internal/service/hooks/post_receive.go70
-rw-r--r--internal/service/hooks/post_receive_test.go134
-rw-r--r--internal/service/hooks/pre_receive.go91
-rw-r--r--internal/service/hooks/pre_receive_test.go145
-rw-r--r--internal/service/hooks/server.go10
-rw-r--r--internal/service/hooks/stream_command.go35
-rwxr-xr-xinternal/service/hooks/testdata/gitlab-shell/hooks/post-receive8
-rwxr-xr-xinternal/service/hooks/testdata/gitlab-shell/hooks/pre-receive9
-rwxr-xr-xinternal/service/hooks/testdata/gitlab-shell/hooks/update9
-rw-r--r--internal/service/hooks/testhelper_test.go47
-rw-r--r--internal/service/hooks/update.go61
-rw-r--r--internal/service/hooks/update_test.go162
-rw-r--r--internal/service/register.go2
14 files changed, 788 insertions, 0 deletions
diff --git a/changelogs/unreleased/jc-add-hook-rpcs.yml b/changelogs/unreleased/jc-add-hook-rpcs.yml
new file mode 100644
index 000000000..78a550468
--- /dev/null
+++ b/changelogs/unreleased/jc-add-hook-rpcs.yml
@@ -0,0 +1,5 @@
+---
+title: Add hook rpcs
+merge_request: 1686
+author:
+type: performance
diff --git a/internal/service/hooks/post_receive.go b/internal/service/hooks/post_receive.go
new file mode 100644
index 000000000..bfe6e628a
--- /dev/null
+++ b/internal/service/hooks/post_receive.go
@@ -0,0 +1,70 @@
+package hook
+
+import (
+ "errors"
+ "os/exec"
+
+ "gitlab.com/gitlab-org/gitaly/streamio"
+
+ "gitlab.com/gitlab-org/gitaly/internal/helper"
+ "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+)
+
+func (s *server) PostReceiveHook(stream gitalypb.HookService_PostReceiveHookServer) error {
+ firstRequest, err := stream.Recv()
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ if err := validatePostReceiveHookRequest(firstRequest); err != nil {
+ return helper.ErrInvalidArgument(err)
+ }
+
+ hookEnv, err := hookRequestEnv(firstRequest)
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ stdin := streamio.NewReader(func() ([]byte, error) {
+ req, err := stream.Recv()
+ return req.GetStdin(), err
+ })
+ stdout := streamio.NewWriter(func(p []byte) error { return stream.Send(&gitalypb.PostReceiveHookResponse{Stdout: p}) })
+ stderr := streamio.NewWriter(func(p []byte) error { return stream.Send(&gitalypb.PostReceiveHookResponse{Stderr: p}) })
+
+ repoPath, err := helper.GetRepoPath(firstRequest.GetRepository())
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ c := exec.Command(gitlabShellHook("post-receive"))
+ c.Dir = repoPath
+
+ success, err := streamCommandResponse(
+ stream.Context(),
+ stdin,
+ stdout, stderr,
+ c,
+ hookEnv,
+ )
+
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ if err := stream.SendMsg(&gitalypb.PostReceiveHookResponse{
+ Success: success,
+ }); err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ return nil
+}
+
+func validatePostReceiveHookRequest(in *gitalypb.PostReceiveHookRequest) error {
+ if in.GetRepository() == nil {
+ return errors.New("repository is empty")
+ }
+
+ return nil
+}
diff --git a/internal/service/hooks/post_receive_test.go b/internal/service/hooks/post_receive_test.go
new file mode 100644
index 000000000..bf9adf0fa
--- /dev/null
+++ b/internal/service/hooks/post_receive_test.go
@@ -0,0 +1,134 @@
+package hook
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "gitlab.com/gitlab-org/gitaly/internal/helper/text"
+
+ "github.com/stretchr/testify/assert"
+ "gitlab.com/gitlab-org/gitaly/streamio"
+
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitaly/internal/config"
+ "gitlab.com/gitlab-org/gitaly/internal/testhelper"
+ "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+ "google.golang.org/grpc/codes"
+)
+
+func TestPostReceiveInvalidArgument(t *testing.T) {
+ server, serverSocketPath := runHooksServer(t)
+ defer server.Stop()
+
+ client, conn := newHooksClient(t, serverSocketPath)
+ defer conn.Close()
+
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ stream, err := client.PostReceiveHook(ctx)
+ require.NoError(t, err)
+ require.NoError(t, stream.Send(&gitalypb.PostReceiveHookRequest{}), "empty repository should result in an error")
+ _, err = stream.Recv()
+
+ testhelper.RequireGrpcError(t, err, codes.InvalidArgument)
+}
+
+func TestPostReceive(t *testing.T) {
+ rubyDir := config.Config.Ruby.Dir
+ defer func(rubyDir string) {
+ config.Config.Ruby.Dir = rubyDir
+ }(rubyDir)
+
+ cwd, err := os.Getwd()
+ require.NoError(t, err)
+ config.Config.Ruby.Dir = filepath.Join(cwd, "testdata")
+
+ server, serverSocketPath := runHooksServer(t)
+ defer server.Stop()
+
+ testRepo, _, cleanupFn := testhelper.NewTestRepo(t)
+ defer cleanupFn()
+
+ client, conn := newHooksClient(t, serverSocketPath)
+ defer conn.Close()
+
+ testCases := []struct {
+ desc string
+ stdin io.Reader
+ req gitalypb.PostReceiveHookRequest
+ success bool
+ stdout string
+ stderr string
+ }{
+ {
+ desc: "valid stdin",
+ stdin: bytes.NewBufferString("a\nb\nc\nd\ne\nf\ng"),
+ req: gitalypb.PostReceiveHookRequest{Repository: testRepo, KeyId: "key_id"},
+ success: true,
+ stdout: "OK",
+ stderr: "",
+ },
+ {
+ desc: "missing stdin",
+ stdin: bytes.NewBuffer(nil),
+ req: gitalypb.PostReceiveHookRequest{Repository: testRepo, KeyId: "key_id"},
+ success: false,
+ stdout: "",
+ stderr: "FAIL",
+ },
+ {
+ desc: "missing key_id",
+ stdin: bytes.NewBuffer(nil),
+ req: gitalypb.PostReceiveHookRequest{Repository: testRepo},
+ success: false,
+ stdout: "",
+ stderr: "FAIL",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ stream, err := client.PostReceiveHook(ctx)
+ require.NoError(t, err)
+ require.NoError(t, stream.Send(&tc.req))
+
+ go func() {
+ writer := streamio.NewWriter(func(p []byte) error {
+ return stream.Send(&gitalypb.PostReceiveHookRequest{Stdin: p})
+ })
+ _, err := io.Copy(writer, tc.stdin)
+ require.NoError(t, err)
+ require.NoError(t, stream.CloseSend(), "close send")
+ }()
+
+ var success bool
+ var stdout, stderr bytes.Buffer
+ for {
+ resp, err := stream.Recv()
+ if err == io.EOF {
+ break
+ }
+
+ _, err = stdout.Write(resp.GetStdout())
+ require.NoError(t, err)
+ stderr.Write(resp.GetStderr())
+ require.NoError(t, err)
+
+ success = resp.GetSuccess()
+ require.NoError(t, err)
+ }
+
+ require.Equal(t, tc.success, success)
+ assert.Equal(t, tc.stderr, text.ChompBytes(stderr.Bytes()), "hook stderr")
+ assert.Equal(t, tc.stdout, text.ChompBytes(stdout.Bytes()), "hook stdout")
+ })
+ }
+}
diff --git a/internal/service/hooks/pre_receive.go b/internal/service/hooks/pre_receive.go
new file mode 100644
index 000000000..63ff2db90
--- /dev/null
+++ b/internal/service/hooks/pre_receive.go
@@ -0,0 +1,91 @@
+package hook
+
+import (
+ "errors"
+ "fmt"
+ "os/exec"
+ "path/filepath"
+
+ "gitlab.com/gitlab-org/gitaly/streamio"
+
+ "gitlab.com/gitlab-org/gitaly/internal/config"
+ "gitlab.com/gitlab-org/gitaly/internal/gitlabshell"
+ "gitlab.com/gitlab-org/gitaly/internal/helper"
+ "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+)
+
+type hookRequest interface {
+ GetKeyId() string
+ GetRepository() *gitalypb.Repository
+}
+
+func hookRequestEnv(req hookRequest) ([]string, error) {
+ return append(gitlabshell.Env(),
+ fmt.Sprintf("GL_ID=%s", req.GetKeyId()),
+ fmt.Sprintf("GL_REPOSITORY=%s", req.GetRepository().GetGlRepository())), nil
+}
+
+func gitlabShellHook(hookName string) string {
+ return filepath.Join(config.Config.Ruby.Dir, "gitlab-shell", "hooks", hookName)
+}
+
+func (s *server) PreReceiveHook(stream gitalypb.HookService_PreReceiveHookServer) error {
+ firstRequest, err := stream.Recv()
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ if err := validatePreReceiveHookRequest(firstRequest); err != nil {
+ return helper.ErrInvalidArgument(err)
+ }
+
+ hookEnv, err := hookRequestEnv(firstRequest)
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ hookEnv = append(hookEnv, fmt.Sprintf("GL_PROTOCOL=%s", firstRequest.GetProtocol()))
+
+ stdin := streamio.NewReader(func() ([]byte, error) {
+ req, err := stream.Recv()
+ return req.GetStdin(), err
+ })
+ stdout := streamio.NewWriter(func(p []byte) error { return stream.Send(&gitalypb.PreReceiveHookResponse{Stdout: p}) })
+ stderr := streamio.NewWriter(func(p []byte) error { return stream.Send(&gitalypb.PreReceiveHookResponse{Stderr: p}) })
+
+ repoPath, err := helper.GetRepoPath(firstRequest.GetRepository())
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ c := exec.Command(gitlabShellHook("pre-receive"))
+ c.Dir = repoPath
+
+ success, err := streamCommandResponse(
+ stream.Context(),
+ stdin,
+ stdout, stderr,
+ c,
+ hookEnv,
+ )
+
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ if err := stream.SendMsg(&gitalypb.PreReceiveHookResponse{
+ Success: success,
+ }); err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ return nil
+}
+
+func validatePreReceiveHookRequest(in *gitalypb.PreReceiveHookRequest) error {
+ if in.GetRepository() == nil {
+ return errors.New("repository is empty")
+ }
+
+ return nil
+}
diff --git a/internal/service/hooks/pre_receive_test.go b/internal/service/hooks/pre_receive_test.go
new file mode 100644
index 000000000..52ce8ec7b
--- /dev/null
+++ b/internal/service/hooks/pre_receive_test.go
@@ -0,0 +1,145 @@
+package hook
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "gitlab.com/gitlab-org/gitaly/internal/helper/text"
+
+ "github.com/stretchr/testify/assert"
+
+ "gitlab.com/gitlab-org/gitaly/streamio"
+
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitaly/internal/config"
+ "gitlab.com/gitlab-org/gitaly/internal/testhelper"
+ "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+ "google.golang.org/grpc/codes"
+)
+
+func TestPreReceiveInvalidArgument(t *testing.T) {
+ server, serverSocketPath := runHooksServer(t)
+ defer server.Stop()
+
+ client, conn := newHooksClient(t, serverSocketPath)
+ defer conn.Close()
+
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ stream, err := client.PreReceiveHook(ctx)
+ require.NoError(t, err)
+ require.NoError(t, stream.Send(&gitalypb.PreReceiveHookRequest{}))
+ _, err = stream.Recv()
+
+ testhelper.RequireGrpcError(t, err, codes.InvalidArgument)
+}
+
+func TestPreReceive(t *testing.T) {
+ rubyDir := config.Config.Ruby.Dir
+ defer func(rubyDir string) {
+ config.Config.Ruby.Dir = rubyDir
+ }(rubyDir)
+
+ cwd, err := os.Getwd()
+ require.NoError(t, err)
+ config.Config.Ruby.Dir = filepath.Join(cwd, "testdata")
+
+ server, serverSocketPath := runHooksServer(t)
+ defer server.Stop()
+
+ testRepo, _, cleanupFn := testhelper.NewTestRepo(t)
+ defer cleanupFn()
+
+ client, conn := newHooksClient(t, serverSocketPath)
+ defer conn.Close()
+
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ testCases := []struct {
+ desc string
+ stdin io.Reader
+ req gitalypb.PreReceiveHookRequest
+ success bool
+ stdout, stderr string
+ }{
+ {
+ desc: "valid stdin",
+ stdin: bytes.NewBufferString("a\nb\nc\nd\ne\nf\ng"),
+ req: gitalypb.PreReceiveHookRequest{Repository: testRepo, KeyId: "key_id", Protocol: "protocol"},
+ success: true,
+ stdout: "OK",
+ stderr: "",
+ },
+ {
+ desc: "missing stdin",
+ stdin: bytes.NewBuffer(nil),
+ req: gitalypb.PreReceiveHookRequest{Repository: testRepo, KeyId: "key_id", Protocol: "protocol"},
+ success: false,
+ stdout: "",
+ stderr: "FAIL",
+ },
+ {
+ desc: "missing protocol",
+ stdin: bytes.NewBufferString("a\nb\nc\nd\ne\nf\ng"),
+ req: gitalypb.PreReceiveHookRequest{Repository: testRepo, KeyId: "key_id"},
+ success: false,
+ stdout: "",
+ stderr: "FAIL",
+ },
+ {
+ desc: "missing key_id",
+ stdin: bytes.NewBufferString("a\nb\nc\nd\ne\nf\ng"),
+ req: gitalypb.PreReceiveHookRequest{Repository: testRepo, Protocol: "protocol"},
+ success: false,
+ stdout: "",
+ stderr: "FAIL",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ stream, err := client.PreReceiveHook(ctx)
+ require.NoError(t, err)
+ require.NoError(t, stream.Send(&tc.req))
+
+ go func() {
+ writer := streamio.NewWriter(func(p []byte) error {
+ return stream.Send(&gitalypb.PreReceiveHookRequest{Stdin: p})
+ })
+ _, err := io.Copy(writer, tc.stdin)
+ require.NoError(t, err)
+ require.NoError(t, stream.CloseSend(), "close send")
+ }()
+
+ var success bool
+ var stdout, stderr bytes.Buffer
+ for {
+ resp, err := stream.Recv()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Errorf("error when receiving stream: %v", err)
+ }
+
+ _, err = stdout.Write(resp.GetStdout())
+ require.NoError(t, err)
+ _, err = stderr.Write(resp.GetStderr())
+ require.NoError(t, err)
+
+ success = resp.GetSuccess()
+ require.NoError(t, err)
+ }
+
+ require.Equal(t, tc.success, success)
+ assert.Equal(t, tc.stderr, text.ChompBytes(stderr.Bytes()), "hook stderr")
+ assert.Equal(t, tc.stdout, text.ChompBytes(stdout.Bytes()), "hook stdout")
+ })
+ }
+}
diff --git a/internal/service/hooks/server.go b/internal/service/hooks/server.go
new file mode 100644
index 000000000..1977af1df
--- /dev/null
+++ b/internal/service/hooks/server.go
@@ -0,0 +1,10 @@
+package hook
+
+import "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+
+type server struct{}
+
+// NewServer creates a new instance of a gRPC namespace server
+func NewServer() gitalypb.HookServiceServer {
+ return &server{}
+}
diff --git a/internal/service/hooks/stream_command.go b/internal/service/hooks/stream_command.go
new file mode 100644
index 000000000..30d8003bf
--- /dev/null
+++ b/internal/service/hooks/stream_command.go
@@ -0,0 +1,35 @@
+package hook
+
+import (
+ "context"
+ "io"
+ "os/exec"
+
+ "gitlab.com/gitlab-org/gitaly/internal/command"
+ "gitlab.com/gitlab-org/gitaly/internal/helper"
+)
+
+func streamCommandResponse(
+ ctx context.Context,
+ stdin io.Reader,
+ stdout, stderr io.Writer,
+ c *exec.Cmd,
+ env []string,
+) (bool, error) {
+ cmd, err := command.New(ctx, c, stdin, stdout, stderr, env...)
+ if err != nil {
+ return false, helper.ErrInternal(err)
+ }
+
+ err = cmd.Wait()
+ if err == nil {
+ return true, nil
+ }
+
+ code, ok := command.ExitStatus(err)
+ if ok && code != 0 {
+ return false, nil
+ }
+
+ return false, err
+}
diff --git a/internal/service/hooks/testdata/gitlab-shell/hooks/post-receive b/internal/service/hooks/testdata/gitlab-shell/hooks/post-receive
new file mode 100755
index 000000000..6f0207819
--- /dev/null
+++ b/internal/service/hooks/testdata/gitlab-shell/hooks/post-receive
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+
+# Tests inputs to post-receive
+
+abort("FAIL") if $stdin.read.empty?
+abort("FAIL") if %w[GL_ID GL_REPOSITORY].any? { |k| ENV[k].empty? }
+
+puts "OK"
diff --git a/internal/service/hooks/testdata/gitlab-shell/hooks/pre-receive b/internal/service/hooks/testdata/gitlab-shell/hooks/pre-receive
new file mode 100755
index 000000000..fc967a761
--- /dev/null
+++ b/internal/service/hooks/testdata/gitlab-shell/hooks/pre-receive
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+
+# Tests inputs to pre-receive
+
+abort("FAIL") if $stdin.read.empty?
+abort("FAIL") if %w[GL_ID GL_REPOSITORY GL_PROTOCOL].any? { |k| ENV[k].empty? }
+
+puts "OK"
+
diff --git a/internal/service/hooks/testdata/gitlab-shell/hooks/update b/internal/service/hooks/testdata/gitlab-shell/hooks/update
new file mode 100755
index 000000000..f7a121069
--- /dev/null
+++ b/internal/service/hooks/testdata/gitlab-shell/hooks/update
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+
+# Tests inputs to update
+
+abort("FAIL") unless ARGV.size == 3
+abort("FAIL") if ARGV.any? { |arg| arg.empty? }
+abort("FAIL") if ENV['GL_ID'].empty?
+
+puts "OK"
diff --git a/internal/service/hooks/testhelper_test.go b/internal/service/hooks/testhelper_test.go
new file mode 100644
index 000000000..72367ac75
--- /dev/null
+++ b/internal/service/hooks/testhelper_test.go
@@ -0,0 +1,47 @@
+package hook
+
+import (
+ "net"
+ "testing"
+
+ gitalyauth "gitlab.com/gitlab-org/gitaly/auth"
+ "gitlab.com/gitlab-org/gitaly/internal/config"
+ "gitlab.com/gitlab-org/gitaly/internal/server/auth"
+ "gitlab.com/gitlab-org/gitaly/internal/testhelper"
+ "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/reflection"
+)
+
+func newHooksClient(t *testing.T, serverSocketPath string) (gitalypb.HookServiceClient, *grpc.ClientConn) {
+ connOpts := []grpc.DialOption{
+ grpc.WithInsecure(),
+ grpc.WithPerRPCCredentials(gitalyauth.RPCCredentials(config.Config.Auth.Token)),
+ }
+ conn, err := grpc.Dial(serverSocketPath, connOpts...)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return gitalypb.NewHookServiceClient(conn), conn
+}
+
+func runHooksServer(t *testing.T) (*grpc.Server, string) {
+ streamInt := []grpc.StreamServerInterceptor{auth.StreamServerInterceptor(config.Config.Auth)}
+ unaryInt := []grpc.UnaryServerInterceptor{auth.UnaryServerInterceptor(config.Config.Auth)}
+
+ server := testhelper.NewTestGrpcServer(t, streamInt, unaryInt)
+ serverSocketPath := testhelper.GetTemporaryGitalySocketFileName()
+
+ listener, err := net.Listen("unix", serverSocketPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ gitalypb.RegisterHookServiceServer(server, NewServer())
+ reflection.Register(server)
+
+ go server.Serve(listener)
+
+ return server, "unix://" + serverSocketPath
+}
diff --git a/internal/service/hooks/update.go b/internal/service/hooks/update.go
new file mode 100644
index 000000000..6cfad6093
--- /dev/null
+++ b/internal/service/hooks/update.go
@@ -0,0 +1,61 @@
+package hook
+
+import (
+ "errors"
+ "os/exec"
+
+ "gitlab.com/gitlab-org/gitaly/streamio"
+
+ "gitlab.com/gitlab-org/gitaly/internal/helper"
+ "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+)
+
+func (s *server) UpdateHook(in *gitalypb.UpdateHookRequest, stream gitalypb.HookService_UpdateHookServer) error {
+ if err := validateUpdateHookRequest(in); err != nil {
+ return helper.ErrInvalidArgument(err)
+ }
+
+ hookEnv, err := hookRequestEnv(in)
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ stdout := streamio.NewWriter(func(p []byte) error { return stream.Send(&gitalypb.UpdateHookResponse{Stdout: p}) })
+ stderr := streamio.NewWriter(func(p []byte) error { return stream.Send(&gitalypb.UpdateHookResponse{Stderr: p}) })
+
+ repoPath, err := helper.GetRepoPath(in.GetRepository())
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ c := exec.Command(gitlabShellHook("update"), string(in.GetRef()), in.GetOldValue(), in.GetNewValue())
+ c.Dir = repoPath
+
+ success, err := streamCommandResponse(
+ stream.Context(),
+ nil,
+ stdout, stderr,
+ c,
+ hookEnv,
+ )
+
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ if err := stream.SendMsg(&gitalypb.PreReceiveHookResponse{
+ Success: success,
+ }); err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ return nil
+}
+
+func validateUpdateHookRequest(in *gitalypb.UpdateHookRequest) error {
+ if in.GetRepository() == nil {
+ return errors.New("repository is empty")
+ }
+
+ return nil
+}
diff --git a/internal/service/hooks/update_test.go b/internal/service/hooks/update_test.go
new file mode 100644
index 000000000..07b64a28c
--- /dev/null
+++ b/internal/service/hooks/update_test.go
@@ -0,0 +1,162 @@
+package hook
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "gitlab.com/gitlab-org/gitaly/internal/helper/text"
+
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitaly/internal/config"
+ "gitlab.com/gitlab-org/gitaly/internal/testhelper"
+ "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+ "google.golang.org/grpc/codes"
+)
+
+func TestUpdateInvalidArgument(t *testing.T) {
+ server, serverSocketPath := runHooksServer(t)
+ defer server.Stop()
+
+ client, conn := newHooksClient(t, serverSocketPath)
+ defer conn.Close()
+
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ stream, err := client.UpdateHook(ctx, &gitalypb.UpdateHookRequest{})
+ require.NoError(t, err)
+ _, err = stream.Recv()
+
+ testhelper.RequireGrpcError(t, err, codes.InvalidArgument)
+}
+
+func TestUpdate(t *testing.T) {
+ rubyDir := config.Config.Ruby.Dir
+ defer func() {
+ config.Config.Ruby.Dir = rubyDir
+ }()
+
+ cwd, err := os.Getwd()
+ require.NoError(t, err)
+ config.Config.Ruby.Dir = filepath.Join(cwd, "testdata")
+
+ server, serverSocketPath := runHooksServer(t)
+ defer server.Stop()
+
+ testRepo, _, cleanupFn := testhelper.NewTestRepo(t)
+ defer cleanupFn()
+
+ client, conn := newHooksClient(t, serverSocketPath)
+ defer conn.Close()
+
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ testCases := []struct {
+ desc string
+ req gitalypb.UpdateHookRequest
+ success bool
+ stdout, stderr string
+ }{
+ {
+ desc: "valid inputs",
+ req: gitalypb.UpdateHookRequest{
+ Repository: testRepo,
+ Ref: []byte("master"),
+ OldValue: "a",
+ NewValue: "b",
+ KeyId: "key",
+ },
+ success: true,
+ stdout: "OK",
+ stderr: "",
+ },
+ {
+ desc: "missing ref",
+ req: gitalypb.UpdateHookRequest{
+ Repository: testRepo,
+ Ref: nil,
+ OldValue: "a",
+ NewValue: "b",
+ KeyId: "key",
+ },
+ success: false,
+ stdout: "",
+ stderr: "FAIL",
+ },
+ {
+ desc: "missing old value",
+ req: gitalypb.UpdateHookRequest{
+ Repository: testRepo,
+ Ref: []byte("master"),
+ OldValue: "",
+ NewValue: "b",
+ KeyId: "key",
+ },
+ success: false,
+ stdout: "",
+ stderr: "FAIL",
+ },
+ {
+ desc: "missing new value",
+ req: gitalypb.UpdateHookRequest{
+ Repository: testRepo,
+ Ref: []byte("master"),
+ OldValue: "a",
+ NewValue: "",
+ KeyId: "key",
+ },
+ success: false,
+ stdout: "",
+ stderr: "FAIL",
+ },
+ {
+ desc: "missing key_id value",
+ req: gitalypb.UpdateHookRequest{
+ Repository: testRepo,
+ Ref: []byte("master"),
+ OldValue: "a",
+ NewValue: "b",
+ KeyId: "",
+ },
+ success: false,
+ stdout: "",
+ stderr: "FAIL",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ stream, err := client.UpdateHook(ctx, &tc.req)
+ require.NoError(t, err)
+
+ var success bool
+ var stderr, stdout bytes.Buffer
+ for {
+ resp, err := stream.Recv()
+ if err == io.EOF {
+ break
+ }
+
+ stderr.Write(resp.GetStderr())
+ stdout.Write(resp.GetStdout())
+
+ if err != nil {
+ t.Errorf("error when receiving stream: %v", err)
+ }
+
+ success = resp.GetSuccess()
+ require.NoError(t, err)
+ }
+
+ require.Equal(t, tc.success, success)
+ assert.Equal(t, tc.stderr, text.ChompBytes(stderr.Bytes()), "hook stderr")
+ assert.Equal(t, tc.stdout, text.ChompBytes(stdout.Bytes()), "hook stdout")
+ })
+ }
+}
diff --git a/internal/service/register.go b/internal/service/register.go
index f12215e56..317e0a2a3 100644
--- a/internal/service/register.go
+++ b/internal/service/register.go
@@ -7,6 +7,7 @@ import (
"gitlab.com/gitlab-org/gitaly/internal/service/commit"
"gitlab.com/gitlab-org/gitaly/internal/service/conflicts"
"gitlab.com/gitlab-org/gitaly/internal/service/diff"
+ hook "gitlab.com/gitlab-org/gitaly/internal/service/hooks"
"gitlab.com/gitlab-org/gitaly/internal/service/namespace"
"gitlab.com/gitlab-org/gitaly/internal/service/objectpool"
"gitlab.com/gitlab-org/gitaly/internal/service/operations"
@@ -41,6 +42,7 @@ func RegisterAll(grpcServer *grpc.Server, rubyServer *rubyserver.Server) {
gitalypb.RegisterRemoteServiceServer(grpcServer, remote.NewServer(rubyServer))
gitalypb.RegisterServerServiceServer(grpcServer, server.NewServer())
gitalypb.RegisterObjectPoolServiceServer(grpcServer, objectpool.NewServer())
+ gitalypb.RegisterHookServiceServer(grpcServer, hook.NewServer())
healthpb.RegisterHealthServer(grpcServer, health.NewServer())
}