diff --git a/.gitignore b/.gitignore index 246c2621..d5340eb2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ dist/ .DS_Store + +# intellij idea/goland +.idea/ diff --git a/cmd/task/task.go b/cmd/task/task.go index 694a0a5c..2e853580 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -17,7 +17,7 @@ var ( version = "master" ) -const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [--dry] [task...] +const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [--dry] [--summary] [task...] Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was specified. @@ -56,6 +56,7 @@ func main() { verbose bool silent bool dry bool + summary bool dir string output string ) @@ -69,6 +70,7 @@ func main() { pflag.BoolVarP(&verbose, "verbose", "v", false, "enables verbose mode") pflag.BoolVarP(&silent, "silent", "s", false, "disables echoing") pflag.BoolVar(&dry, "dry", false, "compiles and prints tasks in the order that they would be run, without executing them") + pflag.BoolVar(&summary, "summary", false, "show summary about a task") pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution") pflag.StringVarP(&output, "output", "o", "", "sets output style: [interleaved|group|prefixed]") pflag.Parse() @@ -96,6 +98,7 @@ func main() { Silent: silent, Dir: dir, Dry: dry, + Summary: summary, Stdin: os.Stdin, Stdout: os.Stdout, diff --git a/docs/usage.md b/docs/usage.md index 29f55ed2..fd597ec9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -542,6 +542,51 @@ would print the following output: * test: Run all the go tests. ``` +## Display summary of task + +Running `task --summary task-name` will show a summary of a task +The following Taskfile: + +```yaml +version: '2' + +tasks: + release: + deps: [build] + summary: | + Release your project to github + + It will build your project before starting the release it. + Please make sure that you have set GITHUB_TOKEN before starting. + cmds: + - your-release-tool + + build: + cmds: + - your-build-tool +``` + +with running ``task --summary release`` would print the following output: + +``` +task: release + +Release your project to github + +It will build your project before starting the release it. +Please make sure that you have set GITHUB_TOKEN before starting. + +dependencies: + - build + +commands: + - your-release-tool +``` +If a summary is missing, the description will be printed. +If the task does not have a summary or a description, a warning is printed. + +Please note: *showing the summary will not execute the command*. + ## Silent mode Silent mode disables echoing of commands before Task runs it. diff --git a/internal/summary/summary.go b/internal/summary/summary.go new file mode 100644 index 00000000..655ec76c --- /dev/null +++ b/internal/summary/summary.go @@ -0,0 +1,103 @@ +package summary + +import ( + "strings" + + "github.com/go-task/task/v2/internal/logger" + "github.com/go-task/task/v2/internal/taskfile" +) + +func PrintTasks(l *logger.Logger, t *taskfile.Taskfile, c []taskfile.Call) { + for i, call := range c { + printSpaceBetweenSummaries(l, i) + PrintTask(l, t.Tasks[call.Task]) + } +} + +func printSpaceBetweenSummaries(l *logger.Logger, i int) { + spaceRequired := i > 0 + if !spaceRequired { + return + } + + l.Outf("") + l.Outf("") +} + +func PrintTask(l *logger.Logger, t *taskfile.Task) { + printTaskName(l, t) + printTaskDescribingText(t, l) + printTaskDependencies(l, t) + printTaskCommands(l, t) +} + +func printTaskDescribingText(t *taskfile.Task, l *logger.Logger) { + if hasSummary(t) { + printTaskSummary(l, t) + } else if hasDescription(t) { + printTaskDescription(l, t) + } else { + printNoDescriptionOrSummary(l) + } +} + +func hasSummary(t *taskfile.Task) bool { + return t.Summary != "" +} + +func printTaskSummary(l *logger.Logger, t *taskfile.Task) { + lines := strings.Split(t.Summary, "\n") + for i, line := range lines { + notLastLine := i+1 < len(lines) + if notLastLine || line != "" { + l.Outf(line) + } + } +} + +func printTaskName(l *logger.Logger, t *taskfile.Task) { + l.Outf("task: %s", t.Task) + l.Outf("") +} + +func hasDescription(t *taskfile.Task) bool { + return t.Desc != "" +} + +func printTaskDescription(l *logger.Logger, t *taskfile.Task) { + l.Outf(t.Desc) +} + +func printNoDescriptionOrSummary(l *logger.Logger) { + l.Outf("(task does not have description or summary)") +} + +func printTaskDependencies(l *logger.Logger, t *taskfile.Task) { + if len(t.Deps) == 0 { + return + } + + l.Outf("") + l.Outf("dependencies:") + + for _, d := range t.Deps { + l.Outf(" - %s", d.Task) + } +} + +func printTaskCommands(l *logger.Logger, t *taskfile.Task) { + if len(t.Cmds) == 0 { + return + } + + l.Outf("") + l.Outf("commands:") + for _, c := range t.Cmds { + isCommand := c.Cmd != "" + if isCommand { + l.Outf(" - %s", c.Cmd) + } else { + l.Outf(" - Task: %s", c.Task) + } + } +} diff --git a/internal/summary/summary_test.go b/internal/summary/summary_test.go new file mode 100644 index 00000000..95029efc --- /dev/null +++ b/internal/summary/summary_test.go @@ -0,0 +1,173 @@ +package summary_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/go-task/task/v2/internal/logger" + "github.com/go-task/task/v2/internal/summary" + "github.com/go-task/task/v2/internal/taskfile" + + "github.com/stretchr/testify/assert" +) + +func TestPrintsDependenciesIfPresent(t *testing.T) { + buffer, l := createDummyLogger() + task := &taskfile.Task{ + Deps: []*taskfile.Dep{ + {Task: "dep1"}, + {Task: "dep2"}, + {Task: "dep3"}, + }, + } + + summary.PrintTask(&l, task) + + assert.Contains(t, buffer.String(), "\ndependencies:\n - dep1\n - dep2\n - dep3\n") +} + +func createDummyLogger() (*bytes.Buffer, logger.Logger) { + buffer := &bytes.Buffer{} + l := logger.Logger{ + Stderr: buffer, + Stdout: buffer, + Verbose: false, + } + return buffer, l +} + +func TestDoesNotPrintDependenciesIfMissing(t *testing.T) { + buffer, l := createDummyLogger() + task := &taskfile.Task{ + Deps: []*taskfile.Dep{}, + } + + summary.PrintTask(&l, task) + + assert.NotContains(t, buffer.String(), "dependencies:") +} + +func TestPrintTaskName(t *testing.T) { + buffer, l := createDummyLogger() + task := &taskfile.Task{ + Task: "my-task-name", + } + + summary.PrintTask(&l, task) + + assert.Contains(t, buffer.String(), "task: my-task-name\n") +} + +func TestPrintTaskCommandsIfPresent(t *testing.T) { + buffer, l := createDummyLogger() + task := &taskfile.Task{ + Cmds: []*taskfile.Cmd{ + {Cmd: "command-1"}, + {Cmd: "command-2"}, + {Task: "task-1"}, + }, + } + + summary.PrintTask(&l, task) + + assert.Contains(t, buffer.String(), "\ncommands:\n") + assert.Contains(t, buffer.String(), "\n - command-1\n") + assert.Contains(t, buffer.String(), "\n - command-2\n") + assert.Contains(t, buffer.String(), "\n - Task: task-1\n") +} + +func TestDoesNotPrintCommandIfMissing(t *testing.T) { + buffer, l := createDummyLogger() + task := &taskfile.Task{ + Cmds: []*taskfile.Cmd{}, + } + + summary.PrintTask(&l, task) + + assert.NotContains(t, buffer.String(), "commands") +} + +func TestLayout(t *testing.T) { + buffer, l := createDummyLogger() + task := &taskfile.Task{ + Task: "sample-task", + Summary: "line1\nline2\nline3\n", + Deps: []*taskfile.Dep{ + {Task: "dependency"}, + }, + Cmds: []*taskfile.Cmd{ + {Cmd: "command"}, + }, + } + + summary.PrintTask(&l, task) + + assert.Equal(t, expectedOutput(), buffer.String()) +} + +func expectedOutput() string { + expected := `task: sample-task + +line1 +line2 +line3 + +dependencies: + - dependency + +commands: + - command +` + return expected +} + +func TestPrintDescriptionAsFallback(t *testing.T) { + buffer, l := createDummyLogger() + taskWithoutSummary := &taskfile.Task{ + Desc: "description", + } + + taskWithSummary := &taskfile.Task{ + Desc: "description", + Summary: "summary", + } + taskWithoutSummaryOrDescription := &taskfile.Task{} + + summary.PrintTask(&l, taskWithoutSummary) + + assert.Contains(t, buffer.String(), "description") + + buffer.Reset() + summary.PrintTask(&l, taskWithSummary) + + assert.NotContains(t, buffer.String(), "description") + + buffer.Reset() + summary.PrintTask(&l, taskWithoutSummaryOrDescription) + + assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n") + +} + +func TestPrintAllWithSpaces(t *testing.T) { + buffer, l := createDummyLogger() + + t1 := &taskfile.Task{Task: "t1"} + t2 := &taskfile.Task{Task: "t2"} + t3 := &taskfile.Task{Task: "t3"} + + tasks := make(taskfile.Tasks, 3) + tasks["t1"] = t1 + tasks["t2"] = t2 + tasks["t3"] = t3 + + summary.PrintTasks(&l, + &taskfile.Taskfile{Tasks: tasks}, + []taskfile.Call{{Task: "t1"}, {Task: "t2"}, {Task: "t3"}}) + + assert.True(t, strings.HasPrefix(buffer.String(), "task: t1")) + assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2") + assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t3") + +} diff --git a/internal/taskfile/task.go b/internal/taskfile/task.go index 4ebfd5e5..1afcbfa3 100644 --- a/internal/taskfile/task.go +++ b/internal/taskfile/task.go @@ -1,6 +1,6 @@ package taskfile -// Tasks representas a group of tasks +// Tasks represents a group of tasks type Tasks map[string]*Task // Task represents a task @@ -9,6 +9,7 @@ type Task struct { Cmds []*Cmd Deps []*Dep Desc string + Summary string Sources []string Generates []string Status []string diff --git a/task.go b/task.go index 1632d6d6..f890cb5e 100644 --- a/task.go +++ b/task.go @@ -13,6 +13,7 @@ import ( "github.com/go-task/task/v2/internal/execext" "github.com/go-task/task/v2/internal/logger" "github.com/go-task/task/v2/internal/output" + "github.com/go-task/task/v2/internal/summary" "github.com/go-task/task/v2/internal/taskfile" "github.com/go-task/task/v2/internal/taskfile/read" "github.com/go-task/task/v2/internal/taskfile/version" @@ -36,6 +37,7 @@ type Executor struct { Verbose bool Silent bool Dry bool + Summary bool Stdin io.Reader Stdout io.Writer @@ -62,6 +64,11 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error { } } + if e.Summary { + summary.PrintTasks(e.Logger, e.Taskfile, calls) + return nil + } + if e.Watch { return e.watchTasks(calls...) } diff --git a/task_test.go b/task_test.go index 4b3b338f..e2278dc5 100644 --- a/task_test.go +++ b/task_test.go @@ -553,3 +553,25 @@ func TestIncludesCallingRoot(t *testing.T) { } tt.Run(t) } + +func TestSummary(t *testing.T) { + const dir = "testdata/summary" + + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Stdout: &buff, + Stderr: &buff, + Summary: true, + Silent: true, + } + assert.NoError(t, e.Setup()) + assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task-with-summary"}, taskfile.Call{Task: "other-task-with-summary"})) + assert.Equal(t, readTestFixture(t, dir, "task-with-summary.txt"), buff.String()) +} + +func readTestFixture(t *testing.T, dir string, file string) string { + b, err := ioutil.ReadFile(dir + "/" + file) + assert.NoError(t, err, "error reading text fixture") + return string(b) +} diff --git a/testdata/summary/Taskfile.yml b/testdata/summary/Taskfile.yml new file mode 100644 index 00000000..5df3ef01 --- /dev/null +++ b/testdata/summary/Taskfile.yml @@ -0,0 +1,26 @@ +version: 2 + +tasks: + task-with-summary: + deps: [dependend-task-1, dependend-task-2] + summary: | + summary of task-with-summary - line 1 + line 2 + line 3 + cmds: + - echo 'task-with-summary was executed' + - echo 'another command' + - exit 0 + + other-task-with-summary: + summary: summary of other-task-with-summary + cmds: + - echo 'other-task-with-summary was executed' + + dependend-task-1: + cmds: + - echo 'dependend-task-1 was executed' + + dependend-task-2: + cmds: + - echo 'dependend-task-2 was executed' diff --git a/testdata/summary/task-with-summary.txt b/testdata/summary/task-with-summary.txt new file mode 100644 index 00000000..80711300 --- /dev/null +++ b/testdata/summary/task-with-summary.txt @@ -0,0 +1,22 @@ +task: task-with-summary + +summary of task-with-summary - line 1 +line 2 +line 3 + +dependencies: + - dependend-task-1 + - dependend-task-2 + +commands: + - echo 'task-with-summary was executed' + - echo 'another command' + - exit 0 + + +task: other-task-with-summary + +summary of other-task-with-summary + +commands: + - echo 'other-task-with-summary was executed'