<|start_filename|>internal/rules/exec.go<|end_filename|>
package rules

import (
	"context"
	"fisherman/internal/utils"
	"fisherman/pkg/shell/helpers"
	"io"
	"os/exec"

	"github.com/hashicorp/go-multierror"
	"github.com/kballard/go-shellquote"
	"golang.org/x/text/transform"
	"gopkg.in/yaml.v3"
)

var CommandContext = exec.CommandContext

const ExecType = "exec"

type Exec struct {
	BaseRule `yaml:",inline"`
	Name     string            `yaml:"name"`
	Env      map[string]string `yaml:"env"`
	Output   bool              `yaml:"output"`
	Dir      string            `yaml:"dir"`
	Commands []CommandDef      `yaml:"commands"`
}

type CommandDef struct {
	Program  string            `yaml:"program"`
	Args     []string          `yaml:"args"`
	Env      map[string]string `yaml:"env"`
	Output   bool              `yaml:"output"`
	Encoding string            `yaml:"encoding"`
	Dir      string            `yaml:"dir"`
}

func (c *CommandDef) UnmarshalYAML(value *yaml.Node) error {
	(*c) = CommandDef{}

	var shortForm string
	if err := value.Decode(&shortForm); err == nil {
		parts, err := shellquote.Split(shortForm)
		if err != nil {
			return err
		}

		c.Program = parts[0]
		c.Args = parts[1:]

		return nil
	}

	type plain CommandDef

	return value.Decode((*plain)(c))
}

func (c *CommandDef) Compile(variables map[string]interface{}) {
	utils.FillTemplate(&c.Program, variables)
	utils.FillTemplatesArray(c.Args, variables)
	utils.FillTemplatesMap(c.Env, variables)
	utils.FillTemplate(&c.Dir, variables)
}

func (r *Exec) GetPosition() byte {
	return Scripts
}

func (r *Exec) GetPrefix() string {
	if utils.IsEmpty(r.Name) {
		return ExecType
	}

	return r.Name
}

func (r *Exec) Check(ctx context.Context, output io.Writer) error {
	env := helpers.MergeEnv(r.BaseRule.env, r.Env)

	var resultError *multierror.Error
	for _, commandDef := range r.Commands {
		encoding, err := getEncoding(commandDef.Encoding)
		if err != nil {
			return err
		}

		command := CommandContext(ctx, commandDef.Program, commandDef.Args...)
		command.Env = helpers.MergeEnv(env, commandDef.Env)
		command.Dir = utils.FirstNotEmpty(commandDef.Dir, r.Dir, r.BaseRule.cwd)
		command.Stdout = transform.NewWriter(output, encoding.NewDecoder())
		command.Stderr = transform.NewWriter(output, encoding.NewDecoder())

		if err := command.Run(); err != nil {
			resultError = multierror.Append(resultError, err)
		}
	}

	return resultError.ErrorOrNil()
}

func (r *Exec) Compile(variables map[string]interface{}) {
	r.BaseRule.Compile(variables)
	utils.FillTemplate(&r.Dir, variables)
	utils.FillTemplate(&r.Name, variables)
	utils.FillTemplatesMap(r.Env, variables)
	for i := 0; i < len(r.Commands); i++ {
		r.Commands[i].Compile(variables)
	}
}


<|start_filename|>internal/rules/add_to_index_test.go<|end_filename|>
package rules_test

import (
	"context"
	syserrors "errors"
	. "fisherman/internal/rules"
	"fisherman/internal/validation"
	"fisherman/testing/mocks"
	"io/ioutil"
	"testing"

	"github.com/go-errors/errors"
	"github.com/go-git/go-git/v5"
	"github.com/stretchr/testify/assert"
)

func TestAddToIndex_GetPosition(t *testing.T) {
	rule := AddToIndex{}

	assert.Equal(t, PostScripts, rule.GetPosition())
}

func TestAddToIndex_Compile(t *testing.T) {
	rule := AddToIndex{
		Globs: []Glob{
			{Pattern: "{{var1}}", IsRequired: false},
			{Pattern: "data", IsRequired: false},
		},
	}

	rule.Compile(map[string]interface{}{"var1": "VALUE"})

	assert.Equal(t, AddToIndex{
		Globs: []Glob{
			{Pattern: "VALUE", IsRequired: false},
			{Pattern: "data", IsRequired: false},
		},
	}, rule)
}

func TestAddToIndex_Check(t *testing.T) {
	t.Run("not configured rules", func(t *testing.T) {
		rule := AddToIndex{BaseRule: BaseRule{Type: AddToIndexType}}

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.NoError(t, err)
	})

	t.Run("added to index correctly", func(t *testing.T) {
		repo := mocks.NewRepositoryMock(t).
			AddGlobMock.When("glob1/*.go").Then(nil).
			AddGlobMock.When("*.css").Then(nil).
			AddGlobMock.When("mocks").Then(nil)

		rule := makeRule(
			&AddToIndex{
				Globs: []Glob{
					{"glob1/*.go", true},
					{"*.css", true},
					{"mocks", true},
				},
			},
			WithRepository(repo),
		)

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.NoError(t, err)
	})

	t.Run("failed to add files", func(t *testing.T) {
		repo := mocks.NewRepositoryMock(t).
			AddGlobMock.When("glob1/*.go").Then(nil).
			AddGlobMock.When("*.css").Then(syserrors.New("testError")).
			AddGlobMock.When("mocks").Then(nil)

		rule := makeRule(
			&AddToIndex{
				Globs: []Glob{
					{"glob1/*.go", true},
					{"*.css", true},
					{"mocks", true},
				},
			},
			WithRepository(repo),
		)

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.EqualError(t, err, "failed to add files matching pattern '*.css' to the index: testError")
		assert.IsType(t, &errors.Error{}, err)
	})

	t.Run("failed to add files in index with optional flag", func(t *testing.T) {
		repo := mocks.NewRepositoryMock(t).
			AddGlobMock.When("glob1/*.go").Then(nil).
			AddGlobMock.When("*.css").Then(git.ErrGlobNoMatches).
			AddGlobMock.When("mocks").Then(nil)

		tests := []struct {
			name       string
			isRequired bool
		}{
			{name: "optional is true", isRequired: false},
			{name: "optional is false", isRequired: true},
		}

		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				rule := makeRule(
					&AddToIndex{
						BaseRule: BaseRule{Type: AddToIndexType},
						Globs: []Glob{
							{"glob1/*.go", tt.isRequired},
							{"*.css", tt.isRequired},
							{"mocks", tt.isRequired},
						},
					},
					WithRepository(repo),
				)

				err := rule.Check(context.TODO(), ioutil.Discard)

				if !tt.isRequired {
					assert.NoError(t, err)
				} else {
					assert.EqualError(t, err, errorMessage(AddToIndexType, "can't add files matching pattern *.css"))
					assert.IsType(t, &validation.Error{}, err)
				}
			})
		}
	})
}


<|start_filename|>pkg/log/log.go<|end_filename|>
package log

import (
	"fmt"
	"io"
	"os"

	"github.com/fatih/color"
)

type Level int

const (
	DebugLevel Level = iota
	InfoLevel
	ErrorLevel
	NoneLevel
)

var generalOutput io.Writer = os.Stdout

var (
	err *color.Color = color.New(color.FgRed).Add(color.Bold)
	dbg *color.Color = color.New(color.FgYellow)
	inf *color.Color = color.New(color.FgWhite)
)

var (
	ErrorOutput = NewlevelWriter(generalOutput, ErrorLevel)
	DebugOutput = NewlevelWriter(generalOutput, DebugLevel)
	InfoOutput  = NewlevelWriter(generalOutput, InfoLevel)
)

func SetOutput(output io.Writer) {
	generalOutput = output
	ErrorOutput.SetOutput(output)
	DebugOutput.SetOutput(output)
	InfoOutput.SetOutput(output)
}

func Configure(config OutputConfig) {
	if !config.Colors {
		err.DisableColor()
		dbg.DisableColor()
		inf.DisableColor()
	}
	ErrorOutput.SetLevel(config.LogLevel)
	DebugOutput.SetLevel(config.LogLevel)
	InfoOutput.SetLevel(config.LogLevel)
}

func Debug(params ...interface{}) {
	dbg.Fprintln(DebugOutput, params...)
}

func Debugf(message string, params ...interface{}) {
	dbg.Fprintln(DebugOutput, fmt.Sprintf(message, params...))
}

func Error(params ...interface{}) {
	err.Fprintln(ErrorOutput, params...)
}

func Errorf(message string, params ...interface{}) {
	err.Fprintln(ErrorOutput, fmt.Sprintf(message, params...))
}

func Info(params ...interface{}) {
	inf.Fprintln(InfoOutput, params...)
}

func Infof(message string, params ...interface{}) {
	inf.Fprintln(InfoOutput, fmt.Sprintf(message, params...))
}

func Stdout() io.Writer {
	return generalOutput
}


<|start_filename|>internal/internal.go<|end_filename|>
package internal

import (
	"context"
	"fisherman/pkg/vcs"
)

// Repository is interface to comunicate with git.
type Repository interface {
	GetCurrentBranch() (string, error)
	GetUser() (vcs.User, error)
	GetLastTag() (string, error)
	AddGlob(glob string) error
	RemoveGlob(glob string) error
	GetFilesInIndex() ([]string, error)
	GetIndexChanges() (map[string]vcs.Changes, error)
}

// CliCommand is interface to define cli command.
type CliCommand interface {
	Run(ctx context.Context, args []string) error
	Name() string
	Description() string
}


<|start_filename|>internal/utils/panic_interceptor.go<|end_filename|>
package utils

func PanicInterceptor(action func(interface{})) {
	if recovered := recover(); recovered != nil {
		action(recovered)
	}
}


<|start_filename|>internal/commands/remove/remove_test.go<|end_filename|>
package remove_test

import (
	"context"
	"errors"
	. "fisherman/internal/commands/remove"
	"fisherman/internal/configuration"
	"fisherman/pkg/log"
	"fisherman/testing/mocks"
	"fisherman/testing/testutils"
	"io"
	"path/filepath"
	"testing"

	"github.com/go-git/go-billy/v5"
	"github.com/stretchr/testify/assert"
)

// nolint: gochecknoinits
func init() {
	log.SetOutput(io.Discard)
}

func TestCommand_Run(t *testing.T) {
	cwd := filepath.Join("usr", "home")
	configs := map[string]string{
		configuration.GlobalMode: filepath.Join("usr", "home", ".fisherman.yml"),
	}

	tests := []struct {
		name        string
		fs          billy.Filesystem
		expectedErr string
	}{
		{
			name: "executed successful",
			fs: testutils.FsFromMap(t, map[string]string{
				filepath.Join("usr", "home", ".fisherman.yml"):              "content",
				filepath.Join("usr", "home", ".fisherman.yaml"):             "content",
				filepath.Join("usr", "home", ".git", "hooks", "commit-msg"): "content",
				filepath.Join("usr", "home", ".fisherman.yml"):              "content",
			}),
		},
		{
			name: "exist errors",
			fs: mocks.NewFilesystemMock(t).
				StatMock.Return(nil, errors.New("test error")),
			expectedErr: "test error",
		},
		{
			name: "delete error",
			fs: mocks.NewFilesystemMock(t).
				StatMock.Return(nil, nil).
				RemoveMock.Return(errors.New("delete error")),
			expectedErr: "delete error",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			command := NewCommand(
				WithFileSystem(tt.fs),
				WithCwd(cwd),
				WithConfigFiles(configs),
			)

			err := command.Run(context.TODO(), []string{})

			testutils.AssertError(t, tt.expectedErr, err)
		})
	}
}

func TestCommand_Name(t *testing.T) {
	command := NewCommand(
		WithFileSystem(mocks.NewFilesystemMock(t)),
		WithCwd("/"),
	)

	assert.Equal(t, "remove", command.Name())
}

func TestCommand_Description(t *testing.T) {
	command := NewCommand(
		WithFileSystem(mocks.NewFilesystemMock(t)),
		WithCwd("/"),
	)

	assert.NotEmpty(t, command.Description())
}

func TestNewCommand(t *testing.T) {
	t.Run("panic when cwd is not configured", func(t *testing.T) {
		fs := mocks.NewFilesystemMock(t)
		assert.PanicsWithError(t, "Cwd should be configured", func() {
			_ = NewCommand(WithFileSystem(fs))
		})
	})

	t.Run("panic when filesystem is not configured", func(t *testing.T) {
		assert.PanicsWithError(t, "FileSystem should be configured", func() {
			_ = NewCommand(WithCwd("/"))
		})
	})
}


<|start_filename|>internal/rules/base_rule_options.go<|end_filename|>
package rules

import (
	"fisherman/internal"

	"github.com/go-git/go-billy/v5"
)

func WithFileSystem(fileSystem billy.Filesystem) RuleOption {
	return func(ac *BaseRule) {
		ac.fs = fileSystem
	}
}

func WithCwd(cwd string) RuleOption {
	return func(ac *BaseRule) {
		ac.cwd = cwd
	}
}

func WithRepository(repository internal.Repository) RuleOption {
	return func(ac *BaseRule) {
		ac.repo = repository
	}
}

func WithArgs(args []string) RuleOption {
	return func(ac *BaseRule) {
		ac.args = args
	}
}

// WithEnv setups environment variables for BaseRule.
func WithEnv(env []string) RuleOption {
	return func(ac *BaseRule) {
		ac.env = env
	}
}


<|start_filename|>pkg/shell/bash_strategy.go<|end_filename|>
package shell

import (
	"context"
	"os/exec"

	"golang.org/x/text/encoding"
)

type BashStrategy struct{}

const bashBin = "bash"

func Bash() *BashStrategy {
	return &BashStrategy{}
}

func (s *BashStrategy) GetName() string {
	return bashBin
}

func (s *BashStrategy) GetCommand(ctx context.Context) *exec.Cmd {
	return exec.CommandContext(ctx, bashBin)
}

func (s *BashStrategy) ArgsWrapper(args []string) []string {
	return args
}

func (s *BashStrategy) EnvWrapper(env []string) []string {
	return env
}

func (s *BashStrategy) GetEncoding() encoding.Encoding {
	return encoding.Nop
}


<|start_filename|>internal/commands/initialize/initialize.go<|end_filename|>
package initialize

import (
	"context"
	"fisherman/internal/configuration"
	"fisherman/internal/constants"
	"fisherman/internal/utils"
	"fisherman/pkg/guards"
	"fisherman/pkg/log"
	"flag"
	"fmt"
	"os"
	"os/user"
	"path/filepath"

	"github.com/go-errors/errors"
	"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/util"
	"github.com/hashicorp/go-multierror"
)

type Command struct {
	flagSet    *flag.FlagSet
	mode       string
	force      bool
	absolute   bool
	usage      string
	fs         billy.Filesystem
	user       *user.User
	cwd        string
	executable string
}

// TODO: Refactor to implement options pattern.
func NewCommand(options ...initializeOption) *Command {
	command := &Command{
		flagSet: flag.NewFlagSet("init", flag.ExitOnError),
		usage:   "initializes fisherman in git repository",
	}

	for _, option := range options {
		option(command)
	}

	guards.ShouldBeDefined(command.fs, "FileSystem should be configured")
	guards.ShouldBeNotEmpty(command.cwd, "Cwd should be configured")
	guards.ShouldBeNotEmpty(command.executable, "Executable should be configured")
	guards.ShouldBeDefined(command.user, "User should be configured")

	modeMessage := fmt.Sprintf(
		"config location: %s, %s (default), %s. read more here %s",
		configuration.LocalMode,
		configuration.RepoMode,
		configuration.GlobalMode,
		constants.ConfigurationInheritanceURL)

	command.flagSet.StringVar(&command.mode, "mode", configuration.RepoMode, modeMessage)
	command.flagSet.BoolVar(&command.force, "force", false, "forces overwrites existing hooks")
	command.flagSet.BoolVar(&command.absolute, "absolute", false, "used absolute path to binary in hook")

	return command
}

func (c *Command) Run(ctx context.Context, args []string) error {
	err := c.flagSet.Parse(args)
	if err != nil {
		return err
	}

	log.Debugf("Statring initialization (force = %t)", c.force)
	if !c.force {
		var result *multierror.Error
		for _, hookName := range constants.HooksNames {
			hookPath := filepath.Join(c.cwd, ".git", "hooks", hookName)
			log.Debugf("Cheking hook '%s' (%s)", hookName, hookPath)
			exist, err := utils.Exists(c.fs, hookPath)
			if err != nil {
				return err
			}

			if exist {
				log.Debugf("Hook '%s' already exist", hookName)
				result = multierror.Append(result, errors.Errorf("file %s already exists", hookPath))
			}
		}

		err := result.ErrorOrNil()
		if err != nil {
			return err
		}
	}

	for _, hookName := range constants.HooksNames {
		hookPath := filepath.Join(c.cwd, ".git", "hooks", hookName)

		err := util.WriteFile(c.fs, hookPath, buildHook(hookName, c.getBinaryPath(), c.absolute), os.ModePerm)
		if err != nil {
			return err
		}

		log.Infof("Hook '%s' (%s) was writted", hookName, hookPath)
	}

	return c.writeConfig()
}

func (c *Command) Name() string {
	return c.flagSet.Name()
}

func (c *Command) Description() string {
	return c.usage
}

func (c *Command) writeConfig() error {
	configFolder, err := configuration.GetConfigFolder(c.user, c.cwd, c.mode)
	if err != nil {
		return err
	}

	configPath := filepath.Join(configFolder, constants.AppConfigNames[0])
	exist, err := utils.Exists(c.fs, configPath)
	if err != nil {
		return err
	}

	if !exist {
		content := configuration.DefaultConfig
		utils.FillTemplate(&content, map[string]interface{}{
			"URL": constants.ConfigurationFilesDocksURL,
		})

		err := util.WriteFile(c.fs, configPath, []byte(content), os.ModePerm)
		if err != nil {
			return err
		}
	}

	return nil
}

func (c *Command) getBinaryPath() string {
	if c.absolute {
		return c.executable
	}

	return constants.AppName
}


<|start_filename|>internal/rules/suppress_commit_files_test.go<|end_filename|>
package rules_test

import (
	"context"
	"errors"
	. "fisherman/internal/rules"
	"fisherman/testing/mocks"
	"io/ioutil"
	"testing"

	"github.com/go-git/go-git/v5"
	"github.com/stretchr/testify/assert"
)

func TestSuppressCommitFiles_GetPosition(t *testing.T) {
	rule := SuppressCommitFiles{
		BaseRule:        BaseRule{Type: SuppressCommitFilesType},
		Globs:           []string{"glob1", "glob2", "glob3"},
		RemoveFromIndex: true,
	}

	assert.Equal(t, PostScripts, rule.GetPosition())
}

func TestSuppressCommitFiles_Compile(t *testing.T) {
	rule := SuppressCommitFiles{
		BaseRule: BaseRule{
			Type:      SuppressCommitFilesType,
			Condition: "{{var1}}",
		},
		Globs:           []string{"{{var1}}", "{{var1}}.css", "mocks"},
		RemoveFromIndex: true,
	}

	rule.Compile(map[string]interface{}{"var1": "VALUE"})

	assert.Equal(t, SuppressCommitFiles{
		BaseRule: BaseRule{
			Type:      SuppressCommitFilesType,
			Condition: "VALUE",
		},
		Globs:           []string{"VALUE", "VALUE.css", "mocks"},
		RemoveFromIndex: true,
	}, rule)
}

func TestSuppressCommitFiles_Check(t *testing.T) {
	t.Run("not configured rule", func(t *testing.T) {
		rule := SuppressCommitFiles{
			BaseRule: BaseRule{Type: SuppressCommitFilesType},
		}

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.NoError(t, err)
	})

	t.Run("suppressed add files", func(t *testing.T) {
		repo := mocks.NewRepositoryMock(t).
			RemoveGlobMock.When("glob1/demo.go").Then(nil).
			GetFilesInIndexMock.Expect().Return([]string{"glob1/demo.go"}, nil)

		rule := makeRule(
			&SuppressCommitFiles{
				BaseRule: BaseRule{Type: SuppressCommitFilesType},
				Globs:    []string{"glob1/*.go", "*.css", "mocks"},
			},
			WithRepository(repo),
		)

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.EqualError(t, err, "1 error occurred:\n\t* [suppress-commit-files] file glob1/demo.go can not be committed\n\n")
	})

	t.Run("removed files from index", func(t *testing.T) {
		repo := mocks.NewRepositoryMock(t).
			RemoveGlobMock.When("glob1/demo.go").Then(nil).
			RemoveGlobMock.When("demo.css").Then(git.ErrGlobNoMatches).
			GetFilesInIndexMock.Expect().Return([]string{"glob1/demo.go", "demo.css"}, nil)

		rule := makeRule(
			&SuppressCommitFiles{
				BaseRule:        BaseRule{Type: SuppressCommitFilesType},
				Globs:           []string{"glob1/*.go", "*.css", "mocks"},
				RemoveFromIndex: true,
			},
			WithRepository(repo),
		)

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.NoError(t, err)
	})

	t.Run("get files in index error", func(t *testing.T) {
		repo := mocks.NewRepositoryMock(t).
			GetFilesInIndexMock.Expect().Return([]string{"glob1/demo.go"}, errors.New("test error"))

		rule := makeRule(
			&SuppressCommitFiles{
				BaseRule: BaseRule{Type: SuppressCommitFilesType},
				Globs:    []string{"glob1/*.go", "*.css", "mocks"},
			},
			WithRepository(repo),
		)

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.EqualError(t, err, "test error")
	})

	t.Run("glob patter parsing error", func(t *testing.T) {
		repo := mocks.NewRepositoryMock(t).
			GetFilesInIndexMock.Expect().Return([]string{"glob1/demo.go"}, nil)

		rule := makeRule(
			&SuppressCommitFiles{
				BaseRule: BaseRule{Type: SuppressCommitFilesType},
				Globs:    []string{"[/"},
			},
			WithRepository(repo),
		)

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.EqualError(t, err, "syntax error in pattern")
	})

	t.Run("removing files from index error", func(t *testing.T) {
		repo := mocks.NewRepositoryMock(t).
			RemoveGlobMock.Expect("glob1/demo.go").Return(errors.New("test error")).
			GetFilesInIndexMock.Expect().Return([]string{"glob1/demo.go"}, nil)

		rule := makeRule(
			&SuppressCommitFiles{
				BaseRule:        BaseRule{Type: SuppressCommitFilesType},
				Globs:           []string{"glob1/*.go", "*.css", "mocks"},
				RemoveFromIndex: true,
			},
			WithRepository(repo),
		)

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.EqualError(t, err, "test error")
	})
}


