Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions internal/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import (
"errors"
"slices"
"strings"
"time"
)

var ErrFilesIncompatible = errors.New("one of your runners contains incompatible file types")

type Command struct {
Run string `json:"run" mapstructure:"run" toml:"run" yaml:"run"`
Files string `json:"files,omitempty" mapstructure:"files" toml:"files,omitempty" yaml:",omitempty"`
Root string `json:"root,omitempty" mapstructure:"root" toml:"root,omitempty" yaml:",omitempty"`
FailText string `json:"fail_text,omitempty" koanf:"fail_text" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"`
Timeout string `json:"timeout,omitempty" mapstructure:"timeout" toml:"timeout,omitempty" yaml:",omitempty"`
Run string `json:"run" mapstructure:"run" toml:"run" yaml:"run"`
Files string `json:"files,omitempty" mapstructure:"files" toml:"files,omitempty" yaml:",omitempty"`
Root string `json:"root,omitempty" mapstructure:"root" toml:"root,omitempty" yaml:",omitempty"`
FailText string `json:"fail_text,omitempty" koanf:"fail_text" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"`
Timeout time.Duration `json:"timeout,omitempty" jsonschema:"type=string,example=15s" mapstructure:"timeout" toml:"timeout,omitempty" yaml:",omitempty"`

Skip any `json:"skip,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"`
Only any `json:"only,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"`
Expand Down
9 changes: 5 additions & 4 deletions internal/config/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -44,19 +45,19 @@ func TestCommandsToJobsWithTimeout(t *testing.T) {
commands := map[string]*Command{
"lint": {
Run: "echo lint",
Timeout: "60s",
Timeout: 60 * time.Second,
Priority: 1,
},
"test": {
Run: "echo test",
Timeout: "5m",
Timeout: 5 * time.Minute,
},
}

jobs := CommandsToJobs(commands)

assert.Equal(t, jobs, []*Job{
{Name: "lint", Run: "echo lint", Timeout: "60s"},
{Name: "test", Run: "echo test", Timeout: "5m"},
{Name: "lint", Run: "echo lint", Timeout: 60 * time.Second},
{Name: "test", Run: "echo test", Timeout: 5 * time.Minute},
})
}
20 changes: 11 additions & 9 deletions internal/config/job.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package config

import "time"

type Job struct {
Name string `json:"name,omitempty" mapstructure:"name" toml:"name,omitempty" yaml:",omitempty"`
Run string `json:"run,omitempty" jsonschema:"oneof_required=Run a command" mapstructure:"run" toml:"run,omitempty" yaml:",omitempty"`
Script string `json:"script,omitempty" jsonschema:"oneof_required=Run a script" mapstructure:"script" toml:"script,omitempty" yaml:",omitempty"`
Runner string `json:"runner,omitempty" mapstructure:"runner" toml:"runner,omitempty" yaml:",omitempty"`
Args string `json:"args,omitempty" mapstructure:"args" toml:"args,omitempty" yaml:",omitempty"`
Root string `json:"root,omitempty" mapstructure:"root" toml:"root,omitempty" yaml:",omitempty"`
Files string `json:"files,omitempty" mapstructure:"files" toml:"files,omitempty" yaml:",omitempty"`
FailText string `json:"fail_text,omitempty" koanf:"fail_text" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"`
Timeout string `json:"timeout,omitempty" mapstructure:"timeout" toml:"timeout,omitempty" yaml:",omitempty"`
Name string `json:"name,omitempty" mapstructure:"name" toml:"name,omitempty" yaml:",omitempty"`
Run string `json:"run,omitempty" jsonschema:"oneof_required=Run a command" mapstructure:"run" toml:"run,omitempty" yaml:",omitempty"`
Script string `json:"script,omitempty" jsonschema:"oneof_required=Run a script" mapstructure:"script" toml:"script,omitempty" yaml:",omitempty"`
Runner string `json:"runner,omitempty" mapstructure:"runner" toml:"runner,omitempty" yaml:",omitempty"`
Args string `json:"args,omitempty" mapstructure:"args" toml:"args,omitempty" yaml:",omitempty"`
Root string `json:"root,omitempty" mapstructure:"root" toml:"root,omitempty" yaml:",omitempty"`
Files string `json:"files,omitempty" mapstructure:"files" toml:"files,omitempty" yaml:",omitempty"`
FailText string `json:"fail_text,omitempty" koanf:"fail_text" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"`
Timeout time.Duration `json:"timeout,omitempty" jsonschema:"type=string,example=15s" mapstructure:"timeout" toml:"timeout,omitempty" yaml:",omitempty"`

Glob []string `json:"glob,omitempty" jsonschema:"oneof_type=string;array" mapstructure:"glob" toml:"glob,omitempty" yaml:",omitempty"`
Exclude []string `json:"exclude,omitempty" jsonschema:"oneof_type=string;array" mapstructure:"exclude" toml:"exclude,omitempty" yaml:",omitempty"`
Expand Down
20 changes: 19 additions & 1 deletion internal/config/jsonschema.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
"fail_text": {
"type": "string"
},
"timeout": {
"type": "string",
"examples": [
"15s"
]
},
"skip": {
"oneOf": [
{
Expand Down Expand Up @@ -267,6 +273,12 @@
"fail_text": {
"type": "string"
},
"timeout": {
"type": "string",
"examples": [
"15s"
]
},
"glob": {
"oneOf": [
{
Expand Down Expand Up @@ -442,6 +454,12 @@
"fail_text": {
"type": "string"
},
"timeout": {
"type": "string",
"examples": [
"15s"
]
},
"interactive": {
"type": "boolean"
},
Expand All @@ -456,7 +474,7 @@
"type": "object"
}
},
"$comment": "Last updated on 2026.01.20.",
"$comment": "Last updated on 2026.01.27.",
"properties": {
"min_version": {
"type": "string",
Expand Down
11 changes: 6 additions & 5 deletions internal/config/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"slices"
"strconv"
"strings"
"time"
"unicode"
)

Expand All @@ -18,11 +19,11 @@ type Script struct {
Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"`
Priority int `json:"priority,omitempty" mapstructure:"priority" toml:"priority,omitempty" yaml:",omitempty"`

FailText string `json:"fail_text,omitempty" koanf:"fail_text" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"`
Timeout string `json:"timeout,omitempty" mapstructure:"timeout" toml:"timeout,omitempty" yaml:",omitempty"`
Interactive bool `json:"interactive,omitempty" mapstructure:"interactive" toml:"interactive,omitempty" yaml:",omitempty"`
UseStdin bool `json:"use_stdin,omitempty" koanf:"use_stdin" mapstructure:"use_stdin" toml:"use_stdin,omitempty" yaml:"use_stdin,omitempty"`
StageFixed bool `json:"stage_fixed,omitempty" koanf:"stage_fixed" mapstructure:"stage_fixed" toml:"stage_fixed,omitempty" yaml:"stage_fixed,omitempty"`
FailText string `json:"fail_text,omitempty" koanf:"fail_text" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"`
Timeout time.Duration `json:"timeout,omitempty" jsonschema:"type=string,example=15s" mapstructure:"timeout" toml:"timeout,omitempty" yaml:",omitempty"`
Interactive bool `json:"interactive,omitempty" mapstructure:"interactive" toml:"interactive,omitempty" yaml:",omitempty"`
UseStdin bool `json:"use_stdin,omitempty" koanf:"use_stdin" mapstructure:"use_stdin" toml:"use_stdin,omitempty" yaml:"use_stdin,omitempty"`
StageFixed bool `json:"stage_fixed,omitempty" koanf:"stage_fixed" mapstructure:"stage_fixed" toml:"stage_fixed,omitempty" yaml:"stage_fixed,omitempty"`
}

func ScriptsToJobs(scripts map[string]*Script) []*Job {
Expand Down
9 changes: 5 additions & 4 deletions internal/config/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -44,19 +45,19 @@ func TestScriptsToJobsWithTimeout(t *testing.T) {
scripts := map[string]*Script{
"lint.sh": {
Runner: "bash",
Timeout: "30s",
Timeout: 30 * time.Second,
Priority: 1,
},
"test.sh": {
Runner: "bash",
Timeout: "10m",
Timeout: 10 * time.Minute,
},
}

jobs := ScriptsToJobs(scripts)

assert.Equal(t, jobs, []*Job{
{Name: "lint.sh", Script: "lint.sh", Runner: "bash", Timeout: "30s"},
{Name: "test.sh", Script: "test.sh", Runner: "bash", Timeout: "10m"},
{Name: "lint.sh", Script: "lint.sh", Runner: "bash", Timeout: 30 * time.Second},
{Name: "test.sh", Script: "test.sh", Runner: "bash", Timeout: 10 * time.Minute},
})
}
1 change: 0 additions & 1 deletion internal/run/controller/exec/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ type Options struct {
Root string
Commands []string
Env map[string]string
Timeout string
Interactive, UseStdin bool
}

Expand Down
36 changes: 0 additions & 36 deletions internal/run/controller/exec/executor_test.go

This file was deleted.

18 changes: 12 additions & 6 deletions internal/run/controller/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,22 +129,28 @@ func (c *Controller) runSingleJob(ctx context.Context, scope *scope, id string,

env := maps.Clone(scope.env)
maps.Copy(env, job.Env)
ok, failText := c.run(ctx, strings.Join(append(scope.names, name), " ❯ "), scope.follow, exec.Options{

if job.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, job.Timeout)
defer cancel()
}
err = c.run(ctx, strings.Join(append(scope.names, name), " ❯ "), scope.follow, exec.Options{
Root: filepath.Join(c.git.RootPath, scope.root),
Commands: commands,
Interactive: job.Interactive && !scope.opts.DisableTTY,
UseStdin: job.UseStdin,
Timeout: job.Timeout,
Env: env,
})

executionTime := time.Since(startTime)

if !ok {
if failText == "" {
failText = job.FailText
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return result.Failure(name, "timeout ("+job.Timeout.String()+")", executionTime)
}
return result.Failure(name, failText, executionTime)

return result.Failure(name, job.FailText, executionTime)
}

if config.HookUsesStagedFiles(scope.hookName) && job.StageFixed && !scope.opts.NoStageFixed {
Expand Down
36 changes: 3 additions & 33 deletions internal/run/controller/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,16 @@ import (
"context"
"io"
"os"
"time"

"github.com/evilmartians/lefthook/v2/internal/log"
"github.com/evilmartians/lefthook/v2/internal/run/controller/exec"
"github.com/evilmartians/lefthook/v2/internal/system"
)

func (c *Controller) run(ctx context.Context, name string, follow bool, opts exec.Options) (ok bool, failText string) {
func (c *Controller) run(ctx context.Context, name string, follow bool, opts exec.Options) error {
log.SetName(name)
defer log.UnsetName(name)

// Apply timeout if specified
var timeoutDuration string
if opts.Timeout != "" {
timeout, err := parseDuration(opts.Timeout)
if err != nil {
log.Errorf("invalid timeout format '%s': %s\n", opts.Timeout, err)
return false, ""
}
timeoutDuration = opts.Timeout
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}

// If the command does not explicitly `use_stdin` no input will be provided.
var in io.Reader = system.NullReader
if opts.UseStdin {
Expand All @@ -46,27 +31,12 @@ func (c *Controller) run(ctx context.Context, name string, follow bool, opts exe
out = io.Discard
}

err := c.executor.Execute(ctx, opts, in, out)

if err != nil && ctx.Err() == context.DeadlineExceeded {
return false, "timeout (" + timeoutDuration + ")"
}
return err == nil, ""
return c.executor.Execute(ctx, opts, in, out)
}

out := new(bytes.Buffer)

err := c.executor.Execute(ctx, opts, in, out)

log.Execution(name, err, out)

if err != nil && ctx.Err() == context.DeadlineExceeded {
return false, "timeout (" + timeoutDuration + ")"
}
return err == nil, ""
}

// parseDuration parses a duration string (e.g., "60s", "5m", "1h30m").
func parseDuration(duration string) (time.Duration, error) {
return time.ParseDuration(duration)
return err
}
Loading
Loading