mirror of
https://github.com/go-task/task.git
synced 2025-02-15 14:03:30 +02:00
Add Preconditions to Tasks
This commit is contained in:
parent
6ff9ba9df9
commit
bd5882f0f0
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,3 +21,6 @@ dist/
|
|||||||
|
|
||||||
# intellij idea/goland
|
# intellij idea/goland
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# exuberant ctags
|
||||||
|
tags
|
||||||
|
@ -141,6 +141,21 @@ includes:
|
|||||||
docker: ./DockerTasks.yml
|
docker: ./DockerTasks.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Version 2.3
|
||||||
|
|
||||||
|
Version 2.3 comes with `preconditions` stanza in tasks.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '2'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
upload_environment:
|
||||||
|
preconditions:
|
||||||
|
- test -f .env
|
||||||
|
cmds:
|
||||||
|
- aws s3 cp .env s3://myenvironment
|
||||||
|
```
|
||||||
|
|
||||||
Please check the [documentation][includes]
|
Please check the [documentation][includes]
|
||||||
|
|
||||||
[output]: usage.md#output-syntax
|
[output]: usage.md#output-syntax
|
||||||
|
@ -344,6 +344,53 @@ up-to-date.
|
|||||||
Also, `task --status [tasks]...` will exit with a non-zero exit code if any of
|
Also, `task --status [tasks]...` will exit with a non-zero exit code if any of
|
||||||
the tasks are not up-to-date.
|
the tasks are not up-to-date.
|
||||||
|
|
||||||
|
If you need a certain set of conditions to be _true_ you can use the
|
||||||
|
`preconditions` stanza. `preconditions` are very similar to `status`
|
||||||
|
lines except they support `sh` expansion and they SHOULD all return 0
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '2'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
generate-files:
|
||||||
|
cmds:
|
||||||
|
- mkdir directory
|
||||||
|
- touch directory/file1.txt
|
||||||
|
- touch directory/file2.txt
|
||||||
|
# test existence of files
|
||||||
|
preconditions:
|
||||||
|
- test -f .env
|
||||||
|
- sh: "[ 1 = 0 ]"
|
||||||
|
msg: "One doesn't equal Zero, Halting"
|
||||||
|
```
|
||||||
|
|
||||||
|
Preconditions can set specific failure messages that can tell
|
||||||
|
a user what to do using the `msg` field.
|
||||||
|
|
||||||
|
If a task has a dependency on a sub-task with a precondition, and that
|
||||||
|
precondition is not met - the calling task will fail. Adding `ignore_errors`
|
||||||
|
to the precondition will cause parent tasks to execute even if the sub task
|
||||||
|
can not run. Note that a task executed directly with a failing precondition
|
||||||
|
will not run unless `--force` is given.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '2'
|
||||||
|
tasks:
|
||||||
|
task_will_fail:
|
||||||
|
preconditions:
|
||||||
|
- sh: "exit 1"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
task_will_succeed:
|
||||||
|
deps:
|
||||||
|
- task_will_fail
|
||||||
|
|
||||||
|
task_will_succeed:
|
||||||
|
cmds:
|
||||||
|
- task: task_will_fail
|
||||||
|
- echo "I will run"
|
||||||
|
```
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
When doing interpolation of variables, Task will look for the below.
|
When doing interpolation of variables, Task will look for the below.
|
||||||
|
51
internal/taskfile/precondition.go
Normal file
51
internal/taskfile/precondition.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package taskfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrCantUnmarshalPrecondition is returned for invalid precond YAML.
|
||||||
|
ErrCantUnmarshalPrecondition = errors.New("task: can't unmarshal precondition value")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Precondition represents a precondition necessary for a task to run
|
||||||
|
type Precondition struct {
|
||||||
|
Sh string
|
||||||
|
Msg string
|
||||||
|
IgnoreError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||||
|
func (p *Precondition) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var cmd string
|
||||||
|
|
||||||
|
if err := unmarshal(&cmd); err == nil {
|
||||||
|
p.Sh = cmd
|
||||||
|
p.Msg = fmt.Sprintf("`%s` failed", cmd)
|
||||||
|
p.IgnoreError = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sh struct {
|
||||||
|
Sh string
|
||||||
|
Msg string
|
||||||
|
IgnoreError bool `yaml:"ignore_error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := unmarshal(&sh)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
p.Sh = sh.Sh
|
||||||
|
p.Msg = sh.Msg
|
||||||
|
if p.Msg == "" {
|
||||||
|
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.IgnoreError = sh.IgnoreError
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
49
internal/taskfile/precondition_test.go
Normal file
49
internal/taskfile/precondition_test.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package taskfile_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v2/internal/taskfile"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPreconditionParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
content string
|
||||||
|
v interface{}
|
||||||
|
expected interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"test -f foo.txt",
|
||||||
|
&taskfile.Precondition{},
|
||||||
|
&taskfile.Precondition{Sh: `test -f foo.txt`, Msg: "`test -f foo.txt` failed", IgnoreError: false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sh: '[ 1 = 0 ]'",
|
||||||
|
&taskfile.Precondition{},
|
||||||
|
&taskfile.Precondition{Sh: "[ 1 = 0 ]", Msg: "[ 1 = 0 ] failed", IgnoreError: false},
|
||||||
|
},
|
||||||
|
{`
|
||||||
|
sh: "[ 1 = 2 ]"
|
||||||
|
msg: "1 is not 2"
|
||||||
|
`,
|
||||||
|
&taskfile.Precondition{},
|
||||||
|
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2", IgnoreError: false},
|
||||||
|
},
|
||||||
|
{`
|
||||||
|
sh: "[ 1 = 2 ]"
|
||||||
|
msg: "1 is not 2"
|
||||||
|
ignore_error: true
|
||||||
|
`,
|
||||||
|
&taskfile.Precondition{},
|
||||||
|
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2", IgnoreError: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
err := yaml.Unmarshal([]byte(test.content), test.v)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, test.v)
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ type Task struct {
|
|||||||
Sources []string
|
Sources []string
|
||||||
Generates []string
|
Generates []string
|
||||||
Status []string
|
Status []string
|
||||||
|
Precondition []*Precondition
|
||||||
Dir string
|
Dir string
|
||||||
Vars Vars
|
Vars Vars
|
||||||
Env Vars
|
Env Vars
|
||||||
|
@ -10,6 +10,8 @@ var (
|
|||||||
v21 = mustVersion("2.1")
|
v21 = mustVersion("2.1")
|
||||||
v22 = mustVersion("2.2")
|
v22 = mustVersion("2.2")
|
||||||
v23 = mustVersion("2.3")
|
v23 = mustVersion("2.3")
|
||||||
|
v24 = mustVersion("2.4")
|
||||||
|
v25 = mustVersion("2.5")
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsV1 returns if is a given Taskfile version is version 1
|
// IsV1 returns if is a given Taskfile version is version 1
|
||||||
@ -37,6 +39,16 @@ func IsV23(v *semver.Constraints) bool {
|
|||||||
return v.Check(v23)
|
return v.Check(v23)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsV24 returns if is a given Taskfile version is at least version 2.4
|
||||||
|
func IsV24(v *semver.Constraints) bool {
|
||||||
|
return v.Check(v24)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsV25 returns if is a given Taskfile version is at least version 2.5
|
||||||
|
func IsV25(v *semver.Constraints) bool {
|
||||||
|
return v.Check(v25)
|
||||||
|
}
|
||||||
|
|
||||||
func mustVersion(s string) *semver.Version {
|
func mustVersion(s string) *semver.Version {
|
||||||
v, err := semver.NewVersion(s)
|
v, err := semver.NewVersion(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
44
precondition.go
Normal file
44
precondition.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Package task provides ...
|
||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v2/internal/execext"
|
||||||
|
"github.com/go-task/task/v2/internal/taskfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNecessaryPreconditionFailed is returned when a precondition fails
|
||||||
|
ErrNecessaryPreconditionFailed = errors.New("task: precondition not met")
|
||||||
|
// ErrOptionalPreconditionFailed is returned when a precondition fails
|
||||||
|
// that has ignore_error set to true
|
||||||
|
ErrOptionalPreconditionFailed = errors.New("task: optional precondition not met")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||||
|
var optionalPreconditionFailed bool
|
||||||
|
for _, p := range t.Precondition {
|
||||||
|
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||||
|
Command: p.Sh,
|
||||||
|
Dir: t.Dir,
|
||||||
|
Env: getEnviron(t),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Outf(p.Msg)
|
||||||
|
if p.IgnoreError == true {
|
||||||
|
optionalPreconditionFailed = true
|
||||||
|
} else {
|
||||||
|
return false, ErrNecessaryPreconditionFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if optionalPreconditionFailed == true {
|
||||||
|
return true, ErrOptionalPreconditionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
@ -78,8 +78,10 @@ func (e *Executor) isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (
|
|||||||
Env: getEnviron(t),
|
Env: getEnviron(t),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
e.Logger.VerboseOutf("task: status command %s exited non-zero: %s", s, err)
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
e.Logger.VerboseOutf("task: status command %s exited zero", s)
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
35
task.go
35
task.go
@ -119,7 +119,7 @@ func (e *Executor) Setup() error {
|
|||||||
Vars: e.taskvars,
|
Vars: e.taskvars,
|
||||||
Logger: e.Logger,
|
Logger: e.Logger,
|
||||||
}
|
}
|
||||||
case version.IsV2(v), version.IsV21(v), version.IsV22(v):
|
case version.IsV2(v), version.IsV21(v), version.IsV22(v), version.IsV23(v):
|
||||||
e.Compiler = &compilerv2.CompilerV2{
|
e.Compiler = &compilerv2.CompilerV2{
|
||||||
Dir: e.Dir,
|
Dir: e.Dir,
|
||||||
Taskvars: e.taskvars,
|
Taskvars: e.taskvars,
|
||||||
@ -127,8 +127,9 @@ func (e *Executor) Setup() error {
|
|||||||
Expansions: e.Taskfile.Expansions,
|
Expansions: e.Taskfile.Expansions,
|
||||||
Logger: e.Logger,
|
Logger: e.Logger,
|
||||||
}
|
}
|
||||||
case version.IsV23(v):
|
|
||||||
return fmt.Errorf(`task: Taskfile versions greater than v2.3 not implemented in the version of Task`)
|
case version.IsV24(v):
|
||||||
|
return fmt.Errorf(`task: Taskfile versions greater than v2.4 not implemented in the version of Task`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !version.IsV21(v) && e.Taskfile.Output != "" {
|
if !version.IsV21(v) && e.Taskfile.Output != "" {
|
||||||
@ -192,7 +193,13 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if upToDate {
|
|
||||||
|
preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if upToDate && preCondMet {
|
||||||
if !e.Silent {
|
if !e.Silent {
|
||||||
e.Logger.Errf(`task: Task "%s" is up to date`, t.Task)
|
e.Logger.Errf(`task: Task "%s" is up to date`, t.Task)
|
||||||
}
|
}
|
||||||
@ -224,7 +231,15 @@ func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
|
|||||||
d := d
|
d := d
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
return e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
|
err := e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrOptionalPreconditionFailed {
|
||||||
|
e.Logger.Errf("%s", err)
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +251,15 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case cmd.Task != "":
|
case cmd.Task != "":
|
||||||
return e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
|
err := e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrOptionalPreconditionFailed {
|
||||||
|
e.Logger.Errf("%s", err)
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
case cmd.Cmd != "":
|
case cmd.Cmd != "":
|
||||||
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
|
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
|
||||||
e.Logger.Errf(cmd.Cmd)
|
e.Logger.Errf(cmd.Cmd)
|
||||||
|
62
task_test.go
62
task_test.go
@ -273,6 +273,68 @@ func TestStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrecondition(t *testing.T) {
|
||||||
|
const dir = "testdata/precondition"
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
e := &task.Executor{
|
||||||
|
Dir: dir,
|
||||||
|
Stdout: &buff,
|
||||||
|
Stderr: &buff,
|
||||||
|
Silent: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A precondition that has been met
|
||||||
|
assert.NoError(t, e.Setup())
|
||||||
|
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "foo"}))
|
||||||
|
if buff.String() != "" {
|
||||||
|
t.Errorf("Got Output when none was expected: %s", buff.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// A precondition that was not met
|
||||||
|
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "impossible"}))
|
||||||
|
|
||||||
|
if buff.String() != "1 != 0\n" {
|
||||||
|
t.Errorf("Wrong output message: %s", buff.String())
|
||||||
|
}
|
||||||
|
buff.Reset()
|
||||||
|
|
||||||
|
// Calling a task with a precondition in a dependency fails the task
|
||||||
|
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "depends_on_imposssible"}))
|
||||||
|
if buff.String() != "1 != 0\n" {
|
||||||
|
t.Errorf("Wrong output message: %s", buff.String())
|
||||||
|
}
|
||||||
|
buff.Reset()
|
||||||
|
|
||||||
|
// Calling a task with a precondition in a cmd fails the task
|
||||||
|
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "executes_failing_task_as_cmd"}))
|
||||||
|
if buff.String() != "1 != 0\n" {
|
||||||
|
t.Errorf("Wrong output message: %s", buff.String())
|
||||||
|
}
|
||||||
|
buff.Reset()
|
||||||
|
|
||||||
|
// A task with a failing precondition and ignore_errors on still fails
|
||||||
|
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "impossible_but_i_dont_care"}))
|
||||||
|
if buff.String() != "2 != 1\n" {
|
||||||
|
t.Errorf("Wrong output message: %s", buff.String())
|
||||||
|
}
|
||||||
|
buff.Reset()
|
||||||
|
|
||||||
|
// If a precondition has ignore errors, then it will allow _dependent_ tasks to execute
|
||||||
|
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "depends_on_failure_of_impossible"}))
|
||||||
|
if buff.String() != "2 != 1\ntask: optional precondition not met\n" {
|
||||||
|
t.Errorf("Wrong output message: %s", buff.String())
|
||||||
|
}
|
||||||
|
buff.Reset()
|
||||||
|
|
||||||
|
// If a precondition has ignore errors, then it will allow tasks calling it to execute
|
||||||
|
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "executes_failing_task_as_cmd_but_succeeds"}))
|
||||||
|
if buff.String() != "2 != 1\ntask: optional precondition not met\n" {
|
||||||
|
t.Errorf("Wrong output message: %s", buff.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerates(t *testing.T) {
|
func TestGenerates(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
srcTask = "sub/src.txt"
|
srcTask = "sub/src.txt"
|
||||||
|
34
testdata/precondition/Taskfile.yml
vendored
Normal file
34
testdata/precondition/Taskfile.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
foo:
|
||||||
|
precondition:
|
||||||
|
- test -f foo.txt
|
||||||
|
|
||||||
|
impossible:
|
||||||
|
precondition:
|
||||||
|
- sh: "[ 1 = 0 ]"
|
||||||
|
msg: "1 != 0"
|
||||||
|
|
||||||
|
impossible_but_i_dont_care:
|
||||||
|
precondition:
|
||||||
|
- sh: "[ 2 = 1 ]"
|
||||||
|
msg: "2 != 1"
|
||||||
|
ignore_error: true
|
||||||
|
|
||||||
|
depends_on_imposssible:
|
||||||
|
deps:
|
||||||
|
- impossible
|
||||||
|
|
||||||
|
executes_failing_task_as_cmd:
|
||||||
|
cmds:
|
||||||
|
- task: impossible
|
||||||
|
|
||||||
|
depends_on_failure_of_impossible:
|
||||||
|
deps:
|
||||||
|
- impossible_but_i_dont_care
|
||||||
|
|
||||||
|
executes_failing_task_as_cmd_but_succeeds:
|
||||||
|
cmds:
|
||||||
|
- task: impossible_but_i_dont_care
|
||||||
|
|
0
testdata/precondition/foo.txt
vendored
Normal file
0
testdata/precondition/foo.txt
vendored
Normal file
11
variables.go
11
variables.go
@ -73,6 +73,7 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
|
|||||||
IgnoreError: cmd.IgnoreError,
|
IgnoreError: cmd.IgnoreError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if len(origTask.Deps) > 0 {
|
if len(origTask.Deps) > 0 {
|
||||||
new.Deps = make([]*taskfile.Dep, len(origTask.Deps))
|
new.Deps = make([]*taskfile.Dep, len(origTask.Deps))
|
||||||
@ -83,6 +84,16 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(origTask.Precondition) > 0 {
|
||||||
|
new.Precondition = make([]*taskfile.Precondition, len(origTask.Precondition))
|
||||||
|
for i, precond := range origTask.Precondition {
|
||||||
|
new.Precondition[i] = &taskfile.Precondition{
|
||||||
|
Sh: r.Replace(precond.Sh),
|
||||||
|
Msg: r.Replace(precond.Msg),
|
||||||
|
IgnoreError: precond.IgnoreError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &new, r.Err()
|
return &new, r.Err()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user