<|start_filename|>internal/handling/rules_helpers_internal_test.go<|end_filename|>
package handling

import (
	"fisherman/internal/configuration"
	"fisherman/internal/rules"
	"fisherman/testing/mocks"
	"testing"

	"github.com/stretchr/testify/assert"
)

// nolint: dupl
func Test_getPreScripts(t *testing.T) {
	rule1 := getRule(t, rules.PreScripts)
	rule2 := getRule(t, rules.PostScripts)
	rule3 := getRule(t, rules.PreScripts)
	rule4 := getRule(t, rules.Scripts)

	tests := []struct {
		name           string
		ruleCollection []Rule
		expected       []Rule
	}{
		{
			name:           "common filtering",
			ruleCollection: []configuration.Rule{rule1, rule2, rule3, rule4},
			expected:       []configuration.Rule{rule1, rule3},
		},
		{
			name:           "empty collection",
			ruleCollection: []configuration.Rule{},
			expected:       []configuration.Rule{},
		},
		{
			name:           "collection without target rules",
			ruleCollection: []configuration.Rule{rule2, rule4, rule2, rule4},
			expected:       []configuration.Rule{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := getPreScriptRules(tt.ruleCollection)

			assert.EqualValues(t, tt.expected, actual)
		})
	}
}

// nolint: dupl
func Test_getPostScriptRules(t *testing.T) {
	rule1 := getRule(t, rules.PostScripts)
	rule2 := getRule(t, rules.Scripts)
	rule3 := getRule(t, rules.PostScripts)
	rule4 := getRule(t, rules.PreScripts)

	tests := []struct {
		name           string
		ruleCollection []Rule
		expected       []Rule
	}{
		{
			name:           "common filtering",
			ruleCollection: []configuration.Rule{rule1, rule2, rule3, rule4},
			expected:       []configuration.Rule{rule1, rule3},
		},
		{
			name:           "empty collection",
			ruleCollection: []configuration.Rule{},
			expected:       []configuration.Rule{},
		},
		{
			name:           "collection without target rules",
			ruleCollection: []configuration.Rule{rule2, rule4, rule2, rule4},
			expected:       []configuration.Rule{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := getPostScriptRules(tt.ruleCollection)

			assert.EqualValues(t, tt.expected, actual)
		})
	}
}

// nolint: dupl
func Test_getScriptRules(t *testing.T) {
	rule1 := getRule(t, rules.Scripts)
	rule2 := getRule(t, rules.PostScripts)
	rule3 := getRule(t, rules.Scripts)
	rule4 := getRule(t, rules.PreScripts)

	tests := []struct {
		name           string
		ruleCollection []Rule
		expected       []Rule
	}{
		{
			name:           "common filtering",
			ruleCollection: []configuration.Rule{rule1, rule2, rule3, rule4},
			expected:       []configuration.Rule{rule1, rule3},
		},
		{
			name:           "empty collection",
			ruleCollection: []configuration.Rule{},
			expected:       []configuration.Rule{},
		},
		{
			name:           "collection without target rules",
			ruleCollection: []configuration.Rule{rule2, rule4, rule2, rule4},
			expected:       []configuration.Rule{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := getScriptRules(tt.ruleCollection)

			assert.EqualValues(t, tt.expected, actual)
		})
	}
}

func getRule(t *testing.T, ruleType byte) Rule {
	t.Helper()

	return mocks.NewRuleMock(t).
		GetTypeMock.Return(rules.ShellScriptType).
		GetPositionMock.Return(ruleType).
		CompileMock.Return()
}


<|start_filename|>internal/expression/env_test.go<|end_filename|>
package expression_test

