diff --git a/_support/makegen.go b/_support/makegen.go index 3227f1240b214a6f1d5d5e1bcdd9988809cb143d..9a16b2119b26b6a687db0d71165f44abb00618ab 100644 --- a/_support/makegen.go +++ b/_support/makegen.go @@ -135,7 +135,7 @@ func (gm *gitalyMake) CommandPackages() []string { for _, dir := range entries { //Do not build gitaly-remote by default - if dir.Name() == "gitaly-remote" { + if dir.Name() == "gitaly-remote" || dir.Name() == "hooks" { continue } if !dir.IsDir() { diff --git a/cmd/hooks/hooks.go b/cmd/hooks/hooks.go new file mode 100644 index 0000000000000000000000000000000000000000..5f00d720e773b998d639b39c0099d891a25f19ee --- /dev/null +++ b/cmd/hooks/hooks.go @@ -0,0 +1,22 @@ +package hooks + +import ( + "bufio" + "io" +) + +// ReadRefs reads a list of newline delimeted refs from a reader +func ReadRefs(r io.Reader) ([]string, error) { + s := bufio.NewScanner(r) + + var refs []string + for s.Scan() { + refs = append(refs, s.Text()) + } + + if err := s.Err(); err != nil { + return nil, err + } + + return refs, nil +} diff --git a/cmd/hooks/postreceive/postceive.go b/cmd/hooks/postreceive/postceive.go new file mode 100644 index 0000000000000000000000000000000000000000..41c7af5bfe1e141198298679ad69d4d85c6d539f --- /dev/null +++ b/cmd/hooks/postreceive/postceive.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "log" + "os" + + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + gitalyauth "gitlab.com/gitlab-org/gitaly/auth" + "gitlab.com/gitlab-org/gitaly/client" + "gitlab.com/gitlab-org/gitaly/cmd/hooks" + grpccorrelation "gitlab.com/gitlab-org/labkit/correlation/grpc" + grpctracing "gitlab.com/gitlab-org/labkit/tracing/grpc" + "google.golang.org/grpc" +) + +func main() { + refs, err := hooks.ReadRefs(os.Stdin) + if err != nil { + log.Fatalf("error when reading refs: %v", err) + } + keyID := os.Getenv("GL_ID") + repoPath, err := os.Getwd() + if err != nil { + log.Fatalf("error when getting pwd: %v", err) + } + glRepository := os.Getenv("GL_REPOSITORY") + url := os.Getenv("GL_URL") + + conn, err := client.Dial(url, dialOpts()) + if err != nil { + log.Fatalf("error when dialing: %v", err) + } + + c := gitalypb.NewHookServiceClient(conn) + + if _, err = c.PostReceive(context.Background(), &gitalypb.PostReceiveHookRequest{ + RepoPath: repoPath, + KeyId: keyID, + GlRepository: glRepository, + Refs: refs, + }); err != nil { + log.Fatalf("error when calling pre receive hook: %v", err) + } +} + +func dialOpts() []grpc.DialOption { + connOpts := client.DefaultDialOpts + if token := os.Getenv("GITALY_TOKEN"); token != "" { + connOpts = append(connOpts, grpc.WithPerRPCCredentials(gitalyauth.RPCCredentials(token))) + } + + // Add grpc client interceptors + connOpts = append(connOpts, grpc.WithStreamInterceptor( + grpc_middleware.ChainStreamClient( + grpctracing.StreamClientTracingInterceptor(), // Tracing + grpccorrelation.StreamClientCorrelationInterceptor(), // Correlation + )), + + grpc.WithUnaryInterceptor( + grpc_middleware.ChainUnaryClient( + grpctracing.UnaryClientTracingInterceptor(), // Tracing + grpccorrelation.UnaryClientCorrelationInterceptor(), // Correlation + ))) + + return connOpts +} diff --git a/cmd/hooks/prereceive/preceive.go b/cmd/hooks/prereceive/preceive.go new file mode 100644 index 0000000000000000000000000000000000000000..36dfe7c08968365051fc5351735e6658ab8e03e3 --- /dev/null +++ b/cmd/hooks/prereceive/preceive.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "log" + "os" + + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + gitalyauth "gitlab.com/gitlab-org/gitaly/auth" + "gitlab.com/gitlab-org/gitaly/client" + "gitlab.com/gitlab-org/gitaly/cmd/hooks" + grpccorrelation "gitlab.com/gitlab-org/labkit/correlation/grpc" + grpctracing "gitlab.com/gitlab-org/labkit/tracing/grpc" + "google.golang.org/grpc" +) + +func main() { + refs, err := hooks.ReadRefs(os.Stdin) + if err != nil { + log.Fatalf("error when reading refs: %v", err) + } + keyID := os.Getenv("GL_ID") + protocol := os.Getenv("GL_PROTOCOL") + repoPath, err := os.Getwd() + if err != nil { + log.Fatalf("error when getting pwd: %v", err) + } + glRepository := os.Getenv("GL_REPOSITORY") + url := os.Getenv("GL_URL") + + conn, err := client.Dial(url, dialOpts()) + if err != nil { + log.Fatalf("error when dialing: %v", err) + } + + c := gitalypb.NewHookServiceClient(conn) + + if _, err = c.PreReceive(context.Background(), &gitalypb.PreReceiveHookRequest{ + GlRepository: glRepository, + RepoPath: repoPath, + KeyId: keyID, + Protocol: protocol, + Refs: refs, + }); err != nil { + log.Fatalf("error when calling pre receive hook: %v", err) + } +} + +func dialOpts() []grpc.DialOption { + connOpts := client.DefaultDialOpts + if token := os.Getenv("GITALY_TOKEN"); token != "" { + connOpts = append(connOpts, grpc.WithPerRPCCredentials(gitalyauth.RPCCredentials(token))) + } + + // Add grpc client interceptors + connOpts = append(connOpts, grpc.WithStreamInterceptor( + grpc_middleware.ChainStreamClient( + grpctracing.StreamClientTracingInterceptor(), // Tracing + grpccorrelation.StreamClientCorrelationInterceptor(), // Correlation + )), + + grpc.WithUnaryInterceptor( + grpc_middleware.ChainUnaryClient( + grpctracing.UnaryClientTracingInterceptor(), // Tracing + grpccorrelation.UnaryClientCorrelationInterceptor(), // Correlation + ))) + + return connOpts +} diff --git a/cmd/hooks/update/update.go b/cmd/hooks/update/update.go new file mode 100644 index 0000000000000000000000000000000000000000..49a2b8e40349f8216fb346961709547a1b5fa3b8 --- /dev/null +++ b/cmd/hooks/update/update.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "log" + "os" + + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + gitalyauth "gitlab.com/gitlab-org/gitaly/auth" + "gitlab.com/gitlab-org/gitaly/client" + grpccorrelation "gitlab.com/gitlab-org/labkit/correlation/grpc" + grpctracing "gitlab.com/gitlab-org/labkit/tracing/grpc" + "google.golang.org/grpc" +) + +func main() { + keyID := os.Getenv("GL_ID") + repoPath, err := os.Getwd() + if err != nil { + log.Fatalf("error when getting pwd: %v", err) + } + + if len(os.Args) < 4 { + log.Fatalf("only found %d arguments, 3 required", len(os.Args)-1) + } + + refName, oldVal, newVal := os.Args[1], os.Args[2], os.Args[3] + + url := os.Getenv("GL_URL") + + conn, err := client.Dial(url, dialOpts()) + if err != nil { + log.Fatalf("error when dialing: %v", err) + } + + c := gitalypb.NewHookServiceClient(conn) + + if _, err = c.Update(context.Background(), &gitalypb.UpdateHookRequest{ + RepoPath: repoPath, + KeyId: keyID, + Ref: refName, + OldValue: oldVal, + NewValue: newVal, + }); err != nil { + log.Fatalf("error when calling pre receive hook: %v", err) + } +} + +func dialOpts() []grpc.DialOption { + connOpts := client.DefaultDialOpts + if token := os.Getenv("GITALY_TOKEN"); token != "" { + connOpts = append(connOpts, grpc.WithPerRPCCredentials(gitalyauth.RPCCredentials(token))) + } + + // Add grpc client interceptors + connOpts = append(connOpts, grpc.WithStreamInterceptor( + grpc_middleware.ChainStreamClient( + grpctracing.StreamClientTracingInterceptor(), // Tracing + grpccorrelation.StreamClientCorrelationInterceptor(), // Correlation + )), + + grpc.WithUnaryInterceptor( + grpc_middleware.ChainUnaryClient( + grpctracing.UnaryClientTracingInterceptor(), // Tracing + grpccorrelation.UnaryClientCorrelationInterceptor(), // Correlation + ))) + + return connOpts +} diff --git a/go.mod b/go.mod index 0daf1038a7b72ae706040615446b8249a6e40cf3..e89c3f7664717c7e41e8ce77a9061292820ba02e 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/sirupsen/logrus v1.2.0 github.com/stretchr/testify v1.2.2 github.com/tinylib/msgp v1.1.0 // indirect - gitlab.com/gitlab-org/gitaly-proto v1.32.0 + gitlab.com/gitlab-org/gitaly-proto v1.33.1-0.20190620234657-0f7ad34f3faa gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c golang.org/x/net v0.0.0-20190311183353-d8887717615a golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 diff --git a/go.sum b/go.sum index cc56b0adbd6c78c022552b098e5006ba754756e3..17495172a1a02efc451078222f4808cd8fbabfb3 100644 --- a/go.sum +++ b/go.sum @@ -102,8 +102,8 @@ github.com/uber/jaeger-client-go v2.15.0+incompatible h1:NP3qsSqNxh8VYr956ur1N/1 github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo= github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -gitlab.com/gitlab-org/gitaly-proto v1.32.0 h1:TRe/iw/Gid1RNM2VzK+WICIw4/N7V5s0IdhmgiPyqNE= -gitlab.com/gitlab-org/gitaly-proto v1.32.0/go.mod h1:zNjk/86bjwLVJ4NcvInBcXcLdptdRFQ28sYrdFbrFgY= +gitlab.com/gitlab-org/gitaly-proto v1.33.1-0.20190620234657-0f7ad34f3faa h1:UxMg+wTfi661L7stu+guHU21YqBUTc2B36KOdlhjo6o= +gitlab.com/gitlab-org/gitaly-proto v1.33.1-0.20190620234657-0f7ad34f3faa/go.mod h1:zNjk/86bjwLVJ4NcvInBcXcLdptdRFQ28sYrdFbrFgY= gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c h1:xo48LcGsTCasKcJpQDBCCuZU+aP8uGaboUVvD7Lgm6g= gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c/go.mod h1:rYhLgfrbEcyfinG+R3EvKu6bZSsmwQqcXzLfHWSfUKM= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= diff --git a/internal/git/hooks/hooks.go b/internal/git/hooks/hooks.go index c77c3ed1e373ba8f7f080901f2b0d7492c1b9f2e..94426b8bd52f212b9798831e4cd4bc153f987b1b 100644 --- a/internal/git/hooks/hooks.go +++ b/internal/git/hooks/hooks.go @@ -25,3 +25,16 @@ func Path() string { return path.Join(config.Config.Ruby.Dir, "git-hooks") } + +// GitPath returns the path where the go implemeneted global git hooks are located +func GitPath() string { + if len(Override) > 0 { + return Override + } + + if os.Getenv("GITALY_TESTING_NO_GIT_HOOKS") == "1" { + return "/var/empty" + } + + return path.Join(config.Config.BinDir, "hooks") +} diff --git a/internal/rubyserver/rubyserver.go b/internal/rubyserver/rubyserver.go index 3b75b665ee739b2114beeb86b771cbd5e1f386f9..cda8da076e78af0bb19df00f02f8772d74ef0d6e 100644 --- a/internal/rubyserver/rubyserver.go +++ b/internal/rubyserver/rubyserver.go @@ -111,7 +111,7 @@ func Start() (*Server, error) { "GITALY_RUBY_GITLAB_SHELL_PATH="+cfg.GitlabShell.Dir, "GITALY_RUBY_GITALY_BIN_DIR="+cfg.BinDir, "GITALY_VERSION="+version.GetVersion(), - "GITALY_GIT_HOOKS_DIR="+hooks.Path(), + "GITALY_GIT_HOOKS_DIR="+hooks.GitPath(), ) env = append(env, command.GitEnv...) diff --git a/internal/service/hook/post_receive.go b/internal/service/hook/post_receive.go new file mode 100644 index 0000000000000000000000000000000000000000..f72ef94fe03c574d867cec176a59b7179cd53746 --- /dev/null +++ b/internal/service/hook/post_receive.go @@ -0,0 +1,39 @@ +package hook + +import ( + "bytes" + "context" + "fmt" + "os/exec" + + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/command" + "gitlab.com/gitlab-org/gitaly/internal/git/hooks" + "gitlab.com/gitlab-org/gitaly/internal/helper" +) + +func (s *server) PostReceive(ctx context.Context, in *gitalypb.PostReceiveHookRequest) (*gitalypb.PostReceiveHookResponse, error) { + postreceiveHook := fmt.Sprintf("%s/post-receive", hooks.Path()) + + var stdin bytes.Buffer + for _, ref := range in.GetRefs() { + stdin.WriteString(fmt.Sprintf("%s\n", ref)) + } + + env := []string{ + fmt.Sprintf("GL_REPO_PATH=%s", in.GetRepoPath()), + fmt.Sprintf("GL_ID=%s", in.GetKeyId()), + fmt.Sprintf("GL_REPOSITORY=%s", in.GetGlRepository()), + } + + cmd, err := command.New(ctx, exec.Command(postreceiveHook), &stdin, nil, nil, env...) + if err != nil { + return &gitalypb.PostReceiveHookResponse{}, helper.ErrInternal(err) + } + + if err := cmd.Wait(); err != nil { + return &gitalypb.PostReceiveHookResponse{}, helper.ErrInternal(err) + } + + return &gitalypb.PostReceiveHookResponse{}, nil +} diff --git a/internal/service/hook/pre_receive.go b/internal/service/hook/pre_receive.go new file mode 100644 index 0000000000000000000000000000000000000000..38d283c6cfc274568f4a737590d0c38aba616c13 --- /dev/null +++ b/internal/service/hook/pre_receive.go @@ -0,0 +1,40 @@ +package hook + +import ( + "bytes" + "context" + "fmt" + "os/exec" + + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/command" + "gitlab.com/gitlab-org/gitaly/internal/git/hooks" + "gitlab.com/gitlab-org/gitaly/internal/helper" +) + +func (s *server) PreReceive(ctx context.Context, in *gitalypb.PreReceiveHookRequest) (*gitalypb.PreReceiveHookResponse, error) { + prereceiveHook := fmt.Sprintf("%s/pre-receive", hooks.Path()) + + var stdin bytes.Buffer + for _, ref := range in.GetRefs() { + stdin.WriteString(fmt.Sprintf("%s\n", ref)) + } + + env := []string{ + fmt.Sprintf("GL_ID=%s", in.GetKeyId()), + fmt.Sprintf("GL_PROTOCOL=%s", in.GetProtocol()), + fmt.Sprintf("GL_REPO_PATH=%s", in.GetRepoPath()), + fmt.Sprintf("GL_REPOSITORY=%s", in.GetGlRepository()), + } + + cmd, err := command.New(ctx, exec.Command(prereceiveHook), &stdin, nil, nil, env...) + if err != nil { + return &gitalypb.PreReceiveHookResponse{}, helper.ErrInternal(err) + } + + if err := cmd.Wait(); err != nil { + return &gitalypb.PreReceiveHookResponse{}, helper.ErrInternal(err) + } + + return &gitalypb.PreReceiveHookResponse{}, nil +} diff --git a/internal/service/hook/server.go b/internal/service/hook/server.go new file mode 100644 index 0000000000000000000000000000000000000000..24473515570be3affe83dd9cb8b6901e4d2168ff --- /dev/null +++ b/internal/service/hook/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/hook/update.go b/internal/service/hook/update.go new file mode 100644 index 0000000000000000000000000000000000000000..2afe585cd9facd64b89c6302c8cae99fcb132421 --- /dev/null +++ b/internal/service/hook/update.go @@ -0,0 +1,32 @@ +package hook + +import ( + "context" + "fmt" + "os/exec" + + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/command" + "gitlab.com/gitlab-org/gitaly/internal/git/hooks" + "gitlab.com/gitlab-org/gitaly/internal/helper" +) + +func (s *server) Update(ctx context.Context, in *gitalypb.UpdateHookRequest) (*gitalypb.UpdateHookResponse, error) { + updateHook := fmt.Sprintf("%s/pre-receive", hooks.Path()) + + env := []string{ + fmt.Sprintf("GL_ID=%s", in.GetKeyId()), + fmt.Sprintf("GL_REPO_PATH=%s", in.GetRepoPath()), + } + + cmd, err := command.New(ctx, exec.Command(updateHook, in.GetRef(), in.GetOldValue(), in.GetNewValue()), nil, nil, nil, env...) + if err != nil { + return &gitalypb.UpdateHookResponse{}, helper.ErrInternal(err) + } + + if err := cmd.Wait(); err != nil { + return &gitalypb.UpdateHookResponse{}, helper.ErrInternal(err) + } + + return &gitalypb.UpdateHookResponse{}, nil +} diff --git a/internal/service/smarthttp/receive_pack.go b/internal/service/smarthttp/receive_pack.go index 5ff0d584e51281033539f0d0cd7d0cb5dab7b912..c93f61ffb88c2ac6813e758e7e03adae4dcc7f0d 100644 --- a/internal/service/smarthttp/receive_pack.go +++ b/internal/service/smarthttp/receive_pack.go @@ -45,6 +45,7 @@ func (s *server) PostReceivePack(stream gitalypb.SmartHTTPService_PostReceivePac fmt.Sprintf("GL_ID=%s", req.GlId), "GL_PROTOCOL=http", fmt.Sprintf("GITLAB_SHELL_DIR=%s", config.Config.GitlabShell.Dir), + fmt.Sprintf("GL_URL=%s", config.Config.SocketPath), } if req.GlRepository != "" { env = append(env, fmt.Sprintf("GL_REPOSITORY=%s", req.GlRepository)) @@ -61,7 +62,7 @@ func (s *server) PostReceivePack(stream gitalypb.SmartHTTPService_PostReceivePac env = git.AddGitProtocolEnv(ctx, req, env) env = append(env, command.GitEnv...) - opts := append([]string{fmt.Sprintf("core.hooksPath=%s", hooks.Path())}, req.GitConfigOptions...) + opts := append([]string{fmt.Sprintf("core.hooksPath=%s", hooks.GitPath())}, req.GitConfigOptions...) gitOptions := git.BuildGitOptions(opts, "receive-pack", "--stateless-rpc", repoPath) cmd, err := git.BareCommand(ctx, stdin, stdout, nil, env, gitOptions...) diff --git a/internal/service/ssh/receive_pack.go b/internal/service/ssh/receive_pack.go index e952e77779a7f739ebeb9bca0727cc398b7c0c93..f03e2291ff6423d053cfc778489916a670406b15 100644 --- a/internal/service/ssh/receive_pack.go +++ b/internal/service/ssh/receive_pack.go @@ -56,6 +56,7 @@ func sshReceivePack(stream gitalypb.SSHService_SSHReceivePackServer, req *gitaly fmt.Sprintf("GL_USERNAME=%s", req.GlUsername), "GL_PROTOCOL=ssh", fmt.Sprintf("GITLAB_SHELL_DIR=%s", config.Config.GitlabShell.Dir), + fmt.Sprintf("GL_URL=%s", config.Config.SocketPath), } if req.GlRepository != "" { env = append(env, fmt.Sprintf("GL_REPOSITORY=%s", req.GlRepository)) @@ -69,7 +70,7 @@ func sshReceivePack(stream gitalypb.SSHService_SSHReceivePackServer, req *gitaly env = git.AddGitProtocolEnv(ctx, req, env) env = append(env, command.GitEnv...) - opts := append([]string{fmt.Sprintf("core.hooksPath=%s", hooks.Path())}, req.GitConfigOptions...) + opts := append([]string{fmt.Sprintf("core.hooksPath=%s", hooks.GitPath())}, req.GitConfigOptions...) gitOptions := git.BuildGitOptions(opts, "receive-pack", repoPath) cmd, err := git.BareCommand(ctx, stdin, stdout, stderr, env, gitOptions...) diff --git a/ruby/gitlab-shell/hooks/post-receive b/ruby/gitlab-shell/hooks/post-receive index 2b6538f0323d08966071c43da8c575cfb8f7890b..b8b2850b22a85032f3c26573b23dfe202493acb9 100755 --- a/ruby/gitlab-shell/hooks/post-receive +++ b/ruby/gitlab-shell/hooks/post-receive @@ -6,7 +6,8 @@ refs = $stdin.read key_id = ENV.delete('GL_ID') gl_repository = ENV['GL_REPOSITORY'] -repo_path = Dir.pwd +#repo_path = Dir.pwd +repo_path = ENV['GL_REPO_PATH'] require_relative '../lib/gitlab_custom_hook' require_relative '../lib/hooks_utils' diff --git a/ruby/gitlab-shell/hooks/pre-receive b/ruby/gitlab-shell/hooks/pre-receive index 6ce5879519575bca9854a0fe93f13b71dac9830e..658a712c51e2c0442f8f1bf557edb8d6a2bb1077 100755 --- a/ruby/gitlab-shell/hooks/pre-receive +++ b/ruby/gitlab-shell/hooks/pre-receive @@ -6,7 +6,8 @@ refs = $stdin.read key_id = ENV.delete('GL_ID') protocol = ENV.delete('GL_PROTOCOL') -repo_path = Dir.pwd +#repo_path = Dir.pwd +repo_path = ENV.delete('GL_REPO_PATH') gl_repository = ENV['GL_REPOSITORY'] def increase_reference_counter(gl_repository, repo_path) diff --git a/ruby/gitlab-shell/hooks/update b/ruby/gitlab-shell/hooks/update index 4c2fc08b0d7cfcf90bf4f296bccc8468f8668a89..78e43d293d7889532c21e77cfc29787ed81ce600 100755 --- a/ruby/gitlab-shell/hooks/update +++ b/ruby/gitlab-shell/hooks/update @@ -6,7 +6,8 @@ ref_name = ARGV[0] old_value = ARGV[1] new_value = ARGV[2] -repo_path = Dir.pwd +#repo_path = Dir.pwd +repo_path = ENV.delete('GL_REPO_PATH') key_id = ENV.delete('GL_ID') require_relative '../lib/gitlab_custom_hook'