Skip to content

Commit

Permalink
Hooks support
Browse files Browse the repository at this point in the history
  • Loading branch information
hedhyw authored and Dima committed Nov 22, 2020
1 parent 3f40c4a commit 0c04bfb
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 8 deletions.
6 changes: 5 additions & 1 deletion cmd/glmt/glmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"gitlab.com/gitlab-merge-tool/glmt/internal/gitlab"
gitlabi "gitlab.com/gitlab-merge-tool/glmt/internal/gitlab/impl"
"gitlab.com/gitlab-merge-tool/glmt/internal/glmt"
hooksi "gitlab.com/gitlab-merge-tool/glmt/internal/hooks/impl"
"gitlab.com/gitlab-merge-tool/glmt/internal/notifier"
teami "gitlab.com/gitlab-merge-tool/glmt/internal/team/impl"

Expand Down Expand Up @@ -177,5 +178,8 @@ func createCore(dryRun bool, out io.StringWriter, cfg *config.Config) (*glmt.Cor
return nil, err
}

return glmt.NewGLMT(git, gitlab, n, ts), nil
hsCfg := cfg.Hooks
hs := hooksi.NewHooks(hsCfg, os.Stdout, os.Stderr)

return glmt.NewGLMT(git, gitlab, n, ts, hs), nil
}
30 changes: 30 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
package config

import (
"encoding/json"
"os"
"time"

"github.com/yosuke-furukawa/json5/encoding/json5"
)
Expand All @@ -12,6 +14,7 @@ type Config struct {
MR MR `json:"mr"`
Notifier Notifier `json:"notifier"`
Mentioner Mentioner `json:"mentioner"`
Hooks Hooks `json:"hooks"`
}