import (
	"fisherman/internal/constants"
	. "fisherman/internal/expression"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestEnvVars_IsEmpty(t *testing.T) {
	tests := []struct {
		name     string
		vars     EnvVars
		value    string
		expected bool
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := tt.vars.IsEmpty(tt.value)

			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestEnvVars_IsWindows(t *testing.T) {
	tests := []struct {
		name     string
		vars     EnvVars
		value    string
		expected bool
	}{
		{
			name:     "where current os is windows",
			vars:     os(Windows),
			expected: true,
		},
		{
			name:     "where current os is linux",
			vars:     os(Linux),
			expected: false,
		},
		{
			name:     "where current os is macos",
			vars:     os(Macos),
			expected: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := tt.vars.IsWindows()

			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestEnvVars_IsLinux(t *testing.T) {
	tests := []struct {
		name     string
		vars     EnvVars
		expected bool
	}{
		{
			name:     "where current os is windows",
			vars:     os(Windows),
			expected: false,
		},
		{
			name:     "where current os is linux",
			vars:     os(Linux),
			expected: true,
		},
		{
			name:     "where current os is macos",
			vars:     os(Macos),
			expected: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := tt.vars.IsLinux()

			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestEnvVars_IsMacOs(t *testing.T) {
	tests := []struct {
		name     string
		vars     EnvVars
		expected bool
	}{
		{
			name:     "where current os is windows",
			vars:     os(Windows),
			expected: false,
		},
		{
			name:     "where current os is linux",
			vars:     os(Linux),
			expected: false,
		},
		{
			name:     "where current os is macos",
			vars:     os(Macos),
			expected: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := tt.vars.IsMacOs()

			assert.Equal(t, tt.expected, actual)
		})
	}
}

func os(os string) EnvVars {
	return EnvVars{constants.OsVariable: os}
}


<|start_filename|>internal/commands/remove/remove.go<|end_filename|>
package remove

import (
	"context"
	"fisherman/internal/constants"
	"fisherman/internal/utils"
	"fisherman/pkg/guards"
	"fisherman/pkg/log"
	"flag"
	"path/filepath"

	"github.com/go-git/go-billy/v5"
)

type Command struct {
	flagSet     *flag.FlagSet
	usage       string
	fs          billy.Filesystem
	cwd         string
	configFiles map[string]string
}

func NewCommand(options ...removeOption) *Command {
	command := &Command{
		flagSet:     flag.NewFlagSet("remove", flag.ExitOnError),
		usage:       "removes fisherman from git repository",
		configFiles: map[string]string{},
	}

	for _, option := range options {
		option(command)
	}

	guards.ShouldBeDefined(command.fs, "FileSystem should be configured")
	guards.ShouldBeNotEmpty(command.cwd, "Cwd should be configured")

	return command
}

func (c *Command) Run(ctx context.Context, args []string) error {
	err := c.flagSet.Parse(args)
	if err != nil {
		return err
	}

	filesToDelete := []string{}
	for _, config := range c.configFiles {
		filesToDelete = append(filesToDelete, config)
	}

	for _, hookName := range constants.HooksNames {
		path := filepath.Join(c.cwd, ".git", "hooks", hookName)
		exist, err := utils.Exists(c.fs, path)
		if err != nil {
			return err
		}

		if exist {
			filesToDelete = append(filesToDelete, path)
		}
	}

	for _, hookPath := range filesToDelete {
		err := c.fs.Remove(hookPath)
		if err != nil {
			return err
		}

		log.Infof("File '%s' was removed", hookPath)
	}

	return nil
}

func (c *Command) Name() string {
	return c.flagSet.Name()
}

func (c *Command) Description() string {
	return c.usage
}


<|start_filename|>internal/rules/add_to_index.go<|end_filename|>
package rules

import (
	"context"
	"fisherman/internal/utils"
	"io"

	"github.com/go-errors/errors"

	"github.com/go-git/go-git/v5"
)

const AddToIndexType = "add-to-index"

type Glob struct {
	Pattern    string `yaml:"glob"`
	IsRequired bool   `yaml:"required"`
}

type AddToIndex struct {
	BaseRule `yaml:",inline"`
	Globs    []Glob `yaml:"globs"`
}

func (rule *AddToIndex) GetPosition() byte {
	return PostScripts
}

func (rule *AddToIndex) Check(ctx context.Context, _ io.Writer) error {
	if len(rule.Globs) < 1 {
		return nil
	}

	repo := rule.BaseRule.repo
	for _, glob := range rule.Globs {
		if err := repo.AddGlob(glob.Pattern); err != nil {
			if errors.Is(err, git.ErrGlobNoMatches) {
				if !glob.IsRequired {
					continue
				}

				return rule.errorf("can't add files matching pattern %s", glob.Pattern)
			}

			return errors.Errorf("failed to add files matching pattern '%s' to the index: %w", glob.Pattern, err)
		}
	}

	return nil
}

func (rule *AddToIndex) Compile(variables map[string]interface{}) {
	rule.BaseRule.Compile(variables)
	for index := range rule.Globs {
		utils.FillTemplate(&rule.Globs[index].Pattern, variables)
	}
}


<|start_filename|>internal/handling/handler.go<|end_filename|>
package handling

import (
	"context"
	"fisherman/internal"
	"fisherman/internal/configuration"
	"fisherman/internal/expression"
	"fisherman/internal/rules"
	"fisherman/internal/utils"
	"io"

	"github.com/go-errors/errors"
	"github.com/go-git/go-billy/v5"
	"github.com/hashicorp/go-multierror"
)

var ErrNotPresented = errors.New("configuration for hook is not presented")

type CompilableConfig interface {
	Compile(engine expression.Engine, global map[string]interface{}) (map[string]interface{}, error)
}

type Handler interface {
	Handle(ctx context.Context) error
}

type HookHandler struct {
	engine       expression.Engine
	configs      *configuration.HooksConfig
	globalVars   map[string]interface{}
	cwd          string
	fs           billy.Filesystem
	repo         internal.Repository
	args         []string
	env          []string
	workersCount uint
	output       io.Writer

	rules           []configuration.Rule
	scripts         []configuration.Rule
	postScriptRules []configuration.Rule
}

func NewHookHandler(hook string, options ...handlerOptions) (*HookHandler, error) {
	h := &HookHandler{
		configs:         &configuration.HooksConfig{},
		globalVars:      map[string]interface{}{},
		args:            []string{},
		env:             []string{},
		workersCount:    1,
		rules:           []configuration.Rule{},
		scripts:         []configuration.Rule{},
		postScriptRules: []configuration.Rule{},
	}

	for _, option := range options {
		option(h)
	}

	config, err := getConfig(hook, h.configs)
	if err != nil {
		return nil, err
	}

	if config == nil {
		return nil, ErrNotPresented
	}

	var multiError *multierror.Error
	for _, rule := range config.Rules {
		if !utils.Contains(allowedHooks[hook], rule.GetType()) {
			multiError = multierror.Append(multiError, errors.Errorf("rule %s is not allowed", rule.GetType()))

			continue
		}

		rule.Configure(h.ruleOptions()...)
	}

	err = multiError.ErrorOrNil()
	if err != nil {
		return nil, errors.Errorf("%s hook: %v", hook, err)
	}

	h.rules = getPreScriptRules(config.Rules)
	h.scripts = getScriptRules(config.Rules)
	h.postScriptRules = getPostScriptRules(config.Rules)
	h.globalVars, err = config.Compile(h.globalVars)
	if err != nil {
		return nil, err
	}

	return h, nil
}

func (h *HookHandler) Handle(ctx context.Context) error {
	err := h.run(ctx, h.rules)
	if err != nil {
		return err
	}

	err = h.run(ctx, h.scripts)
	if err != nil {
		return err
	}

	return h.run(ctx, h.postScriptRules)
}

func (h *HookHandler) run(ctx context.Context, rules []configuration.Rule) error {
	filteredRules := []configuration.Rule{}
	for _, rule := range rules {
		shouldAdd := true

		condition := rule.GetContition()
		if !utils.IsEmpty(condition) {
			var err error
			shouldAdd, err = h.engine.Eval(condition, h.globalVars)
			if err != nil {
				return err
			}
		}

		if shouldAdd {
			filteredRules = append(filteredRules, rule)
		}
	}

	return h.runRules(ctx, filteredRules)
}

func (h *HookHandler) ruleOptions() []rules.RuleOption {
	return []rules.RuleOption{
		rules.WithCwd(h.cwd),
		rules.WithFileSystem(h.fs),
		rules.WithRepository(h.repo),
		rules.WithArgs(h.args),
		rules.WithEnv(h.env),
	}
}


<|start_filename|>internal/rules/base_rule.go<|end_filename|>
package rules

import (
	"fisherman/internal"
	"fisherman/internal/utils"
	"fisherman/internal/validation"

	"github.com/go-errors/errors"
	"github.com/go-git/go-billy/v5"
)

var (
	PreScripts  byte = 1
	Scripts     byte = 2
	PostScripts byte = 3
)

type RuleOption = func(rule *BaseRule)

type BaseRule struct {
	Type      string `yaml:"type,omitempty"`
	Condition string `yaml:"when,omitempty"`

	cwd  string
	fs   billy.Filesystem
	repo internal.Repository
	args []string
	env  []string
}

func (rule *BaseRule) GetType() string {
	return rule.Type
}

func (rule *BaseRule) GetPrefix() string {
	return rule.Type
}

func (rule *BaseRule) GetContition() string {
	return rule.Condition
}

func (rule *BaseRule) GetPosition() byte {
	return PreScripts
}

func (rule *BaseRule) Compile(variables map[string]interface{}) {
	utils.FillTemplate(&rule.Condition, variables)
}

func (rule *BaseRule) errorf(message string, a ...interface{}) error {
	return validation.Errorf(rule.GetPrefix(), message, a...)
}

func (rule *BaseRule) Configure(options ...RuleOption) {
	for _, option := range options {
		option(rule)
	}
}

func (rule *BaseRule) arg(index int) (string, error) {
	if index < 0 {
		return "", errors.New("incorrect argument index")
	}

	if rule.args == nil || len(rule.args) <= index {
		return "", errors.Errorf("argument at index %d is not provided", index)
	}

	return rule.args[index], nil
}


<|start_filename|>internal/handling/handler_test.go<|end_filename|>
package handling_test

import (
	"context"
	"fisherman/internal/configuration"
	"fisherman/internal/constants"
	"fisherman/internal/expression"
	. "fisherman/internal/handling"
	"fisherman/internal/rules"
	"fisherman/internal/validation"
	"fisherman/pkg/guards"
	"fisherman/testing/mocks"
	"fisherman/testing/testutils"
	"io"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestHookHandler_Handle(t *testing.T) {
	engine := expression.NewGoExpressionEngine()

	validRule := mocks.NewRuleMock(t).
		GetTypeMock.Return(rules.ExecType).
		ConfigureMock.Return().
		GetPositionMock.Return(rules.Scripts).
		CompileMock.Return().
		GetContitionMock.Return("").
		GetPrefixMock.Return("prefix-").
		CheckMock.Return(nil)

	testCases := []struct {
		name         string
		engine       expression.Engine
		rules        []*mocks.RuleMock
		workersCount uint
		expectedErr  string
	}{
		{
			name:   "rules checked successfully",
			engine: engine,
			rules: []*mocks.RuleMock{
				ruleMock(t, "TEST1", "", rules.ShellScriptType).
					ConfigureMock.Return().
					CheckMock.Return(nil),
				ruleMock(t, "TEST2", "1 == 1", rules.ExecType).
					ConfigureMock.Return().
					CheckMock.Return(nil),
				ruleMock(t, "TEST3", "1 == 3", rules.SuppressCommitFilesType).
					ConfigureMock.Return().
					CheckMock.Return(nil),
			},
			workersCount: 2,
		},
		{
			name: "expression engin returns error",
			engine: mocks.NewEngineMock(t).
				EvalMock.Return(false, validation.Errorf("test-rule", "test")),
			rules: []*mocks.RuleMock{
				ruleMock(t, "TEST1", "", rules.ShellScriptType).
					ConfigureMock.Return().
					CheckMock.Return(nil),
				ruleMock(t, "TEST", "1 == 1", rules.ExecType).
					ConfigureMock.Return().
					CheckMock.Return(nil),
				ruleMock(t, "TEST", "1 == 2", rules.SuppressCommitFilesType).
					ConfigureMock.Return().
					CheckMock.Return(nil),
			},
			workersCount: 2,
			expectedErr:  "[test-rule] test",
		},
		{
			name:   "rule returns error",
			engine: engine,
			rules: []*mocks.RuleMock{
				ruleMock(t, "TEST", "", rules.ShellScriptType).
					ConfigureMock.Return().
					CheckMock.Return(nil),
				ruleMock(t, "TEST", "1 == 1", rules.ExecType).
					ConfigureMock.Return().
					CheckMock.Return(validation.Errorf("test-rule", "test")),
				ruleMock(t, "TEST", "1 == 3", rules.SuppressCommitFilesType).
					ConfigureMock.Return().
					CheckMock.Return(nil),
			},
			workersCount: 2,
			expectedErr:  "1 error occurred:\n\t* [exec] [test-rule] test\n\n",
		},
		{
			name:   "configuration error",
			engine: engine,
			rules: []*mocks.RuleMock{
				ruleMock(t, "TEST", "", rules.ShellScriptType),
				ruleMock(t, "TEST", "", rules.ShellScriptType),
				ruleMock(t, "TEST", "", rules.ShellScriptType),
			},
			workersCount: 0,
			expectedErr:  "incorrect workers count",
		},
	}

	t.Run("runs rules", func(t *testing.T) {
		for _, tt := range testCases {
			t.Run(tt.name, func(t *testing.T) {
				handler, err := NewHookHandler(
					"pre-commit",
					WithExpressionEngine(tt.engine),
					WithHooksConfig(&configuration.HooksConfig{
						PreCommitHook: &configuration.HookConfig{
							Rules: applyPosition(tt.rules, rules.PreScripts),
						},
					}),
					WithWorkersCount(tt.workersCount),
				)

				guards.NoError(err)

				err = handler.Handle(context.TODO())

				testutils.AssertError(t, tt.expectedErr, err)
			})
		}
	})

	t.Run("runs scripts", func(t *testing.T) {
		for _, tt := range testCases {
			t.Run(tt.name, func(t *testing.T) {
				handler, err := NewHookHandler(
					"pre-commit",
					WithExpressionEngine(tt.engine),
					WithHooksConfig(&configuration.HooksConfig{
						PreCommitHook: &configuration.HookConfig{
							Rules: applyPosition(tt.rules, rules.Scripts),
						},
					}),
					WithWorkersCount(tt.workersCount),
				)

				guards.NoError(err)

				err = handler.Handle(context.TODO())

				testutils.AssertError(t, tt.expectedErr, err)
			})
		}
	})

	t.Run("runs post scripts", func(t *testing.T) {
		for _, tt := range testCases {
			t.Run(tt.name, func(t *testing.T) {
				handler, err := NewHookHandler(
					"pre-commit",
					WithExpressionEngine(tt.engine),
					WithHooksConfig(&configuration.HooksConfig{
						PreCommitHook: &configuration.HookConfig{
							Rules: applyPosition(tt.rules, rules.PostScripts),
						},
					}),
					WithWorkersCount(tt.workersCount),
				)

				guards.NoError(err)

				err = handler.Handle(context.TODO())

				testutils.AssertError(t, tt.expectedErr, err)
			})
		}
	})

	t.Run("run rules in correct orders", func(t *testing.T) {
		order := []string{}

		ruleDef := func(ruleType string, position byte) configuration.Rule {
			checkHandler := func() func(context.Context, io.Writer) error {
				return func(ec context.Context, w io.Writer) error {
					order = append(order, ruleType)

					return nil
				}
			}

			return mocks.NewRuleMock(t).
				GetTypeMock.Return(rules.ExecType).
				ConfigureMock.Return().
				CompileMock.Return().
				GetContitionMock.Return("").
				GetPrefixMock.Return("prefix-").
				CheckMock.Set(checkHandler()).
				GetPositionMock.Return(position)
		}

		handler, err := NewHookHandler(
			"pre-commit",
			WithExpressionEngine(mocks.NewEngineMock(t)),
			WithHooksConfig(&configuration.HooksConfig{
				PreCommitHook: &configuration.HookConfig{
					Rules: []configuration.Rule{
						ruleDef("rule", rules.PreScripts),
						ruleDef("script", rules.Scripts),
						ruleDef("post-script", rules.PostScripts),
					},
				},
			}),
		)

		guards.NoError(err)

		err = handler.Handle(context.TODO())

		assert.NoError(t, err)
		assert.Equal(t, []string{"rule", "script", "post-script"}, order)
	})

	t.Run("canceled context", func(t *testing.T) {
		handler, err := NewHookHandler(
			"pre-commit",
			WithExpressionEngine(mocks.NewEngineMock(t)),
			WithHooksConfig(&configuration.HooksConfig{
				PreCommitHook: &configuration.HookConfig{
					Rules: []configuration.Rule{
						validRule,
					},
				},
			}),
			WithWorkersCount(4),
			WithFileSystem(mocks.NewFilesystemMock(t)),
			WithRepository(mocks.NewRepositoryMock(t)),
		)

		guards.NoError(err)

		ctx, cancel := context.WithCancel(context.Background())

		cancel()

		err = handler.Handle(ctx)

		assert.EqualError(t, err, "1 error occurred:\n\t* context canceled\n\n")
	})

	t.Run("hook config is not presented", func(t *testing.T) {
		_, err := NewHookHandler(
			constants.PreCommitHook,
			WithHooksConfig(&configuration.HooksConfig{
				PreCommitHook: nil,
			}),
		)

		assert.Error(t, err, ErrNotPresented.Error())
	})

	t.Run("invalid hook name", func(t *testing.T) {
		_, err := NewHookHandler(
			"unknown-hook",
			WithHooksConfig(&configuration.HooksConfig{
				PreCommitHook: nil,
			}),
		)

		assert.Error(t, err, "'unknown-hook' is not valid hook name")
	})
}

func ruleMock(t *testing.T, prefix, condition, ruleType string) *mocks.RuleMock {
	t.Helper()

	return mocks.NewRuleMock(t).
		GetPrefixMock.Return(prefix).
		GetContitionMock.Return(condition).
		GetTypeMock.Return(ruleType).
		CompileMock.Return().
		ConfigureMock.Return()
}

func applyPosition(rules []*mocks.RuleMock, position byte) []configuration.Rule {
	updatedRules := []configuration.Rule{}
	for _, rule := range rules {
		rule.GetPositionMock.Return(position)
		updatedRules = append(updatedRules, rule)
	}

	return updatedRules
}


<|start_filename|>internal/app/fisherman_test.go<|end_filename|>
package app_test

import (
	"context"
	"fisherman/internal"
	. "fisherman/internal/app"
	"fisherman/internal/constants"
	"fisherman/pkg/log"
	"fisherman/pkg/vcs"
	"fisherman/testing/mocks"
	"fisherman/testing/testutils"
	"fmt"
	"io"
	"os"
	"strings"
	"testing"

	"github.com/go-errors/errors"

	"github.com/stretchr/testify/assert"
)

func TestRunner_Run(t *testing.T) {
	log.SetOutput(io.Discard)

	tests := []struct {
		name        string
		args        []string
		commands    []internal.CliCommand
		expectedErr string
	}{
		{
			name: "should run called commnad and return its error",
			args: []string{"init"},
			commands: []internal.CliCommand{
				makeCommand(t, "handle"),
				makeCommand(t, "remove"),
				makeExpectedCommand(t, "init", errors.New("expected error")),
			},
			expectedErr: "expected error",
		},
		{
			name: "should run called commnad and return nil when command executed witout error",
			args: []string{"init"},
			commands: []internal.CliCommand{
				makeCommand(t, "handle"),
				makeCommand(t, "remove"),
				makeExpectedCommand(t, "init", nil),
			},
		},
		{
			name: "should return error when command not found",
			args: []string{"not"},
			commands: []internal.CliCommand{
				makeCommand(t, "handle"),
				makeCommand(t, "remove"),
				makeCommand(t, "init"),
			},
			expectedErr: "unknown command: not",
		},
		{
			name:        "should return error when command not registered",
			args:        []string{"not"},
			commands:    []internal.CliCommand{},
			expectedErr: "unknown command: not",
		},
		{
			name: "should not return error when commnad not specified",
			args: []string{},
			commands: []internal.CliCommand{
				makeCommand(t, "handle"),
				makeCommand(t, "remove"),
				makeCommand(t, "init"),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			app := NewFishermanApp(
				WithCommands(tt.commands),
				WithFs(mocks.NewFilesystemMock(t)),
				WithRepository(mocks.NewRepositoryMock(t)),
				WithCwd("/"),
			)

			assert.NotPanics(t, func() {
				err := app.Run(context.TODO(), tt.args)
				testutils.AssertError(t, tt.expectedErr, err)
			})
		})
	}

	t.Run("default print", func(t *testing.T) {
		buffer := strings.Builder{}
		log.SetOutput(&buffer)
		defer log.SetOutput(io.Discard)

		expectedCommand1 := "init"
		expectedCommand2 := "version"
		expectedCommand3 := "handle"
		expectedCommand4 := "remove"

		app := NewFishermanApp(
			WithCommands([]internal.CliCommand{
				makeCommand(t, expectedCommand1),
				makeCommand(t, expectedCommand2),
				makeCommand(t, expectedCommand3),
				makeCommand(t, expectedCommand4),
			}),
			WithFs(mocks.NewFilesystemMock(t)),
			WithRepository(mocks.NewRepositoryMock(t)),
			WithCwd("/"),
		)

		err := app.Run(context.TODO(), []string{})

		output := buffer.String()

		assert.NoError(t, err)
		assert.Contains(t, output, constants.AppName)
		assert.Contains(t, output, constants.Version)
		assert.Contains(t, output, expectedCommand1)
		assert.Contains(t, output, expectedCommand2)
		assert.Contains(t, output, expectedCommand3)
		assert.Contains(t, output, expectedCommand4)
		assert.Contains(t, output, "Small git hook management tool for developer.")
	})
}

func TestRunner_Interrupt(t *testing.T) {
	chanel := make(chan os.Signal, 1)
	chanel <- os.Interrupt

	commandMock := mocks.NewCliCommandMock(t).
		NameMock.Return("test-command").
		RunMock.Set(func(ctx context.Context, _ []string) error {
		<-ctx.Done()

		return ctx.Err()
	})

	app := NewFishermanApp(
		WithCommands([]internal.CliCommand{commandMock}),
		WithOutput(io.Discard),
		WithFs(mocks.NewFilesystemMock(t)),
		WithRepository(
			mocks.NewRepositoryMock(t).
				GetLastTagMock.Return("tag1", nil).
				GetCurrentBranchMock.Return("master", nil).
				GetUserMock.Return(vcs.User{}, nil),
		),
		WithCwd("/"),
		WithOutput(io.Discard),
		WithInterruptChanel(chanel),
	)

	err := app.Run(context.Background(), []string{"test-command"})

	assert.EqualError(t, err, context.Canceled.Error())
}

func makeCommand(t *testing.T, name string) *mocks.CliCommandMock {
	t.Helper()

	return mocks.NewCliCommandMock(t).
		NameMock.Return(name).
		DescriptionMock.Return(fmt.Sprintf("This is %s command", name))
}

func makeExpectedCommand(t *testing.T, name string, err error) *mocks.CliCommandMock {
	t.Helper()

	return makeCommand(t, name).
		RunMock.Return(err)
}


<|start_filename|>testing/mocks/stubs.go<|end_filename|>
package mocks

import (
	"fisherman/internal/configuration"
)

var Cwd = "~/repository/project"
var Executable = "string"
var Configs = map[string]string{
	configuration.LocalMode:  "N/A",
	configuration.GlobalMode: "N/A",
	configuration.RepoMode:   "~/repository/project/.fisherman.yaml",
}
var HooksConfigStub = configuration.HooksConfig{}


<|start_filename|>internal/handling/handler_options.go<|end_filename|>
package handling

import (
	"fisherman/internal"
	"fisherman/internal/configuration"
	"fisherman/internal/expression"
	"io"

	"github.com/go-git/go-billy/v5"
)

type handlerOptions = func(*HookHandler)

func WithExpressionEngine(engine expression.Engine) handlerOptions {
	return func(h *HookHandler) {
		h.engine = engine
	}
}

func WithHooksConfig(configs *configuration.HooksConfig) handlerOptions {
	return func(h *HookHandler) {
		h.configs = configs
	}
}

func WithGlobalVars(globalVars map[string]interface{}) handlerOptions {
	return func(h *HookHandler) {
		h.globalVars = globalVars
	}
}

func WithCwd(cwd string) handlerOptions {
	return func(h *HookHandler) {
		h.cwd = cwd
	}
}

func WithFileSystem(fs billy.Filesystem) handlerOptions {
	return func(h *HookHandler) {
		h.fs = fs
	}
}

func WithRepository(repo internal.Repository) handlerOptions {
	return func(h *HookHandler) {
		h.repo = repo
	}
}

func WithArgs(args []string) handlerOptions {
	return func(h *HookHandler) {
		h.args = args
	}
}

func WithEnv(env []string) handlerOptions {
	return func(h *HookHandler) {
		h.env = env
	}
}

func WithWorkersCount(workersCount uint) handlerOptions {
	return func(h *HookHandler) {
		h.workersCount = workersCount
	}
}
func WithOutput(output io.Writer) handlerOptions {
	return func(h *HookHandler) {
		h.output = output
	}
}


<|start_filename|>internal/utils/strings_test.go<|end_filename|>
package utils_test

import (
	. "fisherman/internal/utils"
	"testing"

	"github.com/stretchr/testify/assert"
)

// nolint:dupl
func TestIsEmpty(t *testing.T) {
	tests := []struct {
		name     string
		value    string
		expected bool
	}{
		{name: "return true for empty string", value: "", expected: true},
		{name: "return true for spaces only", value: "  ", expected: true},
		{name: "return true for tabs only", value: "\t\t", expected: true},
		{name: "return true for newline charsets only", value: "\n\n", expected: true},
		{name: "return true for mixed witespace string", value: "\t\n ", expected: true},
		{name: "return false for string without space", value: "2", expected: false},
		{name: "return false for not empty string with space", value: " 2", expected: false},
		{name: "return false for not empty string with tab", value: "\t2", expected: false},
		{name: "return false for not empty string with newline", value: "\n2", expected: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, IsEmpty(tt.value))
		})
	}
}

func TestFirstNotEmpty(t *testing.T) {
	defaultValue := "default-value"

	tests := []struct {
		name     string
		values   []string
		expected string
	}{
		{name: "return default value for first empty string", values: []string{"", defaultValue}, expected: defaultValue},
		{name: "return default value for spaces only", values: []string{"  ", defaultValue}, expected: defaultValue},
		{name: "return default value for tabs only", values: []string{"\t\t", defaultValue}, expected: defaultValue},
		{name: "return default value for newline charsets only", values: []string{"\n\n", defaultValue}, expected: defaultValue},
		{name: "return default value for mixed witespace string", values: []string{"\t\n ", defaultValue}, expected: defaultValue},
		{name: "return original string for string without space", values: []string{"2", "2"}, expected: "2"},
		{name: "return original for not empty string with space", values: []string{" 2", " 2"}, expected: " 2"},
		{name: "return original for not empty string with tab", values: []string{"\t2", "\t2"}, expected: "\t2"},
		{name: "return original for not empty string with newline", values: []string{"\n2", "\n2"}, expected: "\n2"},
		{name: "return last empty value", values: []string{"\n", "\t", " ", "\t \n"}, expected: "\t \n"},
		{name: "return first element for single value", values: []string{"\n"}, expected: "\n"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := FirstNotEmpty(tt.values...)

			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestFirstNotEmptyPanic(t *testing.T) {
	assert.PanicsWithError(t, "sequence contains no elements", func() {
		FirstNotEmpty()
	})
}


<|start_filename|>internal/commands/initialize/initialize_test.go<|end_filename|>
package initialize_test

import (
	"context"
	. "fisherman/internal/commands/initialize"
	"fisherman/internal/constants"
	"fisherman/pkg/log"
	"fisherman/testing/mocks"
	"fisherman/testing/testutils"
	"io"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

// nolint: gochecknoinits
func init() {
	log.SetOutput(io.Discard)
}

func TestNewCommand(t *testing.T) {
	command := NewCommand(
		WithFilesystem(mocks.NewFilesystemMock(t)),
		WithCwd(mocks.Cwd),
		WithExecutable(mocks.Executable),
		WithUser(&testutils.TestUser),
	)

	assert.NotNil(t, command)
}

func TestCommand_Run(t *testing.T) {
	t.Run("runs with force mode", func(t *testing.T) {
		cwd := "/demo/"

		fs := testutils.FsFromMap(t, map[string]string{
			filepath.Join(cwd, constants.AppConfigNames[0]): "content",
		})

		command := NewCommand(
			WithFilesystem(fs),
			WithCwd(cwd),
			WithExecutable(mocks.Executable),
			WithUser(&testutils.TestUser),
		)

		err := command.Run(context.TODO(), []string{"--force"})
		assert.NoError(t, err)
	})
}

func TestCommand_Name(t *testing.T) {
	command := NewCommand(
		WithFilesystem(mocks.NewFilesystemMock(t)),
		WithCwd(mocks.Cwd),
		WithExecutable(mocks.Executable),
		WithUser(&testutils.TestUser),
	)

	assert.Equal(t, command.Name(), "init")
}

func TestCommand_Description(t *testing.T) {
	command := NewCommand(
		WithFilesystem(mocks.NewFilesystemMock(t)),
		WithCwd(mocks.Cwd),
		WithExecutable(mocks.Executable),
		WithUser(&testutils.TestUser),
	)

	assert.NotEmpty(t, command.Description())
}


<|start_filename|>pkg/guards/guards.go<|end_filename|>
package guards

import (
	"reflect"
	"strings"

	"github.com/go-errors/errors"
)

// ShouldBeDefined checks that the variable is not nil. In case when a variable is nill, it panic.
func ShouldBeDefined(object interface{}, message string) {
	if object == nil || (reflect.ValueOf(object).Kind() == reflect.Ptr && reflect.ValueOf(object).IsNil()) {
		panic(errors.New(message))
	}
}

// NoError panic when passed error is not nil.
func NoError(err error) {
	if err != nil {
		panic(err)
	}
}

// ShouldBeNotEmpty checks that the string is not empty and should contins not witespace symbols.
func ShouldBeNotEmpty(object string, message string) {
	if len(strings.TrimSpace(object)) == 0 {
		panic(errors.New(message))
	}
}


<|start_filename|>internal/expression/expression_test.go<|end_filename|>
package expression_test

import (
	"errors"
	. "fisherman/internal/expression"
	"fisherman/testing/testutils"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestExpressionEngine_Eval(t *testing.T) {
	engine := NewGoExpressionEngine()

	tests := []struct {
		name        string
		expression  string
		expected    bool
		expectedErr string
	}{
		{
			name:       "arithmetic expression",
			expression: "1 == 1",
			expected:   true,
		},
		{
			name:       "IsEmpty with empty value",
			expression: "IsEmpty(EmptyValue)",
			expected:   true,
		},
		{
			name:       "IsEmpty with not empty value",
			expression: "IsEmpty(X)",
			expected:   false,
		},
		{
			name:        "IsEmpty with not empty value",
			expression:  "IsEmpty()",
			expected:    false,
			expectedErr: "expected bool, but got interface {}",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual, err := engine.Eval(tt.expression, map[string]interface{}{
				"X":          "this is x value",
				"EmptyValue": "",
			})

			assert.Equal(t, tt.expected, actual)
			testutils.AssertError(t, tt.expectedErr, err)
		})
	}

	t.Run("panic in function", func(t *testing.T) {
		_, err := engine.Eval("test()", map[string]interface{}{
			"test": func() bool { panic(errors.New("test error")) },
		})

		assert.EqualError(t, err, "test error (1:1)\n | test()\n | ^")
	})
}


<|start_filename|>internal/expression/constants.go<|end_filename|>
package expression

const Windows = "windows"
const Linux = "linux"
const Macos = "darwin"


<|start_filename|>internal/commands/remove/options.go<|end_filename|>
package remove

import (
	"github.com/go-git/go-billy/v5"
)

type removeOption = func(c *Command)

func WithCwd(cwd string) removeOption {
	return func(c *Command) {
		c.cwd = cwd
	}
}

func WithFileSystem(fs billy.Filesystem) removeOption {
	return func(c *Command) {
		c.fs = fs
	}
}

func WithConfigFiles(configs map[string]string) removeOption {
	return func(c *Command) {
		c.configFiles = configs
	}
}


<|start_filename|>pkg/shell/bash_strategy_test.go<|end_filename|>
// nolint: dupl

package shell_test

import (
	"context"
	. "fisherman/pkg/shell"
	"fisherman/testing/testutils"
	"runtime"
	"testing"

	"github.com/stretchr/testify/assert"
	"golang.org/x/text/encoding"
)

func TestBashStrategy(t *testing.T) {
	if runtime.GOOS == windowsOS {
		t.Skip("test can not be runned on windows")
	}

	for _, tt := range scriptTests {
		t.Run(tt.name, func(t *testing.T) {
			host := NewHost(context.TODO(), Bash())

			actual := host.Run(tt.script)

			testutils.AssertError(t, tt.expectedRrr, actual)
		})
	}
}

func TestBashStrategy_GetName(t *testing.T) {
	actual := Bash().GetName()

	assert.Equal(t, "bash", actual)
}

func TestBashStrategy_ArgsWrapper(t *testing.T) {
	tests := []struct {
		name     string
		args     []string
		expected []string
	}{
		{
			name:     "empty slice",
			args:     []string{},
			expected: []string{},
		},
		{
			name:     "additional arguments",
			args:     []string{"arg1", "arg2"},
			expected: []string{"arg1", "arg2"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := Bash().ArgsWrapper(tt.args)

			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestBashStrategy_EnvWrapper(t *testing.T) {
	for _, tt := range envTests {
		t.Run(tt.name, func(t *testing.T) {
			actual := Bash().EnvWrapper(tt.env)

			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestBashStrategy_GetEncoding(t *testing.T) {
	actual := Bash().GetEncoding()

	assert.EqualValues(t, encoding.Nop, actual)
}


<|start_filename|>pkg/shell/options.go<|end_filename|>
package shell

import (
	"io"

	"golang.org/x/text/encoding"
)

type hostOption = func(str ShellStrategy, host *Host)

// WithStdout setups Stdout writer for shell host.
func WithStdout(output io.Writer) hostOption {
	return func(strategy ShellStrategy, host *Host) {
		host.command.Stdout = output
	}
}

// WithStderr setups Stderr writer for shell host.
func WithStderr(output io.Writer) hostOption {
	return func(strategy ShellStrategy, host *Host) {
		host.command.Stderr = output
	}
}

// WithEnv setups environment variables (ShellStrategy defined variables will be included) for shell host.
func WithEnv(env []string) hostOption {
	return func(strategy ShellStrategy, host *Host) {
		host.command.Env = strategy.EnvWrapper(env)
	}
}

// WithRawEnv setups environment variables (ShellStrategy defined variables will not be included) for shell host.
func WithRawEnv(env []string) hostOption {
	return func(strategy ShellStrategy, host *Host) {
		host.command.Env = env
	}
}

// WithArgs setups arguments (ShellStrategy defined arguments will be included) for shell host.
func WithArgs(args []string) hostOption {
	return func(strategy ShellStrategy, host *Host) {
		host.command.Args = strategy.ArgsWrapper(args)
	}
}

// WithRawArgs setups arguments (ShellStrategy defined arguments will not be included) for shell host.
func WithRawArgs(args []string) hostOption {
	return func(strategy ShellStrategy, host *Host) {
		host.command.Args = args
	}
}

// WithCwd setups environment variables for shell host.
func WithCwd(dir string) hostOption {
	return func(strategy ShellStrategy, host *Host) {
		host.command.Dir = dir
	}
}

// WithEncoding setups shell input/output encoding.
func WithEncoding(encoding encoding.Encoding) hostOption {
	return func(strategy ShellStrategy, host *Host) {
		if encoding != nil {
			host.encoding = encoding
		}
	}
}


<|start_filename|>internal/configuration/loader.go<|end_filename|>
package configuration

import (
	"fisherman/internal/constants"
	"fisherman/internal/utils"
	"fisherman/pkg/log"
	"fisherman/pkg/shell" // TODO: remove this dependency
	"os/user"
	"path/filepath"

	"github.com/go-errors/errors"
	"github.com/go-git/go-billy/v5"
	"github.com/imdario/mergo"
	"gopkg.in/yaml.v3"
)

const gitDir = ".git"

func FindConfigFiles(usr *user.User, cwd string, fs billy.Filesystem) (map[string]string, error) {
	configs := map[string]string{}
	for _, mode := range ModeOptions {
		folder, err := GetConfigFolder(usr, cwd, mode)
		if err != nil {
			return nil, err
		}

		files := []string{}
		for _, name := range constants.AppConfigNames {
			configPath := filepath.Join(folder, name)
			exist, err := utils.Exists(fs, configPath)
			if err != nil {
				return nil, err
			}

			if exist {
				files = append(files, configPath)
			}
		}

		if len(files) > 1 {
			return nil, errors.Errorf("more then one config file specifies in folder '%s'", folder)
		}

		if len(files) == 1 {
			configs[mode] = files[0]
		}
	}

	return configs, nil
}

func GetConfigFolder(usr *user.User, cwd, mode string) (string, error) {
	switch mode {
	case LocalMode:
		return filepath.Join(cwd, gitDir), nil
	case RepoMode:
		return cwd, nil
	case GlobalMode:
		return usr.HomeDir, nil
	default:
		return "", errors.New("unknown config mode")
	}
}

func Load(fs billy.Filesystem, files map[string]string) (*FishermanConfig, error) {
	config := FishermanConfig{
		Output:       log.DefaultOutputConfig,
		DefaultShell: shell.Default().GetName(),
	}

	for _, mode := range ModeOptions {
		file, ok := files[mode]
		if ok {
			loadedConfig, err := unmarshlFile(fs, file)
			if err != nil {
				return &config, err
			}

			err = mergo.MergeWithOverwrite(&config, loadedConfig)
			if err != nil {
				return &config, err
			}
		}
	}

	// TODO: incorrect log level marging
	return &config, nil
}

func unmarshlFile(fs billy.Filesystem, path string) (*FishermanConfig, error) {
	var config FishermanConfig

	file, err := fs.Open(path)
	if err != nil {
		return nil, errors.Errorf("open %s: %w", path, err)
	}
	defer file.Close()

	decoder := yaml.NewDecoder(file)
	decoder.KnownFields(true)
	err = decoder.Decode(&config)
	if err != nil {
		return nil, err
	}

	return &config, nil
}


<|start_filename|>internal/rules/helpers.go<|end_filename|>
package rules

import (
	"fisherman/internal/utils"
	"fmt"
	"strings"

	"golang.org/x/text/encoding"
	"golang.org/x/text/encoding/ianaindex"
)

func plainErrorFormatter(e []error) string {
	parts := []string{}
	for _, err := range e {
		parts = append(parts, err.Error())
	}

	return strings.Join(parts, "\n")
}

func getEncoding(encodingName string) (encoding.Encoding, error) {
	if utils.IsEmpty(encodingName) {
		return encoding.Nop, nil
	}

	if enc, err := ianaindex.IANA.Encoding(encodingName); err == nil {
		return enc, nil
	}

	return nil, fmt.Errorf("'%s' is unknown encoding", encodingName)
}


<|start_filename|>cmd/fisherman/main.go<|end_filename|>
package main

import (
	"context"
	"fisherman/internal"
	"fisherman/internal/app"
	"fisherman/internal/commands/handle"
	"fisherman/internal/commands/initialize"
	"fisherman/internal/commands/remove"
	"fisherman/internal/commands/version"
	"fisherman/internal/configuration"
	"fisherman/internal/expression"
	"fisherman/internal/utils"
	"fisherman/pkg/guards"
	"fisherman/pkg/log"
	"fisherman/pkg/vcs"
	"os"
	"os/user"
	"runtime"

	"github.com/go-git/go-billy/v5/osfs"
)

const fatalExitCode = 1

func main() {
	defer utils.PanicInterceptor(func(recovered interface{}) {
		log.Errorf("Fatal error: %s", recovered)
		if err, ok := recovered.(error); ok {
			log.DumpError(err)
		}

		os.Exit(fatalExitCode)
	})

	usr, err := user.Current()
	guards.NoError(err)

	cwd, err := os.Getwd()
	guards.NoError(err)

	executablePath, err := os.Executable()
	guards.NoError(err)

	fs := osfs.New("")

	configs, err := configuration.FindConfigFiles(usr, cwd, fs)
	guards.NoError(err)

	config, err := configuration.Load(fs, configs)
	guards.NoError(err)

	log.Configure(config.Output)

	ctx := context.Background()
	engine := expression.NewGoExpressionEngine()
	repo := vcs.NewRepository(vcs.WithFsRepo(cwd))

	fishermanApp := app.NewFishermanApp(
		app.WithCommands([]internal.CliCommand{
			initialize.NewCommand(
				initialize.WithCwd(cwd),
				initialize.WithFilesystem(fs),
				initialize.WithUser(usr),
				initialize.WithExecutable(executablePath),
			),
			handle.NewCommand(
				handle.WithExpressionEngine(engine),
				handle.WithHooksConfig(&config.Hooks),
				handle.WithGlobalVars(config.GlobalVariables),
				handle.WithCwd(cwd),
				handle.WithFileSystem(fs),
				handle.WithRepository(repo),
				handle.WithEnv(os.Environ()),
				handle.WithWorkersCount(uint(runtime.NumCPU())),
				handle.WithConfigFiles(configs),
				handle.WithOutput(os.Stdout),
			),
			remove.NewCommand(
				remove.WithCwd(cwd),
				remove.WithFileSystem(fs),
				remove.WithConfigFiles(configs),
			),
			version.NewCommand(),
		}),
		app.WithFs(fs),
		app.WithOutput(os.Stdout),
		app.WithRepository(repo),
		app.WithEnv(os.Environ()),
		app.WithSistemInterruptSignals(),
	)

	if err = fishermanApp.Run(ctx, os.Args[1:]); err != nil {
		panic(err)
	}
}


<|start_filename|>internal/commands/version/version.go<|end_filename|>
package version

import (
	"context"
	"fisherman/internal/constants"
	"fisherman/pkg/log"
	"flag"
	"fmt"
)

type Command struct {
	flagSet *flag.FlagSet
	usage   string
}

func NewCommand() *Command {
	return &Command{
		flagSet: flag.NewFlagSet("version", flag.ExitOnError),
		usage:   "prints fisherman version",
	}
}

func (c *Command) Run(ctx context.Context, _ []string) error {
	_, err := fmt.Fprintf(log.Stdout(), "%s@%s", constants.AppName, constants.Version)

	return err
}

func (c *Command) Name() string {
	return c.flagSet.Name()
}

func (c *Command) Description() string {
	return c.usage
}


<|start_filename|>pkg/shell/powershell_strategy.go<|end_filename|>
package shell

import (
	"context"
	"os/exec"

	"golang.org/x/text/encoding"
)

const powershellBin = "powershell"

type PowershellStrategy struct{}

func PowerShell() *PowershellStrategy {
	return &PowershellStrategy{}
}

func (s *PowershellStrategy) GetName() string {
	return powershellBin
}

func (s *PowershellStrategy) GetCommand(ctx context.Context) *exec.Cmd {
	args := s.ArgsWrapper([]string{})

	return exec.CommandContext(ctx, powershellBin, args...)
}

func (s *PowershellStrategy) ArgsWrapper(args []string) []string {
	return append([]string{"-NoProfile", "-NonInteractive", "-NoLogo"}, args...)
}

func (s *PowershellStrategy) EnvWrapper(env []string) []string {
	return env
}

func (s *PowershellStrategy) GetEncoding() encoding.Encoding {
	return windowsEncoding()
}


<|start_filename|>internal/rules/prepare_message.go<|end_filename|>
package rules

import (
	"context"
	"fisherman/internal/utils"
	"io"
	"io/fs"

	"github.com/go-git/go-billy/v5/util"
)

const PrepareMessageType = "prepare-message"

type PrepareMessage struct {
	BaseRule `yaml:",inline"`
	Message  string `yaml:"message"`
}

func (rule PrepareMessage) Check(ctx context.Context, _ io.Writer) error {
	if utils.IsEmpty(rule.Message) {
		return nil
	}

	arg, err := rule.arg(0)
	if err != nil {
		return err
	}

	return util.WriteFile(rule.BaseRule.fs, arg, []byte(rule.Message), fs.ModePerm)
}

func (rule *PrepareMessage) Compile(variables map[string]interface{}) {
	rule.BaseRule.Compile(variables)
	utils.FillTemplate(&rule.Message, variables)
}


<|start_filename|>pkg/shell/encoding_windows.go<|end_filename|>
//go:build windows
// +build windows

package shell

import (
	"syscall"

	"golang.org/x/text/encoding"
	"golang.org/x/text/encoding/ianaindex"
)

var modkernel32 = syscall.NewLazyDLL("kernel32.dll")
var procGetConsoleCP = modkernel32.NewProc("GetConsoleCP")

var mapping = map[uint32]string{
	037: "IBM037",
	437: "IBM437",
	500: "IBM500",
	708: "ASMO-708",
	// 709:   "",
	// 710:   "",
	720:   "DOS-720",
	737:   "ibm737",
	775:   "ibm775",
	850:   "ibm850",
	852:   "ibm852",
	855:   "IBM855",
	857:   "ibm857",
	858:   "IBM00858",
	860:   "IBM860",
	861:   "ibm861",
	862:   "DOS-862",
	863:   "IBM863",
	864:   "IBM864",
	865:   "IBM865",
	866:   "cp866",
	869:   "ibm869",
	870:   "IBM870",
	874:   "windows-874",
	875:   "cp875",
	932:   "shift_jis",
	936:   "gb2312",
	949:   "ks_c_5601-1987",
	950:   "big5",
	1026:  "IBM1026",
	1047:  "IBM01047",
	1140:  "IBM01140",
	1141:  "IBM01141",
	1142:  "IBM01142",
	1143:  "IBM01143",
	1144:  "IBM01144",
	1145:  "IBM01145",
	1146:  "IBM01146",
	1147:  "IBM01147",
	1148:  "IBM01148",
	1149:  "IBM01149",
	1200:  "utf-16",
	1201:  "unicodeFFFE",
	1250:  "windows-1250",
	1251:  "windows-1251",
	1252:  "windows-1252",
	1253:  "windows-1253",
	1254:  "windows-1254",
	1255:  "windows-1255",
	1256:  "windows-1256",
	1257:  "windows-1257",
	1258:  "windows-1258",
	1361:  "Johab",
	10000: "macintosh",
	10001: "x-mac-japanese",
	10002: "x-mac-chinesetrad",
	10003: "x-mac-korean",
	10004: "x-mac-arabic",
	10005: "x-mac-hebrew",
	10006: "x-mac-greek",
	10007: "x-mac-cyrillic",
	10008: "x-mac-chinesesimp",
	10010: "x-mac-romanian",
	10017: "x-mac-ukrainian",
	10021: "x-mac-thai",
	10029: "x-mac-ce",
	10079: "x-mac-icelandic",
	10081: "x-mac-turkish",
	10082: "x-mac-croatian",
	12000: "utf-32",
	12001: "utf-32BE",
	20000: "x-Chinese_CNS",
	20001: "x-cp20001",
	20002: "x_Chinese-Eten",
	20003: "x-cp20003",
	20004: "x-cp20004",
	20005: "x-cp20005",
	20105: "x-IA5",
	20106: "x-IA5-German",
	20107: "x-IA5-Swedish",
	20108: "x-IA5-Norwegian",
	20127: "us-ascii",
	20261: "x-cp20261",
	20269: "x-cp20269",
	20273: "IBM273",
	20277: "IBM277",
	20278: "IBM278",
	20280: "IBM280",
	20284: "IBM284",
	20285: "IBM285",
	20290: "IBM290",
	20297: "IBM297",
	20420: "IBM420",
	20423: "IBM423",
	20424: "IBM424",
	20833: "x-EBCDIC-KoreanExtended",
	20838: "IBM-Thai",
	20866: "koi8-r",
	20871: "IBM871",
	20880: "IBM880",
	20905: "IBM905",
	20924: "IBM00924",
	20932: "EUC-JP",
	20936: "x-cp20936",
	20949: "x-cp20949",
	21025: "cp1025",
	// 21027: "",
	21866: "koi8-u",
	28591: "iso-8859-1",
	28592: "iso-8859-2",
	28593: "iso-8859-3",
	28594: "iso-8859-4",
	28595: "iso-8859-5",
	28596: "iso-8859-6",
	28597: "iso-8859-7",
	28598: "iso-8859-8",
	28599: "iso-8859-9",
	28603: "iso-8859-13",
	28605: "iso-8859-15",
	29001: "x-Europa",
	38598: "iso-8859-8-i",
	50220: "iso-2022-jp",
	50221: "csISO2022JP",
	50222: "iso-2022-jp",
	50225: "iso-2022-kr",
	50227: "x-cp50227",
	// 50229: "",
	// 50930: "",
	// 50931: "",
	// 50933: "",
	// 50935: "",
	// 50936: "",
	// 50937: "",
	// 50939: "",
	51932: "euc-jp",
	51936: "EUC-CN",
	51949: "euc-kr",
	51950: "",
	52936: "hz-gb-2312",
	54936: "GB18030",
	57002: "x-iscii-de",
	57003: "x-iscii-be",
	57004: "x-iscii-ta",
	57005: "x-iscii-te",
	57006: "x-iscii-as",
	57007: "x-iscii-or",
	57008: "x-iscii-ka",
	57009: "x-iscii-ma",
	57010: "x-iscii-gu",
	57011: "x-iscii-pa",
	65000: "utf-7",
	65001: "utf-8",
}

func windowsEncoding() encoding.Encoding {
	cpID, _, _ := syscall.Syscall(procGetConsoleCP.Addr(), 0, 0, 0, 0)
	if encodingName, ok := mapping[uint32(cpID)]; ok {
		if enc, err := ianaindex.IANA.Encoding(encodingName); err == nil {
			return enc
		}
	}

	// TODO: add warning about details to handle encoding
	return encoding.Nop
}


<|start_filename|>testing/mocks/encoding_mock.go<|end_filename|>
package mocks

// Code generated by http://github.com/gojuno/minimock (dev). DO NOT EDIT.

//go:generate minimock -i golang.org/x/text/encoding.Encoding -o ./testing/mocks/encoding_mock.go -n EncodingMock

import (
	mm_atomic "sync/atomic"
	mm_time "time"

	"github.com/gojuno/minimock/v3"
	mm_encoding "golang.org/x/text/encoding"
)

// EncodingMock implements encoding.Encoding
type EncodingMock struct {
	t minimock.Tester

	funcNewDecoder          func() (dp1 *mm_encoding.Decoder)
	inspectFuncNewDecoder   func()
	afterNewDecoderCounter  uint64
	beforeNewDecoderCounter uint64
	NewDecoderMock          mEncodingMockNewDecoder

	funcNewEncoder          func() (ep1 *mm_encoding.Encoder)
	inspectFuncNewEncoder   func()
	afterNewEncoderCounter  uint64
	beforeNewEncoderCounter uint64
	NewEncoderMock          mEncodingMockNewEncoder
}

// NewEncodingMock returns a mock for encoding.Encoding
func NewEncodingMock(t minimock.Tester) *EncodingMock {
	m := &EncodingMock{t: t}
	if controller, ok := t.(minimock.MockController); ok {
		controller.RegisterMocker(m)
	}

	m.NewDecoderMock = mEncodingMockNewDecoder{mock: m}

	m.NewEncoderMock = mEncodingMockNewEncoder{mock: m}

	return m
}

type mEncodingMockNewDecoder struct {
	mock               *EncodingMock
	defaultExpectation *EncodingMockNewDecoderExpectation
	expectations       []*EncodingMockNewDecoderExpectation
}

// EncodingMockNewDecoderExpectation specifies expectation struct of the Encoding.NewDecoder
type EncodingMockNewDecoderExpectation struct {
	mock *EncodingMock

	results *EncodingMockNewDecoderResults
	Counter uint64
}

// EncodingMockNewDecoderResults contains results of the Encoding.NewDecoder
type EncodingMockNewDecoderResults struct {
	dp1 *mm_encoding.Decoder
}

// Expect sets up expected params for Encoding.NewDecoder
func (mmNewDecoder *mEncodingMockNewDecoder) Expect() *mEncodingMockNewDecoder {
	if mmNewDecoder.mock.funcNewDecoder != nil {
		mmNewDecoder.mock.t.Fatalf("EncodingMock.NewDecoder mock is already set by Set")
	}

	if mmNewDecoder.defaultExpectation == nil {
		mmNewDecoder.defaultExpectation = &EncodingMockNewDecoderExpectation{}
	}

	return mmNewDecoder
}

// Inspect accepts an inspector function that has same arguments as the Encoding.NewDecoder
func (mmNewDecoder *mEncodingMockNewDecoder) Inspect(f func()) *mEncodingMockNewDecoder {
	if mmNewDecoder.mock.inspectFuncNewDecoder != nil {
		mmNewDecoder.mock.t.Fatalf("Inspect function is already set for EncodingMock.NewDecoder")
	}

	mmNewDecoder.mock.inspectFuncNewDecoder = f

	return mmNewDecoder
}

// Return sets up results that will be returned by Encoding.NewDecoder
func (mmNewDecoder *mEncodingMockNewDecoder) Return(dp1 *mm_encoding.Decoder) *EncodingMock {
	if mmNewDecoder.mock.funcNewDecoder != nil {
		mmNewDecoder.mock.t.Fatalf("EncodingMock.NewDecoder mock is already set by Set")
	}

	if mmNewDecoder.defaultExpectation == nil {
		mmNewDecoder.defaultExpectation = &EncodingMockNewDecoderExpectation{mock: mmNewDecoder.mock}
	}
	mmNewDecoder.defaultExpectation.results = &EncodingMockNewDecoderResults{dp1}
	return mmNewDecoder.mock
}

//Set uses given function f to mock the Encoding.NewDecoder method
func (mmNewDecoder *mEncodingMockNewDecoder) Set(f func() (dp1 *mm_encoding.Decoder)) *EncodingMock {
	if mmNewDecoder.defaultExpectation != nil {
		mmNewDecoder.mock.t.Fatalf("Default expectation is already set for the Encoding.NewDecoder method")
	}

	if len(mmNewDecoder.expectations) > 0 {
		mmNewDecoder.mock.t.Fatalf("Some expectations are already set for the Encoding.NewDecoder method")
	}

	mmNewDecoder.mock.funcNewDecoder = f
	return mmNewDecoder.mock
}

// NewDecoder implements encoding.Encoding
func (mmNewDecoder *EncodingMock) NewDecoder() (dp1 *mm_encoding.Decoder) {
	mm_atomic.AddUint64(&mmNewDecoder.beforeNewDecoderCounter, 1)
	defer mm_atomic.AddUint64(&mmNewDecoder.afterNewDecoderCounter, 1)

	if mmNewDecoder.inspectFuncNewDecoder != nil {
		mmNewDecoder.inspectFuncNewDecoder()
	}

	if mmNewDecoder.NewDecoderMock.defaultExpectation != nil {
		mm_atomic.AddUint64(&mmNewDecoder.NewDecoderMock.defaultExpectation.Counter, 1)

		mm_results := mmNewDecoder.NewDecoderMock.defaultExpectation.results
		if mm_results == nil {
			mmNewDecoder.t.Fatal("No results are set for the EncodingMock.NewDecoder")
		}
		return (*mm_results).dp1
	}
	if mmNewDecoder.funcNewDecoder != nil {
		return mmNewDecoder.funcNewDecoder()
	}
	mmNewDecoder.t.Fatalf("Unexpected call to EncodingMock.NewDecoder.")
	return
}

// NewDecoderAfterCounter returns a count of finished EncodingMock.NewDecoder invocations
func (mmNewDecoder *EncodingMock) NewDecoderAfterCounter() uint64 {
	return mm_atomic.LoadUint64(&mmNewDecoder.afterNewDecoderCounter)
}

// NewDecoderBeforeCounter returns a count of EncodingMock.NewDecoder invocations
func (mmNewDecoder *EncodingMock) NewDecoderBeforeCounter() uint64 {
	return mm_atomic.LoadUint64(&mmNewDecoder.beforeNewDecoderCounter)
}

// MinimockNewDecoderDone returns true if the count of the NewDecoder invocations corresponds
// the number of defined expectations
func (m *EncodingMock) MinimockNewDecoderDone() bool {
	for _, e := range m.NewDecoderMock.expectations {
		if mm_atomic.LoadUint64(&e.Counter) < 1 {
			return false
		}
	}

	// if default expectation was set then invocations count should be greater than zero
	if m.NewDecoderMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterNewDecoderCounter) < 1 {
		return false
	}
	// if func was set then invocations count should be greater than zero
	if m.funcNewDecoder != nil && mm_atomic.LoadUint64(&m.afterNewDecoderCounter) < 1 {
		return false
	}
	return true
}

// MinimockNewDecoderInspect logs each unmet expectation
func (m *EncodingMock) MinimockNewDecoderInspect() {
	for _, e := range m.NewDecoderMock.expectations {
		if mm_atomic.LoadUint64(&e.Counter) < 1 {
			m.t.Error("Expected call to EncodingMock.NewDecoder")
		}
	}

	// if default expectation was set then invocations count should be greater than zero
	if m.NewDecoderMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterNewDecoderCounter) < 1 {
		m.t.Error("Expected call to EncodingMock.NewDecoder")
	}
	// if func was set then invocations count should be greater than zero
	if m.funcNewDecoder != nil && mm_atomic.LoadUint64(&m.afterNewDecoderCounter) < 1 {
		m.t.Error("Expected call to EncodingMock.NewDecoder")
	}
}

type mEncodingMockNewEncoder struct {
	mock               *EncodingMock
	defaultExpectation *EncodingMockNewEncoderExpectation
	expectations       []*EncodingMockNewEncoderExpectation
}

// EncodingMockNewEncoderExpectation specifies expectation struct of the Encoding.NewEncoder
type EncodingMockNewEncoderExpectation struct {
	mock *EncodingMock

	results *EncodingMockNewEncoderResults
	Counter uint64
}

// EncodingMockNewEncoderResults contains results of the Encoding.NewEncoder
type EncodingMockNewEncoderResults struct {
	ep1 *mm_encoding.Encoder
}

// Expect sets up expected params for Encoding.NewEncoder
func (mmNewEncoder *mEncodingMockNewEncoder) Expect() *mEncodingMockNewEncoder {
	if mmNewEncoder.mock.funcNewEncoder != nil {
		mmNewEncoder.mock.t.Fatalf("EncodingMock.NewEncoder mock is already set by Set")
	}

	if mmNewEncoder.defaultExpectation == nil {
		mmNewEncoder.defaultExpectation = &EncodingMockNewEncoderExpectation{}
	}

	return mmNewEncoder
}

// Inspect accepts an inspector function that has same arguments as the Encoding.NewEncoder
func (mmNewEncoder *mEncodingMockNewEncoder) Inspect(f func()) *mEncodingMockNewEncoder {
	if mmNewEncoder.mock.inspectFuncNewEncoder != nil {
		mmNewEncoder.mock.t.Fatalf("Inspect function is already set for EncodingMock.NewEncoder")
	}

	mmNewEncoder.mock.inspectFuncNewEncoder = f

	return mmNewEncoder
}

// Return sets up results that will be returned by Encoding.NewEncoder
func (mmNewEncoder *mEncodingMockNewEncoder) Return(ep1 *mm_encoding.Encoder) *EncodingMock {
	if mmNewEncoder.mock.funcNewEncoder != nil {
		mmNewEncoder.mock.t.Fatalf("EncodingMock.NewEncoder mock is already set by Set")
	}

	if mmNewEncoder.defaultExpectation == nil {
		mmNewEncoder.defaultExpectation = &EncodingMockNewEncoderExpectation{mock: mmNewEncoder.mock}
	}
	mmNewEncoder.defaultExpectation.results = &EncodingMockNewEncoderResults{ep1}
	return mmNewEncoder.mock
}

//Set uses given function f to mock the Encoding.NewEncoder method
func (mmNewEncoder *mEncodingMockNewEncoder) Set(f func() (ep1 *mm_encoding.Encoder)) *EncodingMock {
	if mmNewEncoder.defaultExpectation != nil {
		mmNewEncoder.mock.t.Fatalf("Default expectation is already set for the Encoding.NewEncoder method")
	}

	if len(mmNewEncoder.expectations) > 0 {
		mmNewEncoder.mock.t.Fatalf("Some expectations are already set for the Encoding.NewEncoder method")
	}

	mmNewEncoder.mock.funcNewEncoder = f
	return mmNewEncoder.mock
}

// NewEncoder implements encoding.Encoding
func (mmNewEncoder *EncodingMock) NewEncoder() (ep1 *mm_encoding.Encoder) {
	mm_atomic.AddUint64(&mmNewEncoder.beforeNewEncoderCounter, 1)
	defer mm_atomic.AddUint64(&mmNewEncoder.afterNewEncoderCounter, 1)

	if mmNewEncoder.inspectFuncNewEncoder != nil {
		mmNewEncoder.inspectFuncNewEncoder()
	}

	if mmNewEncoder.NewEncoderMock.defaultExpectation != nil {
		mm_atomic.AddUint64(&mmNewEncoder.NewEncoderMock.defaultExpectation.Counter, 1)

		mm_results := mmNewEncoder.NewEncoderMock.defaultExpectation.results
		if mm_results == nil {
			mmNewEncoder.t.Fatal("No results are set for the EncodingMock.NewEncoder")
		}
		return (*mm_results).ep1
	}
	if mmNewEncoder.funcNewEncoder != nil {
		return mmNewEncoder.funcNewEncoder()
	}
	mmNewEncoder.t.Fatalf("Unexpected call to EncodingMock.NewEncoder.")
	return
}

// NewEncoderAfterCounter returns a count of finished EncodingMock.NewEncoder invocations
func (mmNewEncoder *EncodingMock) NewEncoderAfterCounter() uint64 {
	return mm_atomic.LoadUint64(&mmNewEncoder.afterNewEncoderCounter)
}

// NewEncoderBeforeCounter returns a count of EncodingMock.NewEncoder invocations
func (mmNewEncoder *EncodingMock) NewEncoderBeforeCounter() uint64 {
	return mm_atomic.LoadUint64(&mmNewEncoder.beforeNewEncoderCounter)
}

// MinimockNewEncoderDone returns true if the count of the NewEncoder invocations corresponds
// the number of defined expectations
func (m *EncodingMock) MinimockNewEncoderDone() bool {
	for _, e := range m.NewEncoderMock.expectations {
		if mm_atomic.LoadUint64(&e.Counter) < 1 {
			return false
		}
	}

	// if default expectation was set then invocations count should be greater than zero
	if m.NewEncoderMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterNewEncoderCounter) < 1 {
		return false
	}
	// if func was set then invocations count should be greater than zero
	if m.funcNewEncoder != nil && mm_atomic.LoadUint64(&m.afterNewEncoderCounter) < 1 {
		return false
	}
	return true
}

// MinimockNewEncoderInspect logs each unmet expectation
func (m *EncodingMock) MinimockNewEncoderInspect() {
	for _, e := range m.NewEncoderMock.expectations {
		if mm_atomic.LoadUint64(&e.Counter) < 1 {
			m.t.Error("Expected call to EncodingMock.NewEncoder")
		}
	}

	// if default expectation was set then invocations count should be greater than zero
	if m.NewEncoderMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterNewEncoderCounter) < 1 {
		m.t.Error("Expected call to EncodingMock.NewEncoder")
	}
	// if func was set then invocations count should be greater than zero
	if m.funcNewEncoder != nil && mm_atomic.LoadUint64(&m.afterNewEncoderCounter) < 1 {
		m.t.Error("Expected call to EncodingMock.NewEncoder")
	}
}

// MinimockFinish checks that all mocked methods have been called the expected number of times
func (m *EncodingMock) MinimockFinish() {
	if !m.minimockDone() {
		m.MinimockNewDecoderInspect()

		m.MinimockNewEncoderInspect()
		m.t.FailNow()
	}
}

// MinimockWait waits for all mocked methods to be called the expected number of times
func (m *EncodingMock) MinimockWait(timeout mm_time.Duration) {
	timeoutCh := mm_time.After(timeout)
	for {
		if m.minimockDone() {
			return
		}
		select {
		case <-timeoutCh:
			m.MinimockFinish()
			return
		case <-mm_time.After(10 * mm_time.Millisecond):
		}
	}
}

func (m *EncodingMock) minimockDone() bool {
	done := true
	return done &&
		m.MinimockNewDecoderDone() &&
		m.MinimockNewEncoderDone()
}


<|start_filename|>internal/utils/panic_interceptor_test.go<|end_filename|>
package utils_test

import (
	"errors"
	. "fisherman/internal/utils"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestPanicInterceptor(t *testing.T) {
	tests := []struct {
		name           string
		panicData      interface{}
		shouldBeCalled bool
	}{
		{
			name:           "intercepts panic and return with exit code 3",
			panicData:      "test panic",
			shouldBeCalled: true,
		},
		{
			name:           "intercepts panic and return with exit code 0",
			panicData:      errors.New("test error"),
			shouldBeCalled: true,
		},
		{
			name:      "intercepts panic and return with exit code 0",
			panicData: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			called := false

			assert.NotPanics(t, func() {
				defer PanicInterceptor(func(data interface{}) {
					called = true
					assert.Equal(t, tt.panicData, data)
				})

				panic(tt.panicData)
			})

			assert.Equal(t, tt.shouldBeCalled, called)
		})
	}
}


<|start_filename|>internal/handling/run_rules.go<|end_filename|>
package handling

import (
	"context"
	"fisherman/internal/configuration"
	"fisherman/internal/utils"
	"fisherman/pkg/log"
	"fisherman/pkg/prefixwriter"
	"fmt"
	"io"

	"github.com/evg4b/linebyline"
	"github.com/go-errors/errors"
)

type (
	in = <-chan configuration.Rule
)

func (h *HookHandler) runRules(ctx context.Context, rules []configuration.Rule) error {
	if h.workersCount == 0 {
		return errors.New("incorrect workers count")
	}

	if len(rules) == 0 {
		log.Debugf("no rules founded")

		return nil
	}

	input := rulesReduser(ctx, rules)

	g := workersGroupWithContext(ctx)
	for i := 0; i < utils.Min(int(h.workersCount), len(rules)); i++ {
		g.Go(worker(i, input, h.output))
	}

	return g.Wait()
}

func rulesReduser(ctx context.Context, rules []configuration.Rule) chan configuration.Rule {
	input := make(chan configuration.Rule)

	go func() {
		defer close(input)
		for _, rule := range rules {
			if err := ctx.Err(); err != nil {
				return
			}

			input <- rule
		}
	}()

	return input
}

// TODO: Add panic interceptor.
func worker(id int, input in, output io.Writer) func(context.Context) error {
	outputWriter := linebyline.NewByLineWriter(output)

	return func(ctx context.Context) error {
		log.Debugf("workder %d started", id)
		defer log.Debugf("workder %d finished", id)

		for rule := range input {
			log.Debugf("workder %d received rules %s", id, rule.GetPrefix())
			err := checkRule(ctx, rule, outputWriter)
			// TODO: Move canclation to workers run method
			if err != nil {
				return errors.Errorf("[%s] %s", rule.GetType(), err)
			}
		}

		return nil
	}
}

// TODO: Add more detailed validation result.
func checkRule(ctx context.Context, rule configuration.Rule, output io.Writer) error {
	prefix := fmt.Sprintf("%s | ", rule.GetPrefix())
	prefixedWriter := prefixwriter.NewWriter(output, prefix)

	return rule.Check(ctx, prefixedWriter)
}


<|start_filename|>pkg/shell/host_test.go<|end_filename|>
// nolint: errcheck, dupl
package shell_test

import (
	"bytes"
	"context"
	"errors"
	"fisherman/pkg/guards"
	. "fisherman/pkg/shell"
	"fisherman/testing/mocks"
	"fmt"
	"os/exec"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"golang.org/x/text/encoding"
	"golang.org/x/text/transform"
)

func TestHost_Start(t *testing.T) {
	t.Run("second call start returns error", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())
		err := host.Start()
		guards.NoError(err)
		defer host.Terminate()

		err = host.Start()

		assert.EqualError(t, err, "host already started")
	})

	t.Run("start returns error after print script", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())
		defer host.Terminate()

		fmt.Fprintln(host, "echo 1")
		err := host.Start()

		assert.EqualError(t, err, "host already started")
	})

	t.Run("stdin pipe error", func(t *testing.T) {
		command := exec.Command("echo", "test")
		command.Stdin = strings.NewReader("test")
		strategy := mocks.NewShellStrategyMock(t).
			GetCommandMock.Return(command).
			GetEncodingMock.Return(encoding.Nop)
		host := NewHost(context.TODO(), strategy)
		defer host.Terminate()

		err := host.Start()

		assert.EqualError(t, err, "exec: Stdin already set")
	})
}

func TestHost_Run(t *testing.T) {
	t.Run("return error for empty string", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())
		defer host.Terminate()

		err := host.Run("")

		assert.EqualError(t, err, "script can not be empty")
	})

	t.Run("return error for started host", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())
		err := host.Start()
		guards.NoError(err)
		defer host.Terminate()

		err = host.Run("echo 'test'")

		assert.EqualError(t, err, "host already started")
	})
}

func TestHost_Write(t *testing.T) {
	t.Run("write with fmt correctly", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())
		defer host.Terminate()

		_, err := fmt.Fprintln(host, "echo 'test'")

		assert.NoError(t, err)
	})

	t.Run("stdin pipe error", func(t *testing.T) {
		command := exec.Command("echo", "test")
		command.Stdin = strings.NewReader("test")
		strategy := mocks.NewShellStrategyMock(t).
			GetCommandMock.Return(command).
			GetEncodingMock.Return(encoding.Nop)
		host := NewHost(context.TODO(), strategy)
		defer host.Terminate()

		_, err := fmt.Fprintln(host, "echo 'test'")

		assert.EqualError(t, err, "exec: Stdin already set")
	})

	t.Run("write in call endoder", func(t *testing.T) {
		encoderTransformer := mocks.NewTransformerMock(t).
			ResetMock.Return().
			TransformMock.Set(transform.Nop.Transform)

		decoderTransformer := mocks.NewTransformerMock(t).
			ResetMock.Return().
			TransformMock.Set(transform.Nop.Transform)

		encoding := mocks.NewEncodingMock(t).
			NewDecoderMock.Return(&encoding.Decoder{Transformer: decoderTransformer}).
			NewEncoderMock.Return(&encoding.Encoder{Transformer: encoderTransformer})

		host := NewHost(context.TODO(), Default(), WithEncoding(encoding))
		defer host.Terminate()

		_, err := fmt.Fprintln(host, "echo 'test'")

		_ = host.Wait()

		assert.NoError(t, err)
		assert.NotEmpty(t, encoderTransformer.TransformMock.Calls())
		assert.Empty(t, decoderTransformer.TransformMock.Calls())
	})

	t.Run("write in call endoder and decoder", func(t *testing.T) {
		encoderTransformer := mocks.NewTransformerMock(t).
			ResetMock.Set(transform.Nop.Reset).
			TransformMock.Set(transform.Nop.Transform)

		decoderTransformer := mocks.NewTransformerMock(t).
			ResetMock.Set(transform.Nop.Reset).
			TransformMock.Set(transform.Nop.Transform)

		encoding := mocks.NewEncodingMock(t).
			NewDecoderMock.Return(&encoding.Decoder{Transformer: decoderTransformer}).
			NewEncoderMock.Return(&encoding.Encoder{Transformer: encoderTransformer})

		buff := &bytes.Buffer{}
		host := NewHost(context.TODO(), Default(), WithEncoding(encoding), WithStdout(buff), WithStderr(buff))
		defer host.Terminate()

		_, err := fmt.Fprintln(host, "echo 'test'")

		_ = host.Wait()

		assert.NoError(t, err)
		assert.NotEmpty(t, encoderTransformer.TransformMock.Calls())
		assert.NotEmpty(t, decoderTransformer.TransformMock.Calls())
	})
}

func TestHost_Close(t *testing.T) {
	t.Run("not started host", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())

		err := host.Close()

		assert.NoError(t, err)
	})

	t.Run("correctly closed stdin", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())
		err := host.Start()
		guards.NoError(err)
		defer host.Terminate()

		err = host.Close()

		assert.NoError(t, err)
	})

	t.Run("called multiple times", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())
		err := host.Start()
		guards.NoError(err)
		defer host.Terminate()

		assert.NotPanics(t, func() {
			err = host.Close()
			assert.NoError(t, err)

			err = host.Close()
			assert.NoError(t, err)
		})
	})

	t.Run("encoding error", func(t *testing.T) {
		transformer := mocks.NewTransformerMock(t).
			ResetMock.Set(transform.Nop.Reset).
			TransformMock.Set(func(dst, src []byte, atEOF bool) (nDst int, nSrc int, err error) {
			return 0, 0, errors.New("encoding error")
		})

		encoding := mocks.NewEncodingMock(t).
			NewDecoderMock.Return(&encoding.Decoder{Transformer: transformer}).
			NewEncoderMock.Return(&encoding.Encoder{Transformer: transformer})

		host := NewHost(context.TODO(), Default(), WithEncoding(encoding))
		defer host.Terminate()

		_ = host.Start()

		actual := host.Close()

		assert.EqualError(t, actual, "encoding error")
		assert.NotEmpty(t, transformer.TransformMock.Calls())
	})
}

