Repository: go-task/task
Pull request #180 (2019-02-24 07:43:08)

Display task summary
Adding a ``--summary`` flag for task to get more a summary of the task

See #107 

New field in task called `summary`
```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
```

Output for `task --summary release`:

```
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
```

Behaviour:
- `--summary` will not execute the task
- `--summary` will display the summary of a task (see above)
- `--summary` will use description as fallback, if summary does not exist
- `--summary` will output a warning (or information), if no description/summary is present
- `--summary` will output the dependencies if there are any
- `--summary` will output the commands if there are any

Tests:
- Because there are a lot of edge-cases I decided to extract out a package (summary), which is completly unit tested with above named edge cases.
- There is an integration like test in `task_test.go`

Documentation is present for that feature


Patch:
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/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...)
 	}


Test patch:
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/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'