type GitLab struct {
Expand Down Expand Up @@ -43,6 +46,12 @@ type Mentioner struct {
MentionsCount int `json:"count"`
}

type Hooks struct {
AfterCommands map[string][]string `json:"after"`
BeforeCommands map[string][]string `json:"before"`
Timeout Duration `json:"timeout"`
}

func LoadConfig(path string) (*Config, error) {
f, err := os.Open(path)
if err != nil {
Expand All @@ -59,3 +68,24 @@ func LoadConfig(path string) (*Config, error) {

return &c, nil
}

type Duration time.Duration

func (d *Duration) UnmarshalJSON(b []byte) (err error) {
var rawTime string

err = json.Unmarshal(b, &rawTime)
if err != nil {
return err
}

var td time.Duration
td, err = time.ParseDuration(rawTime)
if err != nil {
return err
}

*d = Duration(td)

return nil
}
3 changes: 2 additions & 1 deletion internal/gitlab/impl/http_gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import (
"net/url"
"time"

"github.com/rs/zerolog/log"
"gitlab.com/gitlab-merge-tool/glmt/internal/gitlab"

"github.com/rs/zerolog/log"
)

func NewHTTPGitLab(token, host string) *HTTPGitLab {
Expand Down
34 changes: 29 additions & 5 deletions internal/glmt/glmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package glmt
import (
"context"
"errors"
"fmt"
"math/rand"
"net/url"
"regexp"
Expand All @@ -14,6 +15,7 @@ import (

"gitlab.com/gitlab-merge-tool/glmt/internal/gerr"
"gitlab.com/gitlab-merge-tool/glmt/internal/gitlab"
"gitlab.com/gitlab-merge-tool/glmt/internal/hooks"
"gitlab.com/gitlab-merge-tool/glmt/internal/team"
"gitlab.com/gitlab-merge-tool/glmt/internal/templating"
)
Expand All @@ -31,12 +33,14 @@ func NewGLMT(
gitLab gitlab.GitLab,
notifier Notifier,
teamSource team.TeamFileSource,
hooks hooks.Runner,
) *Core {
return &Core{
git: git,
gitLab: gitLab,
notifier: notifier,
teamSource: teamSource,
hooks: hooks,
}
}

Expand All @@ -46,6 +50,7 @@ type Core struct {
notifier Notifier
teamSource team.TeamFileSource
currentUsername string
hooks hooks.Runner
}

type CreateMRParams struct {
Expand Down Expand Up @@ -88,18 +93,18 @@ func (c *Core) CreateMR(ctx context.Context, params CreateMRParams) (MergeReques
return mr, err
}

cu, err := c.currentUser(ctx)
if err != nil {
return mr, err
}

var ms []*team.Member
if c.teamSource != nil && params.MentionsCount > 0 {
tm, err := c.teamSource.Team(ctx)
if err != nil {
return mr, err
}

cu, err := c.currentUser(ctx)
if err != nil {
return mr, err
}

ms = Mentions(tm, cu, p, params.MentionsCount)
}

Expand All @@ -125,6 +130,19 @@ func (c *Core) CreateMR(ctx context.Context, params CreateMRParams) (MergeReques
d = "Merge " + br + " into " + params.TargetBranch
}

hp := hooks.Params{
Branch: br,
Project: p,
Remote: r,
Username: cu,
// This value will be set in after hook.
MergeRequestURL: "",
}
err = c.hooks.RunBefore(ctx, hp)
if err != nil {
return mr, fmt.Errorf("hooks precondition failed: %w", err)
}

log.Ctx(ctx).Debug().
Interface("context", ta).
Str("title", t).
Expand All @@ -144,6 +162,12 @@ func (c *Core) CreateMR(ctx context.Context, params CreateMRParams) (MergeReques
return mr, err
}

hp.MergeRequestURL = gmr.URL
err = c.hooks.RunAfter(ctx, hp)
if err != nil {
return mr, fmt.Errorf("hooks postcondition failed: %w", err)
}

mr.ID = gmr.ID
mr.IID = gmr.IID
mr.ProjectID = gmr.ProjectID
Expand Down
4 changes: 4 additions & 0 deletions internal/glmt/glmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"regexp"
"testing"

"gitlab.com/gitlab-merge-tool/glmt/internal/config"
"gitlab.com/gitlab-merge-tool/glmt/internal/gitlab"
hooksi "gitlab.com/gitlab-merge-tool/glmt/internal/hooks/impl"
"gitlab.com/gitlab-merge-tool/glmt/internal/team"
teami "gitlab.com/gitlab-merge-tool/glmt/internal/team/impl"
)
Expand Down Expand Up @@ -110,10 +112,12 @@ func TestCreateMR(t *testing.T) {
}

ts, _ := teami.NewTeamSource("")
hs := hooksi.NewHooks(config.Hooks{}, nil, nil)
c := Core{
git: gs,
gitLab: gls,
teamSource: ts,
hooks: hs,
}

mr, err := c.CreateMR(context.Background(), cp)
Expand Down
35 changes: 35 additions & 0 deletions internal/hooks/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package hooks

import (
"context"
"reflect"
)

type Runner interface {
RunAfter(ctx context.Context, params Params) error
RunBefore(ctx context.Context, params Params) error
}

// Params contains environment variables for hook commands. All values
// should be in string format.
type Params struct {
Remote string `env:"GLMT_REMOTE"`
Branch string `env:"GLMT_BRANCH"`
MergeRequestURL string `env:"GLMT_MR_URL"`
Project string `env:"GLMT_PROJECT"`
Username string `env:"GLMT_USERNAME"`
}

// Env specifies the environment by the params. Each entry is of the
// form "key=value".
func (h Params) Env() (env []string) {
rt := reflect.TypeOf(h)
rv := reflect.ValueOf(h)

env = make([]string, rt.NumField())
for i := 0; i < rt.NumField(); i++ {
env[i] = rt.Field(i).Tag.Get("env") + "=" + rv.Field(i).String()
}

return env
}
29 changes: 29 additions & 0 deletions internal/hooks/hooks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package hooks_test

import (
"testing"

"gitlab.com/gitlab-merge-tool/glmt/internal/hooks"
)

func TestHookParamsEnv(t *testing.T) {
const exp = "GLMT_BRANCH=master"

params := hooks.Params{
Branch: "master",
}

env := params.Env()

var got bool
for _, envVal := range env {
if envVal == exp {
got = true
break
}
}

if !got {
t.Fatal(env)
}
}
91 changes: 91 additions & 0 deletions internal/hooks/impl/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Package impl implements hooks.Runner.
package impl

import (
"context"
"fmt"
"io"
"os/exec"
"time"

"gitlab.com/gitlab-merge-tool/glmt/internal/config"
"gitlab.com/gitlab-merge-tool/glmt/internal/hooks"
)

type Hooks struct {
afterCommands map[string][]string
beforeCommands map[string][]string

timeout time.Duration

stdout io.Writer
stderr io.Writer
}

func NewHooks(cfg config.Hooks, stdout io.Writer, stderr io.Writer) *Hooks {
const defaultTimeout = 5 * time.Second

timeout := time.Duration(cfg.Timeout)
if timeout == 0 {
timeout = defaultTimeout
}

return &Hooks{
afterCommands: filterCommands(cfg.AfterCommands),
beforeCommands: filterCommands(cfg.BeforeCommands),

timeout: timeout,

stdout: stdout,
stderr: stderr,
}
}

func (h Hooks) RunAfter(ctx context.Context, params hooks.Params) (err error) {
return h.run(ctx, h.afterCommands, params)
}

func (h Hooks) RunBefore(ctx context.Context, params hooks.Params) error {
return h.run(ctx, h.beforeCommands, params)
}

func (h Hooks) run(
ctx context.Context,
commands map[string][]string,
params hooks.Params,
) (err error) {
env := params.Env()

var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, h.timeout)
defer cancel()

for name, cmd := range commands {
cmdProc := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
cmdProc.Env = env
cmdProc.Stdout = h.stdout
cmdProc.Stderr = h.stderr

err = cmdProc.Run()
if err != nil {
return fmt.Errorf("%s: running command: %w", name, err)
}
}

return nil
}

// filterCommands removes empty commands.
func filterCommands(commands map[string][]string) (filtered map[string][]string) {
filtered = make(map[string][]string, len(commands))

for name, cmd := range commands {
if len(cmd) == 0 {
continue
}

filtered[name] = cmd
}

return filtered
}
Loading

0 comments on commit 0c04bfb

Please sign in to comment.