func TestHost_Terminate(t *testing.T) {
	t.Run("returns errors where host not started", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())

		err := host.Terminate()

		assert.EqualError(t, err, "host is not started")
	})

	t.Run("terminate host correctly", func(t *testing.T) {
		host := NewHost(context.TODO(), Default())
		_ = host.Start()

		err := host.Terminate()

		assert.NoError(t, err)
	})
}

func TestHost_Wait(t *testing.T) {
	t.Run("return encoding error", func(t *testing.T) {
		transformer := mocks.NewTransformerMock(t).
			ResetMock.Set(transform.Nop.Reset).
			TransformMock.Set(func(dst, src []byte, atEOF bool) (nDst int, nSrc int, err error) {
			return 0, 0, errors.New("encoding error")
		})

		encoding := mocks.NewEncodingMock(t).
			NewDecoderMock.Return(&encoding.Decoder{Transformer: transformer}).
			NewEncoderMock.Return(&encoding.Encoder{Transformer: transformer})

		host := NewHost(context.TODO(), Default(), WithEncoding(encoding))
		defer host.Terminate()
		_ = host.Start()

		actual := host.Wait()

		assert.EqualError(t, actual, "encoding error")
		assert.NotEmpty(t, transformer.TransformMock.Calls())
	})
}


<|start_filename|>pkg/shell/encoding_unix.go<|end_filename|>
//go:build !windows
// +build !windows

package shell

import "golang.org/x/text/encoding"

func windowsEncoding() encoding.Encoding {
	return encoding.Nop
}


<|start_filename|>internal/rules/shell_script_test.go<|end_filename|>
package rules_test

import (
	"context"
	. "fisherman/internal/rules"
	"fisherman/testing/testutils"
	"io"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestShellScript_GetPosition(t *testing.T) {
	rule := ShellScript{
		BaseRule: BaseRule{Type: ShellScriptType},
	}

	actual := rule.GetPosition()

	assert.Equal(t, actual, Scripts)
}

func TestShellScript_Compile(t *testing.T) {
	rule := ShellScript{
		BaseRule: BaseRule{Type: ShellScriptType},
		Name:     "{{var1}}",
		Shell:    "{{var1}}",
		Commands: []string{"{{var1}}1", "{{var1}}2"},
		Env: map[string]string{
			"{{var1}}": "{{var1}}",
		},
		Dir:    "{{var1}}",
		Output: true,
	}

	rule.Compile(map[string]interface{}{"var1": "VALUE"})

	assert.Equal(t, ShellScript{
		BaseRule: BaseRule{Type: ShellScriptType},
		Name:     "VALUE",
		Shell:    "{{var1}}",
		Commands: []string{"VALUE1", "VALUE2"},
		Env: map[string]string{
			"{{var1}}": "VALUE",
		},
		Dir:    "VALUE",
		Output: true,
	}, rule)
}

func TestShellScript_GetPrefix(t *testing.T) {
	expectedValue := "TestName"
	rule := ShellScript{
		BaseRule: BaseRule{Type: ShellScriptType},
		Name:     expectedValue,
	}

	actual := rule.GetPrefix()

	assert.Equal(t, actual, expectedValue)
}

func TestShellScript_UnmarshalYAML(t *testing.T) {
	tests := []struct {
		name        string
		config      string
		expected    *ShellScript
		expectedErr string
	}{
		{
			name: "crossplatform script",
			config: `
type: shell-script
when: 1=1
name: TestName
`,
			expected: &ShellScript{
				BaseRule: BaseRule{
					Type:      ShellScriptType,
					Condition: "1=1",
				},
				Name: "TestName",
			},
		},
		{
			name: "incorrect yaml",
			config: `
type: shell-script
when: '
`,
			expected:    nil,
			expectedErr: "yaml: line 3: found unexpected end of stream",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var config ShellScript

			err := testutils.DecodeYaml(tt.config, &config)

			testutils.AssertError(t, tt.expectedErr, err)
			if len(tt.expectedErr) > 0 {
				assert.Equal(t, config, ShellScript{})
			} else {
				assert.Equal(t, *tt.expected, config)
			}
		})
	}
}

func TestShellScript_Check(t *testing.T) {
	t.Run("return error for unknown shell", func(t *testing.T) {
		rule := ShellScript{
			BaseRule: BaseRule{Type: ShellScriptType},
			Shell:    "unknown-shell",
		}

		err := rule.Check(context.TODO(), io.Discard)

		assert.EqualError(t, err, "failed to cheate shell host: unsupported shell")
	})

	t.Run("return error for incorrect encoding", func(t *testing.T) {
		rule := ShellScript{
			BaseRule: BaseRule{Type: ShellScriptType},
			Shell:    "bash",
			Encoding: "incorrect-encoding",
		}

		err := rule.Check(context.TODO(), io.Discard)

		assert.EqualError(t, err, "failed to cheate shell host: 'incorrect-encoding' is unknown encoding")
	})

	t.Run("executed successful", func(t *testing.T) {
		rule := ShellScript{
			BaseRule: BaseRule{Type: ShellScriptType},
			Commands: []string{
				"echo test",
			},
		}

		err := rule.Check(context.TODO(), io.Discard)

		assert.NoError(t, err)
	})

	t.Run("executed with non zero exit code ", func(t *testing.T) {
		rule := ShellScript{
			BaseRule: BaseRule{Type: ShellScriptType},
			Commands: []string{
				"exit 33",
			},
		}

		err := rule.Check(context.TODO(), io.Discard)

		assert.EqualError(t, err, "[shell-script] script finished with exit code 33")
	})
}


<|start_filename|>internal/handling/helpers.go<|end_filename|>
package handling

import (
	"fisherman/internal/configuration"
	"fisherman/internal/constants"
	"fmt"
)

// nolint: cyclop
func getConfig(name string, config *configuration.HooksConfig) (*configuration.HookConfig, error) {
	switch name {
	case constants.ApplyPatchMsgHook:
		return config.ApplyPatchMsgHook, nil
	case constants.CommitMsgHook:
		return config.CommitMsgHook, nil
	case constants.FsMonitorWatchmanHook:
		return config.FsMonitorWatchmanHook, nil
	case constants.PreApplyPatchHook:
		return config.PreApplyPatchHook, nil
	case constants.PreCommitHook:
		return config.PreCommitHook, nil
	case constants.PrePushHook:
		return config.PrePushHook, nil
	case constants.PreRebaseHook:
		return config.PreRebaseHook, nil
	case constants.PrepareCommitMsgHook:
		return config.PrepareCommitMsgHook, nil
	}

	return nil, fmt.Errorf("'%s' is not valid hook name", name)
}


<|start_filename|>internal/constants/urls.go<|end_filename|>
package constants

// ConfigurationDocksURL is url to main docs page.
const ConfigurationDocksURL = "https://fisherman.netlify.app/docs"

// ConfigurationInheritanceURL is url to configuration inheritance page.
const ConfigurationInheritanceURL = "https://fisherman.netlify.app/docs/configuration/configuration-files#configuration-file-inheritance"

// ConfigurationFilesDocksURL is url to configuration files page.
const ConfigurationFilesDocksURL = "https://fisherman.netlify.app/docs/configuration/configuration-files"


<|start_filename|>internal/utils/templates_test.go<|end_filename|>
package utils_test

import (
	"bytes"
	. "fisherman/internal/utils"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestPrintGraphics(t *testing.T) {
	tests := []struct {
		name           string
		content        string
		data           map[string]interface{}
		expectedOutput string
	}{
		{name: "Print template without data", content: "Template", data: nil, expectedOutput: "Template"},
		{
			name:           "Print template with empty data map",
			content:        "Template",
			data:           map[string]interface{}{},
			expectedOutput: "Template",
		},
		{
			name:    "Print template with correct data",
			content: "Template [{{Demo}}] = {{Test}}",
			data: map[string]interface{}{
				"Demo": "this is demo",
				"Test": "this is test",
			},
			expectedOutput: "Template [this is demo] = this is test",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			wr := &bytes.Buffer{}
			PrintGraphics(wr, tt.content, tt.data)
			assert.Equal(t, tt.expectedOutput, wr.String())
		})
	}
}

func TestPrintGraphicsPanics(t *testing.T) {
	tests := []struct {
		name    string
		content string
		data    map[string]interface{}
	}{
		{name: "Panics when template is brocken", content: "Template{{Demo", data: nil},
		{name: "Panics when writer is nil", content: "Template", data: nil},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Panics(t, func() {
				PrintGraphics(nil, tt.content, tt.data)
			})
		})
	}
}

func TestFillTemplate(t *testing.T) {
	tests := []struct {
		name     string
		template string
		data     map[string]interface{}
		expected string
	}{
		{
			name: "should update template correctly",
			data: map[string]interface{}{
				"Test":  "Test value",
				"Test2": "Test value2",
			},
			template: "Template = {{Test}} + {{Test2}}",
			expected: "Template = Test value + Test value2",
		},
		{
			name: "should skip template correctly",
			data: map[string]interface{}{
				"Test":  "Test value",
				"Test2": "Test value2",
			},
			template: "Template test",
			expected: "Template test",
		},
		{
			name: "should skip template correctly",
			data: map[string]interface{}{
				"Test": "[value]",
			},
			template: "{{Test}}={{Test}}={{Test}}",
			expected: "[value]=[value]=[value]",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.NotPanics(t, func() {
				FillTemplate(&tt.template, tt.data)

				assert.Equal(t, tt.expected, tt.template)
			})
		})
	}
}

func TestFillTemplatesArray(t *testing.T) {
	tests := []struct {
		name      string
		data      map[string]interface{}
		templates []string
		expected  []string
		wantErr   bool
	}{
		{
			name: "should update template correctly",
			data: map[string]interface{}{
				"Test":  "Test value",
				"Test2": "Test value2",
			},
			templates: []string{
				"Template = {{Test}} + {{Test2}}",
				"{{Test}}{{Test2}}",
			},
			expected: []string{
				"Template = Test value + Test value2",
				"Test valueTest value2",
			},
		},
		{
			name: "should skip template correctly",
			data: map[string]interface{}{
				"Test":  "Test value",
				"Test2": "Test value2",
			},
			templates: []string{"Template test", "DATA"},
			expected:  []string{"Template test", "DATA"},
		},
		{
			name: "should skip template correctly",
			data: map[string]interface{}{
				"Test": "[value]",
			},
			templates: []string{"{{Test}}={{Test}}={{Test}}"},
			expected:  []string{"[value]=[value]=[value]"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.NotPanics(t, func() {
				FillTemplatesArray(tt.templates, tt.data)
				assert.Equal(t, tt.expected, tt.templates)
			})
		})
	}
}

func TestFillTemplatesMap(t *testing.T) {
	tests := []struct {
		name      string
		data      map[string]interface{}
		templates map[string]string
		expected  map[string]string
		wantErr   bool
	}{
		{
			name: "should update template correctly",
			data: map[string]interface{}{
				"Test":  "Test value",
				"Test2": "Test value2",
			},
			templates: map[string]string{
				"{{Test}}":  "Template = {{Test}} + {{Test2}}",
				"{{Test2}}": "{{Test}}{{Test2}}",
			},
			expected: map[string]string{
				"{{Test}}":  "Template = Test value + Test value2",
				"{{Test2}}": "Test valueTest value2",
			},
		},
		{
			name: "should skip template correctly",
			data: map[string]interface{}{
				"Test":  "Test value",
				"Test2": "Test value2",
			},
			templates: map[string]string{
				"var1": "Template test",
				"var2": "DATA",
			},
			expected: map[string]string{
				"var1": "Template test",
				"var2": "DATA",
			},
		},
		{
			name: "should skip template correctly",
			data: map[string]interface{}{"Test": "[value]"},
			templates: map[string]string{
				"{{Test}}": "{{Test}}={{Test}}={{Test}}",
			},
			expected: map[string]string{
				"{{Test}}": "[value]=[value]=[value]",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.NotPanics(t, func() {
				FillTemplatesMap(tt.templates, tt.data)
				assert.Equal(t, tt.expected, tt.templates)
			})
		})
	}
}


<|start_filename|>internal/app/fisherman.go<|end_filename|>
package app

import (
	"context"
	"fisherman/internal"
	"fisherman/pkg/guards"
	"fisherman/pkg/log"
	"io"
	"os"

	"github.com/go-git/go-billy/v5"
)

// FishermanApp is main application structure.
type FishermanApp struct {
	cwd         string
	fs          billy.Filesystem
	repo        internal.Repository
	output      io.Writer
	commands    CliCommands
	env         []string
	interaption chan os.Signal
}

// NewFishermanApp is an fisherman application constructor.
func NewFishermanApp(options ...appOption) *FishermanApp {
	app := FishermanApp{
		output:      io.Discard,
		commands:    CliCommands{},
		cwd:         "",
		env:         []string{},
		interaption: make(chan os.Signal),
	}

	for _, option := range options {
		option(&app)
	}

	guards.ShouldBeDefined(app.fs, "FileSystem should be configured")
	guards.ShouldBeDefined(app.repo, "Repository should be configured")
	guards.ShouldBeDefined(app.commands, "Commands should be configured")

	return &app
}

// Run runs fisherman application.
func (r *FishermanApp) Run(baseCtx context.Context, args []string) error {
	ctx, cancel := context.WithCancel(baseCtx)
	subscribeInteruption(r.interaption, func() {
		log.Debug("application received interapt event")
		cancel()
	})

	if len(args) < 1 {
		log.Debug("No command detected")
		r.PrintDefaults()

		return nil
	}

	commandName, commandArgs := splitArgs(args)
	command, err := r.commands.GetCommand(commandName)
	if err != nil {
		return err
	}

	if err := command.Run(ctx, commandArgs); err != nil {
		log.Debugf("Command '%s' finished with error, %v", command.Name(), err)

		return err
	}

	log.Debugf("Command '%s' finished witout error", command.Name())

	return nil
}

func subscribeInteruption(interaption chan os.Signal, action func()) {
	go func() {
		<-interaption
		action()
	}()
}

func splitArgs(args []string) (string, []string) {
	return args[0], args[1:]
}


<|start_filename|>internal/rules/shell_script.go<|end_filename|>
package rules

import (
	"context"
	"fisherman/internal/utils"
	"fisherman/pkg/shell"
	"fisherman/pkg/shell/helpers"
	"fmt"
	"io"
	"io/ioutil"
	"os/exec"

	"github.com/go-errors/errors"
)

const ShellScriptType = "shell-script"

type ShellScript struct {
	BaseRule `yaml:",inline"`
	Name     string            `yaml:"name"`
	Shell    string            `yaml:"shell"`
	Commands []string          `yaml:"commands"`
	Env      map[string]string `yaml:"env"`
	Encoding string            `yaml:"encoding"`
	Output   bool              `yaml:"output"`
	Dir      string            `yaml:"dir"`
}

func (rule *ShellScript) GetPosition() byte {
	return Scripts
}

func (rule *ShellScript) GetPrefix() string {
	return utils.FirstNotEmpty(rule.Name, rule.Type)
}

func (rule *ShellScript) Check(ctx context.Context, output io.Writer) error {
	formatterOutput := formatOutput(output, rule)
	env := helpers.MergeEnv(rule.BaseRule.env, rule.Env)
	strategy, err := getShellStrategy(rule.Shell)
	if err != nil {
		return errors.Errorf("failed to cheate shell host: %w", err)
	}

	encoding, err := getEncoding(rule.Encoding)
	if err != nil {
		return errors.Errorf("failed to cheate shell host: %w", err)
	}

	host := shell.NewHost(
		ctx,
		strategy,
		shell.WithEnv(env),
		shell.WithStdout(formatterOutput),
		shell.WithCwd(rule.Dir),
		shell.WithEncoding(encoding),
	)

	for _, command := range rule.Commands {
		_, err := fmt.Fprintln(host, command)
		if err != nil {
			return errors.Errorf("failed to exec shell script: %w", err)
		}
	}

	err = host.Wait()
	if err != nil {
		var exitCodeError *exec.ExitError
		if errors.As(err, &exitCodeError) {
			return rule.errorf("script finished with exit code %d", exitCodeError.ExitCode())
		}

		return errors.Errorf("failed to exec shell script: %w", err)
	}

	return nil
}

func (rule *ShellScript) Compile(variables map[string]interface{}) {
	rule.BaseRule.Compile(variables)
	utils.FillTemplate(&rule.Dir, variables)
	utils.FillTemplate(&rule.Name, variables)
	utils.FillTemplatesArray(rule.Commands, variables)
	utils.FillTemplatesMap(rule.Env, variables)
}

func formatOutput(output io.Writer, rule *ShellScript) io.Writer {
	if rule.Output {
		return output
	}

	return ioutil.Discard
}

func getShellStrategy(name string) (shell.ShellStrategy, error) {
	if utils.IsEmpty(name) {
		return shell.Default(), nil
	}

	switch name {
	case "cmd":
		return shell.Cmd(), nil
	case "powershell":
		return shell.PowerShell(), nil
	case "bash":
		return shell.PowerShell(), nil
	default:
		return nil, errors.New("unsupported shell")
	}
}


<|start_filename|>internal/handling/allowed_hooks.go<|end_filename|>
package handling

import (
	"fisherman/internal/constants"
	"fisherman/internal/rules"
)

var allowedHooks = map[string][]string{
	constants.ApplyPatchMsgHook: {
		rules.ShellScriptType,
		rules.ExecType,
	},
	constants.CommitMsgHook: {
		rules.ShellScriptType,
		rules.ExecType,
		rules.CommitMessageType,
	},
	constants.FsMonitorWatchmanHook: {
		rules.ShellScriptType,
		rules.ExecType,
	},
	constants.PreApplyPatchHook: {
		rules.ShellScriptType,
		rules.ExecType,
	},
	constants.PreCommitHook: {
		rules.ShellScriptType,
		rules.ExecType,
		rules.AddToIndexType,
		rules.SuppressCommitFilesType,
	},
	constants.PrePushHook: {
		rules.ShellScriptType,
		rules.ExecType,
	},
	constants.PreRebaseHook: {
		rules.ShellScriptType,
		rules.ExecType,
	},
	constants.PrepareCommitMsgHook: {
		rules.PrepareMessageType,
	},
}


<|start_filename|>internal/constants/hooks.go<|end_filename|>
package constants

var (
	// ApplyPatchMsgHook is applypatch-msg hook constant.
	ApplyPatchMsgHook = "applypatch-msg"
	// CommitMsgHook is commit-msg hook constant.
	CommitMsgHook = "commit-msg"
	// FsMonitorWatchmanHook is fsmonitor-watchman hook constant.
	FsMonitorWatchmanHook = "fsmonitor-watchman"
	// PreApplyPatchHook is pre-applypatch hook constant.
	PreApplyPatchHook = "pre-applypatch"
	// PreCommitHook is pre-commit hook constant.
	PreCommitHook = "pre-commit"
	// PrePushHook is pre-push hook constant.
	PrePushHook = "pre-push"
	// PreRebaseHook is pre-rebase hook constant.
	PreRebaseHook = "pre-rebase"
	// PrepareCommitMsgHook is prepare-commit-msg hook constant.
	PrepareCommitMsgHook = "prepare-commit-msg"
)

// HooksNames is list on supported hooks.
var HooksNames = []string{
	ApplyPatchMsgHook,
	CommitMsgHook,
	FsMonitorWatchmanHook,
	PreApplyPatchHook,
	PreCommitHook,
	PrePushHook,
	PreRebaseHook,
	PrepareCommitMsgHook,
}


<|start_filename|>pkg/shell/host.go<|end_filename|>
package shell

import (
	"context"
	"errors"
	"fisherman/internal/utils"
	"io"
	"os/exec"
	"sync"

	"golang.org/x/text/encoding"
	"golang.org/x/text/transform"
)

// ShellStrategy is interface to describe base concrete shell command.
type ShellStrategy interface { // nolint: revive
	GetName() string
	GetCommand(context.Context) *exec.Cmd
	ArgsWrapper([]string) []string
	EnvWrapper([]string) []string
	GetEncoding() encoding.Encoding
}

// Host is shell host structure to comunicate with shell process.
type Host struct {
	command    *exec.Cmd
	stdin      io.Writer
	closeStdin func() error
	closeOnce  sync.Once
	mu         sync.Mutex
	encoding   encoding.Encoding
}

// NewHost creates new shell host based on passed strategy.
func NewHost(ctx context.Context, strategy ShellStrategy, options ...hostOption) *Host {
	host := &Host{
		command:    strategy.GetCommand(ctx),
		encoding:   strategy.GetEncoding(),
		mu:         sync.Mutex{},
		closeStdin: func() error { return nil },
	}

	for _, option := range options {
		option(strategy, host)
	}

	return host
}

// Write is io.Writer interface implementation.
// Write automatically starts shell process if it has not been started before.
func (host *Host) Write(payload []byte) (int, error) {
	host.mu.Lock()
	defer host.mu.Unlock()

	if !host.isStarted() {
		if err := host.startUnsave(); err != nil {
			return 0, err
		}
	}

	return host.stdin.Write(payload)
}

// Run runs new shell host based on passed strategy.
func (host *Host) Run(script string) error {
	if utils.IsEmpty(script) {
		return errors.New("script can not be empty")
	}

	if err := host.Start(); err != nil {
		return err
	}

	if script[len(script)-1] != '\n' {
		script += "\n"
	}

	if _, err := host.Write([]byte(script)); err != nil {
		return err
	}

	return host.Wait()
}

// Start starts shell host.
func (host *Host) Start() error {
	host.mu.Lock()
	defer host.mu.Unlock()

	if host.isStarted() {
		return errors.New("host already started")
	}

	return host.startUnsave()
}

// Wait waits for the shell to exit and waits for any copying from stdout or stderr to complete.
// Wait automatically closes stdin pipe, and writing will be unavailable after the call.
func (host *Host) Wait() error {
	if err := host.Close(); err != nil {
		return err
	}

	return host.command.Wait()
}

func (host *Host) Close() (err error) {
	host.closeOnce.Do(func() {
		err = host.closeStdin()
	})

	return err
}

func (host *Host) Terminate() error {
	if !host.isStarted() {
		return errors.New("host is not started")
	}

	return host.command.Process.Kill()
}

func (host *Host) isStarted() bool {
	return host.command.Process != nil
}

func (host *Host) startUnsave() error {
	originStdin, err := host.command.StdinPipe()
	if err != nil {
		return err
	}

	decoder := host.encoding.NewDecoder()
	encoder := host.encoding.NewEncoder()

	host.command.Stdout = wrapWriter(host.command.Stdout, decoder)
	host.command.Stderr = wrapWriter(host.command.Stderr, decoder)
	stdin := wrapWriter(originStdin, encoder)

	host.stdin = stdin
	host.closeStdin = func() error {
		encodingWrapperErr := stdin.Close()
		stdinErr := originStdin.Close()

		if encodingWrapperErr != nil {
			return encodingWrapperErr
		}

		return stdinErr
	}

	return host.command.Start()
}

func wrapWriter(w io.Writer, t transform.Transformer) io.WriteCloser {
	if w != nil {
		return transform.NewWriter(w, t)
	}

	return nil
}


<|start_filename|>internal/rules/prepare_message_test.go<|end_filename|>
package rules_test

import (
	"context"
	"fisherman/internal/configuration"
	. "fisherman/internal/rules"
	"fisherman/testing/testutils"
	"io/ioutil"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestPrepareMessage_Check(t *testing.T) {
	messageFilePath := "./hooks/MESSAGE"
	message := "custom message"

	fs := testutils.FsFromMap(t, map[string]string{
		messageFilePath: message,
	})

	t.Run("not configured rule", func(t *testing.T) {
		rule := makeRule(
			&PrepareMessage{},
			WithArgs([]string{messageFilePath}),
			WithFileSystem(fs),
		)

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.NoError(t, err)
	})

	t.Run("succeeded check ", func(t *testing.T) {
		rule := makeRule(
			&PrepareMessage{Message: message},
			WithArgs([]string{messageFilePath}),
			WithFileSystem(fs),
		)

		err := rule.Check(context.TODO(), ioutil.Discard)

		assert.NoError(t, err)
	})
}

func TestPrepareMessage_Compile(t *testing.T) {
	rule := PrepareMessage{
		Message: "{{var1}}",
	}

	rule.Compile(map[string]interface{}{"var1": "VALUE"})

	assert.Equal(t, PrepareMessage{
		Message: "VALUE",
	}, rule)
}

func makeRule(rule configuration.Rule, options ...RuleOption) configuration.Rule {
	rule.Configure(options...)

	return rule
}


<|start_filename|>pkg/guards/guards_test.go<|end_filename|>
package guards_test

import (
	. "fisherman/pkg/guards"
	"testing"

	"github.com/go-errors/errors"
	"github.com/stretchr/testify/assert"
)

func TestShouldBeDefined(t *testing.T) {
	var nilPointer *struct{}

	tests := []struct {
		name    string
		object  interface{}
		message string
		err     string
	}{
		{
			name:    "should not panic for zero",
			message: "unknown",
			object:  0,
		},
		{
			name:    "should not panic for defined empty string",
			message: "unknown",
			object:  "",
		},
		{
			name:    "should not panic for defined empty struct",
			message: "unknown",
			object:  struct{}{},
		},
		{
			name:    "should not panic for defined pointer to struct",
			message: "unknown",
			object:  &struct{}{},
		},
		{
			name:    "should panic for defined pointer to struct",
			message: "value is nil",
			object:  nilPointer,
			err:     "value is nil",
		},
		{
			name:    "should not panic for defined empty slice",
			message: "unknown",
			object:  []string{},
		},
		{
			name:    "should panic for nil value",
			message: "value is nil",
			err:     "value is nil",
			object:  nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if len(tt.err) > 0 {
				assert.PanicsWithError(t, tt.err, func() {
					ShouldBeDefined(tt.object, tt.message)
				})
			} else {
				assert.NotPanics(t, func() {
					ShouldBeDefined(tt.object, tt.message)
				})
			}
		})
	}
}

func TestNoError(t *testing.T) {
	t.Run("panic for error", func(t *testing.T) {
		err := errors.New("Test err")
		assert.PanicsWithError(t, err.Error(), func() {
			NoError(err)
		})
	})
	t.Run("does not panic for nil", func(t *testing.T) {
		assert.NotPanics(t, func() {
			NoError(nil)
		})
	})
}

func TestShouldBeNotEmpty(t *testing.T) {
	tests := []struct {
		name   string
		object string
		err    string
	}{
		{
			name:   "should not panic for string with numbers",
			object: "0",
		},
		{
			name:   "should panic for empty string",
			object: "",
			err:    "string is empty",
		},
		{
			name:   "should panic for tabs",
			object: "\t\t",
			err:    "string is empty",
		},
		{
			name:   "should panic for carret symbol",
			object: "\r\r",
			err:    "string is empty",
		},
		{
			name:   "should panic for spaces",
			object: "   ",
			err:    "string is empty",
		},
		{
			name:   "should not panic for string with witespace symbols",
			object: "\n\r\t  not empty",
		},
		{
			name:   "should panic for mixed content",
			err:    "string is empty",
			object: " \t \n   ",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if len(tt.err) > 0 {
				assert.PanicsWithError(t, tt.err, func() {
					ShouldBeNotEmpty(tt.object, "string is empty")
				})
			} else {
				assert.NotPanics(t, func() {
					ShouldBeNotEmpty(tt.object, "string is empty")
				})
			}
		})
	}
}


<|start_filename|>internal/rules/suppress_commit_files.go<|end_filename|>
package rules

import (
	"context"
	"fisherman/internal/utils"
	"io"
	"path/filepath"

	"github.com/go-errors/errors"

	"github.com/go-git/go-git/v5"
	"github.com/hashicorp/go-multierror"
)

const SuppressCommitFilesType = "suppress-commit-files"

type SuppressCommitFiles struct {
	BaseRule        `yaml:",inline"`
	Globs           []string `yaml:"globs"`
	RemoveFromIndex bool     `yaml:"remove-from-index"`
}

// nolint: cyclop
func (rule *SuppressCommitFiles) Check(ctx context.Context, _ io.Writer) error {
	if len(rule.Globs) == 0 {
		return nil
	}

	repo := rule.BaseRule.repo
	files, err := repo.GetFilesInIndex()
	if err != nil {
		return err
	}

	matchedFiles := []string{}
	for _, glob := range rule.Globs {
		for _, file := range files {
			matched, err := filepath.Match(glob, file)
			if err != nil {
				return err
			}
			if matched {
				matchedFiles = append(matchedFiles, file)
			}
		}
	}

	var multiError *multierror.Error
	for _, file := range matchedFiles {
		if rule.RemoveFromIndex {
			err := repo.RemoveGlob(file)
			if err != nil && !errors.Is(err, git.ErrGlobNoMatches) {
				return err
			}
		} else {
			multiError = multierror.Append(multiError, rule.errorf("file %s can not be committed", file))
		}
	}

	return multiError.ErrorOrNil()
}

func (rule *SuppressCommitFiles) GetPosition() byte {
	return PostScripts
}

func (rule *SuppressCommitFiles) Compile(variables map[string]interface{}) {
	rule.BaseRule.Compile(variables)
	utils.FillTemplatesArray(rule.Globs, variables)
}


<|start_filename|>internal/rules/suppressed_text_test.go<|end_filename|>
package rules_test

import (
	"context"
	"errors"
	"fisherman/internal"
	. "fisherman/internal/rules"
	"fisherman/pkg/vcs"
	"fisherman/testing/mocks"
	"io"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestSuppressedText_Check(t *testing.T) {
	t.Run("checked suppression", func(t *testing.T) {
		tests := []struct {
			name        string
			substrings  []string
			repo        internal.Repository
			expectedErr []string
		}{
			{
				name: "no changes",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.Return(map[string]vcs.Changes{}, nil),
				substrings: []string{"suppressed text"},
			},
			{
				name: "suppressed text not found",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.
					Return(map[string]vcs.Changes{
						"test/file.go": {{Status: vcs.Added, Change: "this is valid text"}},
						"README.md":    {{Status: vcs.Added, Change: "hello word"}},
					}, nil),
				substrings: []string{"suppressed text"},
			},
			{
				name: "suppressed text is deleted",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.
					Return(map[string]vcs.Changes{
						"test/file.go": {{Status: vcs.Deleted, Change: "this is suppressed text"}},
						"README.md":    {{Status: vcs.Added, Change: "hello word"}},
					}, nil),
				substrings: []string{"suppressed text"},
			},
			{
				name: "suppressed text is deleted",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.
					Return(map[string]vcs.Changes{
						"test/file.go": {{Status: vcs.Deleted, Change: "this is suppressed text"}},
						"README.md":    {{Status: vcs.Added, Change: "hello word"}},
					}, nil),
				substrings: []string{"suppressed text"},
			},
			{
				name: "suppressed text founded",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.
					Return(map[string]vcs.Changes{
						"test/file.go": {{Status: vcs.Added, Change: "this is suppressed text"}},
						"README.md":    {{Status: vcs.Added, Change: "hello word"}},
					}, nil),
				substrings:  []string{"suppressed text"},
				expectedErr: []string{"file 'test/file.go' should not contains 'suppressed text'"},
			},
			{
				name: "suppressed multiple text founded",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.
					Return(map[string]vcs.Changes{
						"test/file.go": {
							{Status: vcs.Added, Change: "this is suppressed text"},
							{Status: vcs.Added, Change: "this is second suppressed text line"},
						},
						"README.md": {{Status: vcs.Added, Change: "hello word"}},
					}, nil),
				substrings:  []string{"suppressed text"},
				expectedErr: []string{"file 'test/file.go' should not contains 'suppressed text'"},
			},
			{
				name: "multiple files with suppressed text founded",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.
					Return(map[string]vcs.Changes{
						"test/file.go": {{Status: vcs.Added, Change: "this is suppressed text"}},
						"README.md":    {{Status: vcs.Added, Change: "this is second suppressed text in other file"}},
					}, nil),
				substrings: []string{"suppressed text"},
				expectedErr: []string{
					"file 'test/file.go' should not contains 'suppressed text'",
					"file 'README.md' should not contains 'suppressed text'",
				},
			},
			{
				name: "multiple suppressed string in one line",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.
					Return(map[string]vcs.Changes{
						"test/file.go": {{Status: vcs.Added, Change: "this is suppressed text"}},
						"README.md":    {{Status: vcs.Deleted, Change: "this is second suppressed text in other file"}},
					}, nil),
				substrings: []string{"suppressed", "text"},
				expectedErr: []string{
					"file 'test/file.go' should not contains 'suppressed'",
					"file 'test/file.go' should not contains 'text'",
				},
			},
			{
				name: "multiple suppressed string in difference lines",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.
					Return(map[string]vcs.Changes{
						"test/file.go": {
							{Status: vcs.Added, Change: "this is suppressed line"},
							{Status: vcs.Added, Change: "this is not allowed text"},
						},
						"README.md": {
							{Status: vcs.Added, Change: "this is suppressed line"},
							{Status: vcs.Added, Change: "this is not allowed text"},
						},
					}, nil),
				substrings: []string{"suppressed", "text"},
				expectedErr: []string{
					"file 'test/file.go' should not contains 'suppressed'",
					"file 'test/file.go' should not contains 'text'",
					"file 'README.md' should not contains 'suppressed'",
					"file 'README.md' should not contains 'text'",
				},
			},
			{
				name: "internal error",
				repo: mocks.NewRepositoryMock(t).
					GetIndexChangesMock.
					Return(map[string]vcs.Changes{}, errors.New("test error")),
				substrings:  []string{"suppressed", "text"},
				expectedErr: []string{"test error"},
			},
		}
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				rule := makeRule(
					&SuppressedText{
						BaseRule:   BaseRule{Type: SuppressedTextType},
						Substrings: tt.substrings,
					},
					WithRepository(tt.repo),
				)

				err := rule.Check(context.TODO(), io.Discard)

				assertFlatMultiError(t, tt.expectedErr, err)
			})
		}
	})

	t.Run("checked suppression with excluded files", func(t *testing.T) {
		tests := []struct {
			name        string
			exclude     []string
			expectedErr []string
		}{
			{
				name:    "suppressed files not excluded",
				exclude: []string{"other-file.go"},
				expectedErr: []string{
					"file 'test/file.go' should not contains 'suppressed text'",
					"file 'README.md' should not contains 'suppressed text'",
				},
			},
			{
				name:        "suppressed single files excluded with glob",
				exclude:     []string{"*.md"},
				expectedErr: []string{"file 'test/file.go' should not contains 'suppressed text'"},
			},
			{
				name:        "suppressed single files not excluded with path",
				exclude:     []string{"test/file.go"},
				expectedErr: []string{"file 'README.md' should not contains 'suppressed text'"},
			},
			{
				name:        "winvalid glob pattern",
				exclude:     []string{"some/[*"},
				expectedErr: []string{"syntax error in pattern"},
			},
		}
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				repo := mocks.NewRepositoryMock(t).GetIndexChangesMock.
					Return(map[string]vcs.Changes{
						"test/file.go": {
							{Status: vcs.Added, Change: "this is suppressed text"},
							{Status: vcs.Added, Change: "this is allowed text"},
						},
						"README.md": {
							{Status: vcs.Added, Change: "this is suppressed text"},
							{Status: vcs.Added, Change: "this is allowed text"},
						},
					}, nil)

				rule := makeRule(
					&SuppressedText{
						BaseRule:      BaseRule{Type: SuppressedTextType},
						Substrings:    []string{"suppressed text"},
						ExcludedGlobs: tt.exclude,
					},
					WithRepository(repo),
				)

				err := rule.Check(context.TODO(), io.Discard)

				assertFlatMultiError(t, tt.expectedErr, err)
			})
		}
	})
}

func TestSuppressedText_GetPosition(t *testing.T) {
	rule := SuppressedText{
		BaseRule:   BaseRule{Type: SuppressedTextType},
		Substrings: []string{"suppressed text"},
	}

	actual := rule.GetPosition()

	assert.Equal(t, actual, PostScripts)
}

func TestSuppressedText_Compile(t *testing.T) {
	rule := SuppressedText{
		BaseRule:   BaseRule{Type: ShellScriptType},
		Substrings: []string{"DEMO {{var1}}"},
		ExcludedGlobs: []string{
			"Glob1 {{var1}}",
			"Glob2 {{var1}}",
		},
	}

	rule.Compile(map[string]interface{}{"var1": "VALUE"})

	assert.Equal(t, SuppressedText{
		BaseRule:   BaseRule{Type: ShellScriptType},
		Substrings: []string{"DEMO VALUE"},
		ExcludedGlobs: []string{
			"Glob1 VALUE",
			"Glob2 VALUE",
		},
	}, rule)
}

func assertFlatMultiError(t *testing.T, expected []string, actual error) {
	t.Helper()

	if len(expected) > 0 {
		message := actual.Error()
		parts := strings.Split(message, "\n")
		assert.Equal(t, len(parts), len(expected))
		for _, expectedLine := range expected {
			assert.Contains(t, parts, expectedLine)
		}
	} else {
		assert.NoError(t, actual)
	}
}


<|start_filename|>internal/commands/handle/run.go<|end_filename|>
package handle

import (
	"context"
	"errors"
	"fisherman/internal/configuration"
	"fisherman/internal/constants"
	"fisherman/internal/handling"
	"fisherman/internal/utils"
	"fisherman/pkg/log"
)

const noFilesLabel = "N/A"

func (c *Command) Run(ctx context.Context, args []string) error {
	err := c.flagSet.Parse(args)
	if err != nil {
		return err
	}

	handler, err := handling.NewHookHandler(
		c.hook,
		handling.WithExpressionEngine(c.engine),
		handling.WithHooksConfig(c.config),
		handling.WithGlobalVars(c.globalVars),
		handling.WithCwd(c.cwd),
		handling.WithFileSystem(c.fs),
		handling.WithRepository(c.repo),
		handling.WithArgs(c.flagSet.Args()),
		handling.WithEnv(c.env),
		handling.WithWorkersCount(c.workersCount),
		handling.WithOutput(c.output),
	)

	if err != nil {
		if errors.Is(err, handling.ErrNotPresented) {
			log.Debugf("hook %s not presented", c.hook)

			return nil
		}

		return err
	}

	log.Debugf("handler for '%s' hook founded", c.hook)

	files := c.configFiles
	utils.PrintGraphics(log.InfoOutput, constants.HookHeader, map[string]interface{}{
		constants.HookName:                 c.hook,
		constants.GlobalConfigPath:         utils.FirstNotEmpty(files[configuration.GlobalMode], noFilesLabel),
		constants.RepoConfigPath:           utils.FirstNotEmpty(files[configuration.RepoMode], noFilesLabel),
		constants.LocalConfigPath:          utils.FirstNotEmpty(files[configuration.LocalMode], noFilesLabel),
		constants.FishermanVersionVariable: constants.Version,
	})

	return handler.Handle(ctx)
}


<|start_filename|>pkg/shell/helpers/envs.go<|end_filename|>
package helpers

import (
	"fmt"
	"strings"
)

const keyValuePartsCount = 2

// MergeEnv concatenates environment variables list with passed map[string]string.
func MergeEnv(env []string, vars map[string]string) []string {
	if len(vars) == 0 {
		return env
	}

	parsedEnv := map[string]string{}
	for _, variable := range env {
		parts := strings.SplitN(variable, "=", keyValuePartsCount)
		parsedEnv[parts[0]] = parts[1]
	}

	for key, value := range vars {
		parsedEnv[key] = value
	}

	resultEnv := []string{}
	for key, value := range parsedEnv {
		resultEnv = append(resultEnv, fmt.Sprintf("%s=%s", key, value))
	}

	return resultEnv
}


<|start_filename|>internal/expression/expression.go<|end_filename|>
package expression

import (
	"github.com/antonmedv/expr"
	"github.com/antonmedv/expr/vm"
	"github.com/imdario/mergo"
)

type Engine interface {
	Eval(expression string, vars map[string]interface{}) (bool, error)
}

type GoExpressionEngine struct {
	vm vm.VM
}

func NewGoExpressionEngine() *GoExpressionEngine {
	return &GoExpressionEngine{}
}

func (e *GoExpressionEngine) Eval(expString string, vars map[string]interface{}) (bool, error) {
	env := EnvVars{}
	var castedVars EnvVars = vars

	if err := mergo.Merge(&env, castedVars); err != nil {
		return false, err
	}

	expression, err := expr.Compile(expString, engineOptions(env)...)
	if err != nil {
		return false, err
	}

	output, err := e.vm.Run(expression, env)
	if err != nil {
		return false, err
	}

	return output.(bool), nil
}

func engineOptions(env EnvVars) []expr.Option {
	return []expr.Option{
		expr.Env(env),
		expr.AllowUndefinedVariables(),
		expr.AsBool(),
		expr.Optimize(true),
	}
}


<|start_filename|>pkg/shell/cmd_strategy.go<|end_filename|>
package shell

import (
	"context"
	"os/exec"

	"golang.org/x/text/encoding"
)

type CmdStrategy struct{}

const cmdBin = "cmd"

func Cmd() *CmdStrategy {
	return &CmdStrategy{}
}

func (s *CmdStrategy) GetName() string {
	return cmdBin
}

func (s *CmdStrategy) GetCommand(ctx context.Context) *exec.Cmd {
	args := s.ArgsWrapper([]string{})

	return exec.CommandContext(ctx, cmdBin, args...)
}

func (s *CmdStrategy) ArgsWrapper(args []string) []string {
	return append([]string{"/Q", "/D", "/K"}, args...)
}

func (s *CmdStrategy) EnvWrapper(env []string) []string {
	return env
}

func (s *CmdStrategy) GetEncoding() encoding.Encoding {
	return windowsEncoding()
}


<|start_filename|>internal/handling/workers_group.go<|end_filename|>
package handling

import (
	"context"
	"sync"

	"github.com/hashicorp/go-multierror"
)

type workersGroup struct {
	wg  sync.WaitGroup
	ctx context.Context
	mu  sync.Mutex
	err *multierror.Error
}

func workersGroupWithContext(ctx context.Context) *workersGroup {
	return &workersGroup{ctx: ctx}
}

func (g *workersGroup) Wait() error {
	g.wg.Wait()

	return g.err.ErrorOrNil()
}

func (g *workersGroup) Go(f func(context.Context) error) {
	g.wg.Add(1)

	go func() {
		defer g.wg.Done()

		if err := g.ctx.Err(); err != nil {
			g.error(err)

			return
		}

		if err := f(g.ctx); err != nil {
			g.error(err)
		}
	}()
}

func (g *workersGroup) error(err error) {
	g.mu.Lock()
	g.err = multierror.Append(g.err, err)
	g.mu.Unlock()
}


<|start_filename|>testing/mocks/transformer_mock.go<|end_filename|>
package mocks

// Code generated by http://github.com/gojuno/minimock (dev). DO NOT EDIT.

//go:generate minimock -i golang.org/x/text/transform.Transformer -o ./testing/mocks/transformer_mock.go -n TransformerMock

import (
	"sync"
	mm_atomic "sync/atomic"
	mm_time "time"

	"github.com/gojuno/minimock/v3"
)

// TransformerMock implements transform.Transformer
type TransformerMock struct {
	t minimock.Tester

	funcReset          func()
	inspectFuncReset   func()
	afterResetCounter  uint64
	beforeResetCounter uint64
	ResetMock          mTransformerMockReset

	funcTransform          func(dst []byte, src []byte, atEOF bool) (nDst int, nSrc int, err error)
	inspectFuncTransform   func(dst []byte, src []byte, atEOF bool)
	afterTransformCounter  uint64
	beforeTransformCounter uint64
	TransformMock          mTransformerMockTransform
}

// NewTransformerMock returns a mock for transform.Transformer
func NewTransformerMock(t minimock.Tester) *TransformerMock {
	m := &TransformerMock{t: t}
	if controller, ok := t.(minimock.MockController); ok {
		controller.RegisterMocker(m)
	}

	m.ResetMock = mTransformerMockReset{mock: m}

	m.TransformMock = mTransformerMockTransform{mock: m}
	m.TransformMock.callArgs = []*TransformerMockTransformParams{}

	return m
}

type mTransformerMockReset struct {
	mock               *TransformerMock
	defaultExpectation *TransformerMockResetExpectation
	expectations       []*TransformerMockResetExpectation
}

// TransformerMockResetExpectation specifies expectation struct of the Transformer.Reset
type TransformerMockResetExpectation struct {
	mock *TransformerMock

	Counter uint64
}

// Expect sets up expected params for Transformer.Reset
func (mmReset *mTransformerMockReset) Expect() *mTransformerMockReset {
	if mmReset.mock.funcReset != nil {
		mmReset.mock.t.Fatalf("TransformerMock.Reset mock is already set by Set")
	}

	if mmReset.defaultExpectation == nil {
		mmReset.defaultExpectation = &TransformerMockResetExpectation{}
	}

	return mmReset
}

// Inspect accepts an inspector function that has same arguments as the Transformer.Reset
func (mmReset *mTransformerMockReset) Inspect(f func()) *mTransformerMockReset {
	if mmReset.mock.inspectFuncReset != nil {
		mmReset.mock.t.Fatalf("Inspect function is already set for TransformerMock.Reset")
	}

	mmReset.mock.inspectFuncReset = f

	return mmReset
}

// Return sets up results that will be returned by Transformer.Reset
func (mmReset *mTransformerMockReset) Return() *TransformerMock {
	if mmReset.mock.funcReset != nil {
		mmReset.mock.t.Fatalf("TransformerMock.Reset mock is already set by Set")
	}

	if mmReset.defaultExpectation == nil {
		mmReset.defaultExpectation = &TransformerMockResetExpectation{mock: mmReset.mock}
	}

	return mmReset.mock
}

//Set uses given function f to mock the Transformer.Reset method
func (mmReset *mTransformerMockReset) Set(f func()) *TransformerMock {
	if mmReset.defaultExpectation != nil {
		mmReset.mock.t.Fatalf("Default expectation is already set for the Transformer.Reset method")
	}

	if len(mmReset.expectations) > 0 {
		mmReset.mock.t.Fatalf("Some expectations are already set for the Transformer.Reset method")
	}

	mmReset.mock.funcReset = f
	return mmReset.mock
}

// Reset implements transform.Transformer
func (mmReset *TransformerMock) Reset() {
	mm_atomic.AddUint64(&mmReset.beforeResetCounter, 1)
	defer mm_atomic.AddUint64(&mmReset.afterResetCounter, 1)

	if mmReset.inspectFuncReset != nil {
		mmReset.inspectFuncReset()
	}

	if mmReset.ResetMock.defaultExpectation != nil {
		mm_atomic.AddUint64(&mmReset.ResetMock.defaultExpectation.Counter, 1)

		return

	}
	if mmReset.funcReset != nil {
		mmReset.funcReset()
		return
	}
	mmReset.t.Fatalf("Unexpected call to TransformerMock.Reset.")

}

// ResetAfterCounter returns a count of finished TransformerMock.Reset invocations
func (mmReset *TransformerMock) ResetAfterCounter() uint64 {
	return mm_atomic.LoadUint64(&mmReset.afterResetCounter)
}

// ResetBeforeCounter returns a count of TransformerMock.Reset invocations
func (mmReset *TransformerMock) ResetBeforeCounter() uint64 {
	return mm_atomic.LoadUint64(&mmReset.beforeResetCounter)
}

// MinimockResetDone returns true if the count of the Reset invocations corresponds
// the number of defined expectations
func (m *TransformerMock) MinimockResetDone() bool {
	for _, e := range m.ResetMock.expectations {
		if mm_atomic.LoadUint64(&e.Counter) < 1 {
			return false
		}
	}

	// if default expectation was set then invocations count should be greater than zero
	if m.ResetMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterResetCounter) < 1 {
		return false
	}
	// if func was set then invocations count should be greater than zero
	if m.funcReset != nil && mm_atomic.LoadUint64(&m.afterResetCounter) < 1 {
		return false
	}
	return true
}

// MinimockResetInspect logs each unmet expectation
func (m *TransformerMock) MinimockResetInspect() {
	for _, e := range m.ResetMock.expectations {
		if mm_atomic.LoadUint64(&e.Counter) < 1 {
			m.t.Error("Expected call to TransformerMock.Reset")
		}
	}

	// if default expectation was set then invocations count should be greater than zero
	if m.ResetMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterResetCounter) < 1 {
		m.t.Error("Expected call to TransformerMock.Reset")
	}
	// if func was set then invocations count should be greater than zero
	if m.funcReset != nil && mm_atomic.LoadUint64(&m.afterResetCounter) < 1 {
		m.t.Error("Expected call to TransformerMock.Reset")
	}
}

type mTransformerMockTransform struct {
	mock               *TransformerMock
	defaultExpectation *TransformerMockTransformExpectation
	expectations       []*TransformerMockTransformExpectation

	callArgs []*TransformerMockTransformParams
	mutex    sync.RWMutex
}

// TransformerMockTransformExpectation specifies expectation struct of the Transformer.Transform
type TransformerMockTransformExpectation struct {
	mock    *TransformerMock
	params  *TransformerMockTransformParams
	results *TransformerMockTransformResults
	Counter uint64
}

// TransformerMockTransformParams contains parameters of the Transformer.Transform
type TransformerMockTransformParams struct {
	dst   []byte
	src   []byte
	atEOF bool
}

// TransformerMockTransformResults contains results of the Transformer.Transform
type TransformerMockTransformResults struct {
	nDst int
	nSrc int
	err  error
}

// Expect sets up expected params for Transformer.Transform
func (mmTransform *mTransformerMockTransform) Expect(dst []byte, src []byte, atEOF bool) *mTransformerMockTransform {
	if mmTransform.mock.funcTransform != nil {
		mmTransform.mock.t.Fatalf("TransformerMock.Transform mock is already set by Set")
	}

	if mmTransform.defaultExpectation == nil {
		mmTransform.defaultExpectation = &TransformerMockTransformExpectation{}
	}

	mmTransform.defaultExpectation.params = &TransformerMockTransformParams{dst, src, atEOF}
	for _, e := range mmTransform.expectations {
		if minimock.Equal(e.params, mmTransform.defaultExpectation.params) {
			mmTransform.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmTransform.defaultExpectation.params)
		}
	}

	return mmTransform
}

// Inspect accepts an inspector function that has same arguments as the Transformer.Transform
func (mmTransform *mTransformerMockTransform) Inspect(f func(dst []byte, src []byte, atEOF bool)) *mTransformerMockTransform {
	if mmTransform.mock.inspectFuncTransform != nil {
		mmTransform.mock.t.Fatalf("Inspect function is already set for TransformerMock.Transform")
	}

	mmTransform.mock.inspectFuncTransform = f

	return mmTransform
}

// Return sets up results that will be returned by Transformer.Transform
func (mmTransform *mTransformerMockTransform) Return(nDst int, nSrc int, err error) *TransformerMock {
	if mmTransform.mock.funcTransform != nil {
		mmTransform.mock.t.Fatalf("TransformerMock.Transform mock is already set by Set")
	}

	if mmTransform.defaultExpectation == nil {
		mmTransform.defaultExpectation = &TransformerMockTransformExpectation{mock: mmTransform.mock}
	}
	mmTransform.defaultExpectation.results = &TransformerMockTransformResults{nDst, nSrc, err}
	return mmTransform.mock
}

//Set uses given function f to mock the Transformer.Transform method
func (mmTransform *mTransformerMockTransform) Set(f func(dst []byte, src []byte, atEOF bool) (nDst int, nSrc int, err error)) *TransformerMock {
	if mmTransform.defaultExpectation != nil {
		mmTransform.mock.t.Fatalf("Default expectation is already set for the Transformer.Transform method")
	}

	if len(mmTransform.expectations) > 0 {
		mmTransform.mock.t.Fatalf("Some expectations are already set for the Transformer.Transform method")
	}

	mmTransform.mock.funcTransform = f
	return mmTransform.mock
}

// When sets expectation for the Transformer.Transform which will trigger the result defined by the following
// Then helper
func (mmTransform *mTransformerMockTransform) When(dst []byte, src []byte, atEOF bool) *TransformerMockTransformExpectation {
	if mmTransform.mock.funcTransform != nil {
		mmTransform.mock.t.Fatalf("TransformerMock.Transform mock is already set by Set")
	}

	expectation := &TransformerMockTransformExpectation{
		mock:   mmTransform.mock,
		params: &TransformerMockTransformParams{dst, src, atEOF},
	}
	mmTransform.expectations = append(mmTransform.expectations, expectation)
	return expectation
}

// Then sets up Transformer.Transform return parameters for the expectation previously defined by the When method
func (e *TransformerMockTransformExpectation) Then(nDst int, nSrc int, err error) *TransformerMock {
	e.results = &TransformerMockTransformResults{nDst, nSrc, err}
	return e.mock
}

// Transform implements transform.Transformer
func (mmTransform *TransformerMock) Transform(dst []byte, src []byte, atEOF bool) (nDst int, nSrc int, err error) {
	mm_atomic.AddUint64(&mmTransform.beforeTransformCounter, 1)
	defer mm_atomic.AddUint64(&mmTransform.afterTransformCounter, 1)

	if mmTransform.inspectFuncTransform != nil {
		mmTransform.inspectFuncTransform(dst, src, atEOF)
	}

	mm_params := &TransformerMockTransformParams{dst, src, atEOF}

	// Record call args
	mmTransform.TransformMock.mutex.Lock()
	mmTransform.TransformMock.callArgs = append(mmTransform.TransformMock.callArgs, mm_params)
	mmTransform.TransformMock.mutex.Unlock()

	for _, e := range mmTransform.TransformMock.expectations {
		if minimock.Equal(e.params, mm_params) {
			mm_atomic.AddUint64(&e.Counter, 1)
			return e.results.nDst, e.results.nSrc, e.results.err
		}
	}

	if mmTransform.TransformMock.defaultExpectation != nil {
		mm_atomic.AddUint64(&mmTransform.TransformMock.defaultExpectation.Counter, 1)
		mm_want := mmTransform.TransformMock.defaultExpectation.params
		mm_got := TransformerMockTransformParams{dst, src, atEOF}
		if mm_want != nil && !minimock.Equal(*mm_want, mm_got) {
			mmTransform.t.Errorf("TransformerMock.Transform got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got))
		}

		mm_results := mmTransform.TransformMock.defaultExpectation.results
		if mm_results == nil {
			mmTransform.t.Fatal("No results are set for the TransformerMock.Transform")
		}
		return (*mm_results).nDst, (*mm_results).nSrc, (*mm_results).err
	}
	if mmTransform.funcTransform != nil {
		return mmTransform.funcTransform(dst, src, atEOF)
	}
	mmTransform.t.Fatalf("Unexpected call to TransformerMock.Transform. %v %v %v", dst, src, atEOF)
	return
}

// TransformAfterCounter returns a count of finished TransformerMock.Transform invocations
func (mmTransform *TransformerMock) TransformAfterCounter() uint64 {
	return mm_atomic.LoadUint64(&mmTransform.afterTransformCounter)
}

// TransformBeforeCounter returns a count of TransformerMock.Transform invocations
func (mmTransform *TransformerMock) TransformBeforeCounter() uint64 {
	return mm_atomic.LoadUint64(&mmTransform.beforeTransformCounter)
}

// Calls returns a list of arguments used in each call to TransformerMock.Transform.
// The list is in the same order as the calls were made (i.e. recent calls have a higher index)
func (mmTransform *mTransformerMockTransform) Calls() []*TransformerMockTransformParams {
	mmTransform.mutex.RLock()

	argCopy := make([]*TransformerMockTransformParams, len(mmTransform.callArgs))
	copy(argCopy, mmTransform.callArgs)

	mmTransform.mutex.RUnlock()

	return argCopy
}

// MinimockTransformDone returns true if the count of the Transform invocations corresponds
// the number of defined expectations
func (m *TransformerMock) MinimockTransformDone() bool {
	for _, e := range m.TransformMock.expectations {
		if mm_atomic.LoadUint64(&e.Counter) < 1 {
			return false
		}
	}

	// if default expectation was set then invocations count should be greater than zero
	if m.TransformMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterTransformCounter) < 1 {
		return false
	}
	// if func was set then invocations count should be greater than zero
	if m.funcTransform != nil && mm_atomic.LoadUint64(&m.afterTransformCounter) < 1 {
		return false
	}
	return true
}

// MinimockTransformInspect logs each unmet expectation
func (m *TransformerMock) MinimockTransformInspect() {
	for _, e := range m.TransformMock.expectations {
		if mm_atomic.LoadUint64(&e.Counter) < 1 {
			m.t.Errorf("Expected call to TransformerMock.Transform with params: %#v", *e.params)
		}
	}

	// if default expectation was set then invocations count should be greater than zero
	if m.TransformMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterTransformCounter) < 1 {
		if m.TransformMock.defaultExpectation.params == nil {
			m.t.Error("Expected call to TransformerMock.Transform")
		} else {
			m.t.Errorf("Expected call to TransformerMock.Transform with params: %#v", *m.TransformMock.defaultExpectation.params)
		}
	}
	// if func was set then invocations count should be greater than zero
	if m.funcTransform != nil && mm_atomic.LoadUint64(&m.afterTransformCounter) < 1 {
		m.t.Error("Expected call to TransformerMock.Transform")
	}
}

// MinimockFinish checks that all mocked methods have been called the expected number of times
func (m *TransformerMock) MinimockFinish() {
	if !m.minimockDone() {
		m.MinimockResetInspect()

		m.MinimockTransformInspect()
		m.t.FailNow()
	}
}

// MinimockWait waits for all mocked methods to be called the expected number of times
func (m *TransformerMock) MinimockWait(timeout mm_time.Duration) {
	timeoutCh := mm_time.After(timeout)
	for {
		if m.minimockDone() {
			return
		}
		select {
		case <-timeoutCh:
			m.MinimockFinish()
			return
		case <-mm_time.After(10 * mm_time.Millisecond):
		}
	}
}

func (m *TransformerMock) minimockDone() bool {
	done := true
	return done &&
		m.MinimockResetDone() &&
		m.MinimockTransformDone()
}


<|start_filename|>internal/commands/handle/run_test.go<|end_filename|>
package handle_test

import (
	"context"
	"errors"
	. "fisherman/internal/commands/handle"
	"fisherman/internal/configuration"
	"fisherman/internal/constants"
	"fisherman/internal/rules"
	"fisherman/pkg/log"
	"fisherman/pkg/vcs"
	"fisherman/testing/mocks"
	"io"
	"runtime"
	"testing"

	"github.com/stretchr/testify/assert"
)

var globalVars = map[string]interface{}{
	constants.BranchNameVariable:       "/refs/head/develop",
	constants.TagVariable:              "1.0.0",
	constants.UserEmailVariable:        "<EMAIL>",
	constants.UserNameVariable:         "evg4b",
	constants.FishermanVersionVariable: constants.Version,
	constants.CwdVariable:              "~/project",
	constants.OsVariable:               runtime.GOOS,
}

func TestCommand_Run(t *testing.T) {
	log.SetOutput(io.Discard)

	repoStub := mocks.NewRepositoryMock(t).
		GetCurrentBranchMock.Return("/refs/head/develop", nil).
		GetLastTagMock.Return("1.0.0", nil).
		GetUserMock.Return(vcs.User{UserName: "evg4b", Email: "<EMAIL>"}, nil)

	validRule := mocks.NewRuleMock(t).
		GetTypeMock.Return(rules.ExecType).
		ConfigureMock.Return().
		GetPositionMock.Return(rules.Scripts).
		CompileMock.Return().
		GetContitionMock.Return("").
		GetPrefixMock.Return("prefix-").
		CheckMock.Return(nil)

	t.Run("runs correctly", func(t *testing.T) {
		command := NewCommand(
			WithFileSystem(mocks.NewFilesystemMock(t)),
			WithHooksConfig(&configuration.HooksConfig{
				PreCommitHook: &configuration.HookConfig{
					Rules: []configuration.Rule{validRule},
				},
			}),
			WithRepository(repoStub),
			WithWorkersCount(5),
			WithCwd("/"),
			WithExpressionEngine(mocks.NewEngineMock(t)),
			WithHooksConfig(&mocks.HooksConfigStub),
		)

		err := command.Run(context.TODO(), []string{"--hook", "pre-commit"})

		assert.NoError(t, err)
	})

	t.Run("unknown hook", func(t *testing.T) {
		command := NewCommand(
			WithFileSystem(mocks.NewFilesystemMock(t)),
			WithHooksConfig(&configuration.HooksConfig{
				PreCommitHook: &configuration.HookConfig{
					Rules: []configuration.Rule{
						mocks.NewRuleMock(t),
					},
				},
			}),
			WithRepository(repoStub),
			WithWorkersCount(5),
			WithCwd("/"),
			WithExpressionEngine(mocks.NewEngineMock(t)),
			WithHooksConfig(&mocks.HooksConfigStub),
		)

		err := command.Run(context.TODO(), []string{"--hook", "test"})

		assert.EqualError(t, err, "'test' is not valid hook name")
	})

	t.Run("call handler and return error", func(t *testing.T) {
		command := NewCommand(
			WithFileSystem(mocks.NewFilesystemMock(t)),
			WithHooksConfig(&configuration.HooksConfig{
				PreCommitHook: &configuration.HookConfig{
					Rules: []configuration.Rule{
						validRule.CheckMock.Return(errors.New("test error")),
					},
				},
			}),
			WithRepository(repoStub),
			WithWorkersCount(5),
			WithCwd("/"),
			WithExpressionEngine(mocks.NewEngineMock(t)),
		)

		err := command.Run(context.TODO(), []string{"--hook", "pre-commit"})

		assert.EqualError(t, err, "1 error occurred:\n\t* [exec] test error\n\n")
	})

	t.Run("call handler with global variables", func(t *testing.T) {
		command := NewCommand(
			WithFileSystem(mocks.NewFilesystemMock(t)),
			WithHooksConfig(&configuration.HooksConfig{
				PreCommitHook: &configuration.HookConfig{
					Rules: []configuration.Rule{validRule},
				},
			}),
			WithGlobalVars(globalVars),
			WithRepository(mocks.NewRepositoryMock(t).
				GetCurrentBranchMock.Return("/refs/head/develop", nil).
				GetLastTagMock.Return("1.0.0", errors.New("test error")).
				GetUserMock.Return(vcs.User{UserName: "evg4b", Email: "<EMAIL>"}, nil)),
			WithCwd("/"),
			WithExpressionEngine(mocks.NewEngineMock(t)),
		)

		err := command.Run(context.TODO(), []string{"--hook", "pre-commit"})

		assert.EqualError(t, err, "1 error occurred:\n\t* [exec] test error\n\n")
	})
}


<|start_filename|>internal/commands/initialize/options.go<|end_filename|>
package initialize

import (
	"os/user"

	"github.com/go-git/go-billy/v5"
)

type initializeOption = func(c *Command)

func WithCwd(cwd string) initializeOption {
	return func(c *Command) {
		c.cwd = cwd
	}
}

func WithFilesystem(fs billy.Filesystem) initializeOption {
	return func(c *Command) {
		c.fs = fs
	}
}

func WithUser(user *user.User) initializeOption {
	return func(c *Command) {
		c.user = user
	}
}

func WithExecutable(executable string) initializeOption {
	return func(c *Command) {
		c.executable = executable
	}
}


<|start_filename|>internal/utils/string_slice_test.go<|end_filename|>
package utils_test

import (
	. "fisherman/internal/utils"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestFilter(t *testing.T) {
	tests := []struct {
		name     string
		source   []string
		expected []string
	}{
		{
			name:     "empty slice",
			source:   []string{},
			expected: []string{},
		},
		{
			name:     "two elements are matched",
			source:   []string{"#1", "2", "#3", "4"},
			expected: []string{"#1", "#3"},
		},
		{
			name:     "no matched elements",
			source:   []string{"1", "2", "3", "4"},
			expected: []string{},
		},
		{
			name:     "all elements are matched",
			source:   []string{"#1", "#2", "#3", "#4"},
			expected: []string{"#1", "#2", "#3", "#4"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			filtered := Filter(tt.source, func(value string) bool {
				return strings.Contains(value, "#")
			})

			assert.Equal(t, tt.expected, filtered)
		})
	}
}

func TestContains(t *testing.T) {
	tests := []struct {
		name       string
		collection []string
		value      string
		expected   bool
	}{
		{
			name:       "empty slice",
			collection: []string{},
			value:      "demo",
			expected:   false,
		},
		{
			name:       "single value slice",
			collection: []string{"demo"},
			value:      "demo",
			expected:   true,
		},
		{
			name:       "slice with target value",
			collection: []string{"value1", "value2", "value3", "value4"},
			value:      "value3",
			expected:   true,
		},
		{
			name:       "slice withot target value",
			collection: []string{"value1", "value2", "value3", "value4"},
			value:      "value0",
			expected:   false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actual := Contains(tt.collection, tt.value)
			assert.Equal(t, tt.expected, actual)
		})
	}
}


<|start_filename|>internal/commands/handle/handle_test.go<|end_filename|>
package handle_test

import (
	. "fisherman/internal/commands/handle"
	"fisherman/testing/mocks"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestCommand_Name(t *testing.T) {
	command := NewCommand(
		WithFileSystem(mocks.NewFilesystemMock(t)),
		WithRepository(mocks.NewRepositoryMock(t)),
		WithCwd("/"),
		WithExpressionEngine(mocks.NewEngineMock(t)),
		WithHooksConfig(&mocks.HooksConfigStub),
	)

	assert.Equal(t, "handle", command.Name())
}

func TestCommand_Description(t *testing.T) {
	command := NewCommand(
		WithFileSystem(mocks.NewFilesystemMock(t)),
		WithRepository(mocks.NewRepositoryMock(t)),
		WithCwd("/"),
		WithExpressionEngine(mocks.NewEngineMock(t)),
		WithHooksConfig(&mocks.HooksConfigStub),
	)

	assert.NotEmpty(t, command.Description())
}

func TestNewCommand(t *testing.T) {
	t.Run("panic when cwd is not configured", func(t *testing.T) {
		t.SkipNow()
		assert.PanicsWithError(t, "Cwd should be configured", func() {
			_ = NewCommand(
				WithFileSystem(mocks.NewFilesystemMock(t)),
				WithRepository(mocks.NewRepositoryMock(t)),
				WithExpressionEngine(mocks.NewEngineMock(t)),
				WithHooksConfig(&mocks.HooksConfigStub),
			)
		})
	})

	t.Run("panic when filesystem is not configured", func(t *testing.T) {
		t.SkipNow()
		assert.PanicsWithError(t, "FileSystem should be configured", func() {
			_ = NewCommand(
				WithRepository(mocks.NewRepositoryMock(t)),
				WithCwd("/"),
				WithExpressionEngine(mocks.NewEngineMock(t)),
				WithHooksConfig(&mocks.HooksConfigStub),
			)
		})
	})

	t.Run("panic when repository is not configured", func(t *testing.T) {
		t.SkipNow()
		assert.PanicsWithError(t, "Repository should be configured", func() {
			_ = NewCommand(
				WithFileSystem(mocks.NewFilesystemMock(t)),
				WithCwd("/"),
				WithExpressionEngine(mocks.NewEngineMock(t)),
				WithHooksConfig(&mocks.HooksConfigStub),
			)
		})
	})

	t.Run("panic when expression engine is not configured", func(t *testing.T) {
		t.SkipNow()
		assert.PanicsWithError(t, "ExpressionEngine should be configured", func() {
			_ = NewCommand(
				WithFileSystem(mocks.NewFilesystemMock(t)),
				WithRepository(mocks.NewRepositoryMock(t)),
				WithCwd("/"),
				WithHooksConfig(&mocks.HooksConfigStub),
			)
		})
	})

	t.Run("panic when hooks config is not configured", func(t *testing.T) {
		assert.PanicsWithError(t, "HooksConfig should be configured", func() {
			_ = NewCommand(
				WithFileSystem(mocks.NewFilesystemMock(t)),
				WithRepository(mocks.NewRepositoryMock(t)),
				WithCwd("/"),
				WithExpressionEngine(mocks.NewEngineMock(t)),
			)
		})
	})
}


<|start_filename|>internal/utils/int.go<|end_filename|>
package utils

import "errors"

func Min(values ...int) int {
	if len(values) == 0 {
		panic(errors.New("min: no arguments"))
	}

	minValue := values[0]
	for _, value := range values[1:] {
		if minValue > value {
			minValue = value
		}
	}

	return minValue
}

func Max(values ...int) int {
	if len(values) == 0 {
		panic(errors.New("max: no arguments"))
	}

	maxValue := values[0]
	for _, value := range values[1:] {
		if value > maxValue {
			maxValue = value
		}
	}

	return maxValue
}


<|start_filename|>internal/utils/int_test.go<|end_filename|>
package utils_test

import (
	"fisherman/internal/utils"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
)

var testCases = []struct {
	collection []int
	min        int
	max        int
}{
	{
		collection: []int{0},
		min:        0,
		max:        0,
	},
	{
		collection: []int{-1, 0},
		min:        -1,
		max:        0,
	},
	{
		collection: []int{1, 2, 3, 4, 5},
		min:        1,
		max:        5,
	},
	{
		collection: []int{5, 4, 3, 2, 1},
		min:        1,
		max:        5,
	},
	{
		collection: []int{0, -100, 100, -100, 100, 0},
		min:        -100,
		max:        100,
	},
}

func TestMin(t *testing.T) {
	for _, tt := range testCases {
		t.Run(fmt.Sprintf("returns %d for %v", tt.min, tt.collection), func(t *testing.T) {
			actual := utils.Min(tt.collection...)

			assert.Equal(t, tt.min, actual)
		})
	}

	t.Run("panic for where no arguments", func(t *testing.T) {
		assert.PanicsWithError(t, "min: no arguments", func() {
			_ = utils.Min()
		})
	})
}

func TestMax(t *testing.T) {
	for _, tt := range testCases {
		t.Run(fmt.Sprintf("returns %d for %v", tt.max, tt.collection), func(t *testing.T) {
			actual := utils.Max(tt.collection...)

			assert.Equal(t, tt.max, actual)
		})
	}

	t.Run("panic for where no arguments", func(t *testing.T) {
		assert.PanicsWithError(t, "max: no arguments", func() {
			_ = utils.Max()
		})
	})
}


<|start_filename|>internal/expression/env.go<|end_filename|>
package expression

import (
	"fisherman/internal/constants"
	"fisherman/internal/utils"
)

type EnvVars map[string]interface{}

func (vars EnvVars) IsEmpty(value string) bool {
	return utils.IsEmpty(value)
}
func (vars EnvVars) IsWindows() bool {
	return vars[constants.OsVariable] == Windows
}
func (vars EnvVars) IsLinux() bool {
	return vars[constants.OsVariable] == Linux
}

func (vars EnvVars) IsMacOs() bool {
	return vars[constants.OsVariable] == Macos
}


<|start_filename|>internal/commands/handle/options.go<|end_filename|>
package handle

import (
	"fisherman/internal"
	"fisherman/internal/configuration"
	"fisherman/internal/expression"
	"io"

	"github.com/go-git/go-billy/v5"
)

type commandOption = func(*Command)

func WithExpressionEngine(engine expression.Engine) commandOption {
	return func(h *Command) {
		h.engine = engine
	}
}

func WithHooksConfig(config *configuration.HooksConfig) commandOption {
	return func(h *Command) {
		h.config = config
	}
}

func WithGlobalVars(globalVars map[string]interface{}) commandOption {
	return func(h *Command) {
		h.globalVars = globalVars
	}
}

func WithCwd(cwd string) commandOption {
	return func(h *Command) {
		h.cwd = cwd
	}
}

func WithFileSystem(fs billy.Filesystem) commandOption {
	return func(h *Command) {
		h.fs = fs
	}
}

func WithRepository(repo internal.Repository) commandOption {
	return func(h *Command) {
		h.repo = repo
	}
}
func WithEnv(env []string) commandOption {
	return func(h *Command) {
		h.env = env
	}
}

func WithWorkersCount(workersCount uint) commandOption {
	return func(h *Command) {
		h.workersCount = workersCount
	}
}

func WithConfigFiles(configFiles map[string]string) commandOption {
	return func(h *Command) {
		h.configFiles = configFiles
	}
}

func WithOutput(output io.Writer) commandOption {
	return func(h *Command) {
		h.output = output
	}
}


<|start_filename|>internal/commands/handle/handle.go<|end_filename|>
package handle

import (
	"fisherman/internal"
	"fisherman/internal/configuration"
	"fisherman/internal/expression"
	"fisherman/pkg/guards"
	"flag"
	"io"

	"github.com/go-git/go-billy/v5"
)

type Command struct {
	flagSet      *flag.FlagSet
	hook         string
	usage        string
	engine       expression.Engine
	config       *configuration.HooksConfig
	globalVars   map[string]interface{}
	cwd          string
	fs           billy.Filesystem
	repo         internal.Repository
	env          []string
	workersCount uint
	configFiles  map[string]string
	output       io.Writer
}

const defaultWorkerCount = 5

func NewCommand(options ...commandOption) *Command {
	command := &Command{
		flagSet:      flag.NewFlagSet("handle", flag.ExitOnError),
		usage:        "starts hook processing based on the config file (for debugging only)",
		workersCount: defaultWorkerCount,
		output:       io.Discard,
		configFiles:  map[string]string{},
		globalVars:   map[string]interface{}{},
		env:          []string{},
	}

	for _, option := range options {
		option(command)
	}

	guards.ShouldBeDefined(command.fs, "FileSystem should be configured")
	guards.ShouldBeDefined(command.repo, "Repository should be configured")
	guards.ShouldBeNotEmpty(command.cwd, "Cwd should be configured")
	guards.ShouldBeDefined(command.engine, "ExpressionEngine should be configured")
	guards.ShouldBeDefined(command.config, "HooksConfig should be configured")

	command.flagSet.StringVar(&command.hook, "hook", "<empty>", "hook name")

	return command
}

func (c *Command) Name() string {
	return c.flagSet.Name()
}

func (c *Command) Description() string {
	return c.usage
}
