Repository: cli/cli
Pull request #8223 (2023-10-20 12:37:40)

Fix up NoHeaders linter errors in project commands
Follow up to #8157 

Linter exceptions were added to the project commands tests to move #8157 over the finish line, this PR just goes back through and fixes up the project commands and tests to not require the use of `tableprinter.NoHeader`. There should be no functional changes here, additionally I added tty mode tests where necessary. 


Patch:
diff --git a/pkg/cmd/project/field-list/field_list.go b/pkg/cmd/project/field-list/field_list.go
index 3e36e2a86..f15369c94 100644
--- a/pkg/cmd/project/field-list/field_list.go
+++ b/pkg/cmd/project/field-list/field_list.go
@@ -23,7 +23,6 @@ type listOpts struct {
 
 type listConfig struct {
 	io     *iostreams.IOStreams
-	tp     *tableprinter.TablePrinter
 	client *queries.Client
 	opts   listOpts
 }
@@ -52,10 +51,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
 				opts.number = int32(num)
 			}
 
-			t := tableprinter.New(f.IOStreams, tableprinter.WithHeader("Name", "Data type", "ID"))
 			config := listConfig{
 				io:     f.IOStreams,
-				tp:     t,
 				client: client,
 				opts:   opts,
 			}
@@ -109,14 +106,16 @@ func printResults(config listConfig, fields []queries.ProjectField, login string
 		return cmdutil.NewNoResultsError(fmt.Sprintf("Project %d for owner %s has no fields", config.opts.number, login))
 	}
 
+	tp := tableprinter.New(config.io, tableprinter.WithHeader("Name", "Data type", "ID"))
+
 	for _, f := range fields {
-		config.tp.AddField(f.Name())
-		config.tp.AddField(f.Type())
-		config.tp.AddField(f.ID(), tableprinter.WithTruncate(nil))
-		config.tp.EndRow()
+		tp.AddField(f.Name())
+		tp.AddField(f.Type())
+		tp.AddField(f.ID(), tableprinter.WithTruncate(nil))
+		tp.EndRow()
 	}
 
-	return config.tp.Render()
+	return tp.Render()
 }
 
 func printJSON(config listConfig, project *queries.Project) error {
diff --git a/pkg/cmd/project/item-list/item_list.go b/pkg/cmd/project/item-list/item_list.go
index f2e9bf8a6..45ca0c77f 100644
--- a/pkg/cmd/project/item-list/item_list.go
+++ b/pkg/cmd/project/item-list/item_list.go
@@ -23,7 +23,6 @@ type listOpts struct {
 
 type listConfig struct {
 	io     *iostreams.IOStreams
-	tp     *tableprinter.TablePrinter
 	client *queries.Client
 	opts   listOpts
 }
@@ -52,10 +51,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
 				opts.number = int32(num)
 			}
 
-			t := tableprinter.New(f.IOStreams, tableprinter.WithHeader("Type", "Title", "Number", "Repository", "ID"))
 			config := listConfig{
 				io:     f.IOStreams,
-				tp:     t,
 				client: client,
 				opts:   opts,
 			}
@@ -108,20 +105,22 @@ func printResults(config listConfig, items []queries.ProjectItem, login string)
 		return cmdutil.NewNoResultsError(fmt.Sprintf("Project %d for owner %s has no items", config.opts.number, login))
 	}
 
+	tp := tableprinter.New(config.io, tableprinter.WithHeader("Type", "Title", "Number", "Repository", "ID"))
+
 	for _, i := range items {
-		config.tp.AddField(i.Type())
-		config.tp.AddField(i.Title())
+		tp.AddField(i.Type())
+		tp.AddField(i.Title())
 		if i.Number() == 0 {
-			config.tp.AddField("")
+			tp.AddField("")
 		} else {
-			config.tp.AddField(strconv.Itoa(i.Number()))
+			tp.AddField(strconv.Itoa(i.Number()))
 		}
-		config.tp.AddField(i.Repo())
-		config.tp.AddField(i.ID(), tableprinter.WithTruncate(nil))
-		config.tp.EndRow()
+		tp.AddField(i.Repo())
+		tp.AddField(i.ID(), tableprinter.WithTruncate(nil))
+		tp.EndRow()
 	}
 
-	return config.tp.Render()
+	return tp.Render()
 }
 
 func printJSON(config listConfig, project *queries.Project) error {
diff --git a/pkg/cmd/project/list/list.go b/pkg/cmd/project/list/list.go
index d3168d7eb..88f419b90 100644
--- a/pkg/cmd/project/list/list.go
+++ b/pkg/cmd/project/list/list.go
@@ -23,7 +23,6 @@ type listOpts struct {
 }
 
 type listConfig struct {
-	tp        *tableprinter.TablePrinter
 	client    *queries.Client
 	opts      listOpts
 	URLOpener func(string) error
@@ -52,9 +51,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
 			URLOpener := func(url string) error {
 				return f.Browser.Browse(url)
 			}
-			t := tableprinter.New(f.IOStreams, tableprinter.WithHeader("Number", "Title", "State", "ID"))
+
 			config := listConfig{
-				tp:        t,
 				client:    client,
 				opts:      opts,
 				URLOpener: URLOpener,
@@ -68,7 +66,6 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
 			return runList(config)
 		},
 	}
-
 	listCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner")
 	listCmd.Flags().BoolVarP(&opts.closed, "closed", "", false, "Include closed projects")
 	listCmd.Flags().BoolVarP(&opts.web, "web", "w", false, "Open projects list in the browser")
@@ -157,21 +154,23 @@ func printResults(config listConfig, projects []queries.Project, owner string) e
 		return cmdutil.NewNoResultsError(fmt.Sprintf("No projects found for %s", owner))
 	}
 
+	tp := tableprinter.New(config.io, tableprinter.WithHeader("Number", "Title", "State", "ID"))
+
 	for _, p := range projects {
-		config.tp.AddField(strconv.Itoa(int(p.Number)), tableprinter.WithTruncate(nil))
-		config.tp.AddField(p.Title)
+		tp.AddField(strconv.Itoa(int(p.Number)), tableprinter.WithTruncate(nil))
+		tp.AddField(p.Title)
 		var state string
 		if p.Closed {
 			state = "closed"
 		} else {
 			state = "open"
 		}
-		config.tp.AddField(state)
-		config.tp.AddField(p.ID, tableprinter.WithTruncate(nil))
-		config.tp.EndRow()
+		tp.AddField(state)
+		tp.AddField(p.ID, tableprinter.WithTruncate(nil))
+		tp.EndRow()
 	}
 
-	return config.tp.Render()
+	return tp.Render()
 }
 
 func printJSON(config listConfig, projects []queries.Project, totalCount int) error {


Test patch:
diff --git a/pkg/cmd/project/field-list/field_list_test.go b/pkg/cmd/project/field-list/field_list_test.go
index 85e67451f..ca01599fb 100644
--- a/pkg/cmd/project/field-list/field_list_test.go
+++ b/pkg/cmd/project/field-list/field_list_test.go
@@ -3,7 +3,7 @@ package fieldlist
 import (
 	"testing"
 
-	"github.com/cli/cli/v2/internal/tableprinter"
+	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -87,6 +87,101 @@ func TestNewCmdList(t *testing.T) {
 	}
 }
 
+func TestRunList_User_tty(t *testing.T) {
+	defer gock.Off()
+	// gock.Observe(gock.DumpRequest)
+
+	// get user ID
+	gock.New("https://api.github.com").
+		Post("/graphql").
+		MatchType("json").
+		JSON(map[string]interface{}{
+			"query": "query UserOrgOwner.*",
+			"variables": map[string]interface{}{
+				"login": "monalisa",
+			},
+		}).
+		Reply(200).
+		JSON(map[string]interface{}{
+			"data": map[string]interface{}{
+				"user": map[string]interface{}{
+					"id": "an ID",
+				},
+			},
+			"errors": []interface{}{
+				map[string]interface{}{
+					"type": "NOT_FOUND",
+					"path": []string{"organization"},
+				},
+			},
+		})
+
+	// list project fields
+	gock.New("https://api.github.com").
+		Post("/graphql").
+		JSON(map[string]interface{}{
+			"query": "query UserProject.*",
+			"variables": map[string]interface{}{
+				"login":       "monalisa",
+				"number":      1,
+				"firstItems":  queries.LimitMax,
+				"afterItems":  nil,
+				"firstFields": queries.LimitDefault,
+				"afterFields": nil,
+			},
+		}).
+		Reply(200).
+		JSON(map[string]interface{}{
+			"data": map[string]interface{}{
+				"user": map[string]interface{}{
+					"projectV2": map[string]interface{}{
+						"fields": map[string]interface{}{
+							"nodes": []map[string]interface{}{
+								{
+									"__typename": "ProjectV2Field",
+									"name":       "FieldTitle",
+									"id":         "field ID",
+								},
+								{
+									"__typename": "ProjectV2SingleSelectField",
+									"name":       "Status",
+									"id":         "status ID",
+								},
+								{
+									"__typename": "ProjectV2IterationField",
+									"name":       "Iterations",
+									"id":         "iteration ID",
+								},
+							},
+						},
+					},
+				},
+			},
+		})
+
+	client := queries.NewTestClient()
+
+	ios, _, stdout, _ := iostreams.Test()
+	ios.SetStdoutTTY(true)
+	config := listConfig{
+		opts: listOpts{
+			number: 1,
+			owner:  "monalisa",
+		},
+		client: client,
+		io:     ios,
+	}
+
+	err := runList(config)
+	assert.NoError(t, err)
+	assert.Equal(t, heredoc.Doc(`
+		NAME        DATA TYPE                   ID
+		FieldTitle  ProjectV2Field              field ID
+		Status      ProjectV2SingleSelectField  status ID
+		Iterations  ProjectV2IterationField     iteration ID
+  `), stdout.String())
+}
+
 func TestRunList_User(t *testing.T) {
 	defer gock.Off()
 	// gock.Observe(gock.DumpRequest)
@@ -163,8 +258,6 @@ func TestRunList_User(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			number: 1,
 			owner:  "monalisa",
@@ -257,8 +350,6 @@ func TestRunList_Org(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			number: 1,
 			owner:  "github",
@@ -341,8 +432,6 @@ func TestRunList_Me(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			number: 1,
 			owner:  "@me",
@@ -409,8 +498,6 @@ func TestRunList_Empty(t *testing.T) {
 
 	ios, _, _, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			number: 1,
 			owner:  "@me",
diff --git a/pkg/cmd/project/item-list/item_list_test.go b/pkg/cmd/project/item-list/item_list_test.go
index c36dfc589..f637ea99b 100644
--- a/pkg/cmd/project/item-list/item_list_test.go
+++ b/pkg/cmd/project/item-list/item_list_test.go
@@ -3,12 +3,12 @@ package itemlist
 import (
 	"testing"
 
+	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
 	"github.com/google/shlex"
 
-	"github.com/cli/cli/v2/internal/tableprinter"
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/h2non/gock.v1"
 )
@@ -88,6 +88,115 @@ func TestNewCmdList(t *testing.T) {
 	}
 }
 
+func TestRunList_User_tty(t *testing.T) {
+	defer gock.Off()
+	// gock.Observe(gock.DumpRequest)
+
+	// get user ID
+	gock.New("https://api.github.com").
+		Post("/graphql").
+		MatchType("json").
+		JSON(map[string]interface{}{
+			"query": "query UserOrgOwner.*",
+			"variables": map[string]interface{}{
+				"login": "monalisa",
+			},
+		}).
+		Reply(200).
+		JSON(map[string]interface{}{
+			"data": map[string]interface{}{
+				"user": map[string]interface{}{
+					"id": "an ID",
+				},
+			},
+			"errors": []interface{}{
+				map[string]interface{}{
+					"type": "NOT_FOUND",
+					"path": []string{"organization"},
+				},
+			},
+		})
+
+	// list project items
+	gock.New("https://api.github.com").
+		Post("/graphql").
+		JSON(map[string]interface{}{
+			"query": "query UserProjectWithItems.*",
+			"variables": map[string]interface{}{
+				"firstItems":  queries.LimitDefault,
+				"afterItems":  nil,
+				"firstFields": queries.LimitMax,
+				"afterFields": nil,
+				"login":       "monalisa",
+				"number":      1,
+			},
+		}).
+		Reply(200).
+		JSON(map[string]interface{}{
+			"data": map[string]interface{}{
+				"user": map[string]interface{}{
+					"projectV2": map[string]interface{}{
+						"items": map[string]interface{}{
+							"nodes": []map[string]interface{}{
+								{
+									"id": "issue ID",
+									"content": map[string]interface{}{
+										"__typename": "Issue",
+										"title":      "an issue",
+										"number":     1,
+										"repository": map[string]string{
+											"nameWithOwner": "cli/go-gh",
+										},
+									},
+								},
+								{
+									"id": "pull request ID",
+									"content": map[string]interface{}{
+										"__typename": "PullRequest",
+										"title":      "a pull request",
+										"number":     2,
+										"repository": map[string]string{
+											"nameWithOwner": "cli/go-gh",
+										},
+									},
+								},
+								{
+									"id": "draft issue ID",
+									"content": map[string]interface{}{
+										"title":      "draft issue",
+										"__typename": "DraftIssue",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		})
+
+	client := queries.NewTestClient()
+
+	ios, _, stdout, _ := iostreams.Test()
+	ios.SetStdoutTTY(true)
+	config := listConfig{
+		opts: listOpts{
+			number: 1,
+			owner:  "monalisa",
+		},
+		client: client,
+		io:     ios,
+	}
+
+	err := runList(config)
+	assert.NoError(t, err)
+	assert.Equal(t, heredoc.Doc(`
+		TYPE         TITLE           NUMBER  REPOSITORY  ID
+		Issue        an issue        1       cli/go-gh   issue ID
+		PullRequest  a pull request  2       cli/go-gh   pull request ID
+		DraftIssue   draft issue                         draft issue ID
+  `), stdout.String())
+}
+
 func TestRunList_User(t *testing.T) {
 	defer gock.Off()
 	// gock.Observe(gock.DumpRequest)
@@ -178,8 +287,6 @@ func TestRunList_User(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			number: 1,
 			owner:  "monalisa",
@@ -286,8 +393,6 @@ func TestRunList_Org(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			number: 1,
 			owner:  "github",
@@ -384,8 +489,6 @@ func TestRunList_Me(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			number: 1,
 			owner:  "@me",
diff --git a/pkg/cmd/project/list/list_test.go b/pkg/cmd/project/list/list_test.go
index bafdf8e93..bb840caa6 100644
--- a/pkg/cmd/project/list/list_test.go
+++ b/pkg/cmd/project/list/list_test.go
@@ -4,7 +4,7 @@ import (
 	"bytes"
 	"testing"
 
-	"github.com/cli/cli/v2/internal/tableprinter"
+	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -91,6 +91,85 @@ func TestNewCmdlist(t *testing.T) {
 	}
 }
 
+func TestRunListTTY(t *testing.T) {
+	defer gock.Off()
+	gock.Observe(gock.DumpRequest)
+	// get user ID
+	gock.New("https://api.github.com").
+		Post("/graphql").
+		MatchType("json").
+		JSON(map[string]interface{}{
+			"query": "query UserOrgOwner.*",
+			"variables": map[string]interface{}{
+				"login": "monalisa",
+			},
+		}).
+		Reply(200).
+		JSON(map[string]interface{}{
+			"data": map[string]interface{}{
+				"user": map[string]interface{}{
+					"id": "an ID",
+				},
+			},
+			"errors": []interface{}{
+				map[string]interface{}{
+					"type": "NOT_FOUND",
+					"path": []string{"organization"},
+				},
+			},
+		})
+
+	gock.New("https://api.github.com").
+		Post("/graphql").
+		Reply(200).
+		JSON(map[string]interface{}{
+			"data": map[string]interface{}{
+				"user": map[string]interface{}{
+					"login": "monalisa",
+					"projectsV2": map[string]interface{}{
+						"nodes": []interface{}{
+							map[string]interface{}{
+								"title":            "Project 1",
+								"shortDescription": "Short description 1",
+								"url":              "url1",
+								"closed":           false,
+								"ID":               "id-1",
+								"number":           1,
+							},
+							map[string]interface{}{
+								"title":            "Project 2",
+								"shortDescription": "",
+								"url":              "url2",
+								"closed":           true,
+								"ID":               "id-2",
+								"number":           2,
+							},
+						},
+					},
+				},
+			},
+		})
+
+	client := queries.NewTestClient()
+
+	ios, _, stdout, _ := iostreams.Test()
+	ios.SetStdoutTTY(true)
+	config := listConfig{
+		opts: listOpts{
+			owner: "monalisa",
+		},
+		client: client,
+		io:     ios,
+	}
+
+	err := runList(config)
+	assert.NoError(t, err)
+	assert.Equal(t, heredoc.Doc(`
+		NUMBER  TITLE      STATE  ID
+		1       Project 1  open   id-1
+  `), stdout.String())
+}
+
 func TestRunList(t *testing.T) {
 	defer gock.Off()
 	gock.Observe(gock.DumpRequest)
@@ -154,8 +233,6 @@ func TestRunList(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			owner: "monalisa",
 		},
@@ -226,8 +303,6 @@ func TestRunList_Me(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			owner: "@me",
 		},
@@ -298,8 +373,6 @@ func TestRunListViewer(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp:     tableprinter.New(ios, tableprinter.NoHeader),
 		opts:   listOpts{},
 		client: client,
 		io:     ios,
@@ -377,8 +450,6 @@ func TestRunListOrg(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			owner: "github",
 		},
@@ -432,8 +503,6 @@ func TestRunListEmpty(t *testing.T) {
 
 	ios, _, _, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp:     tableprinter.New(ios, tableprinter.NoHeader),
 		opts:   listOpts{},
 		client: client,
 		io:     ios,
@@ -509,8 +578,6 @@ func TestRunListWithClosed(t *testing.T) {
 
 	ios, _, stdout, _ := iostreams.Test()
 	config := listConfig{
-		//nolint:staticcheck // SA1019: tableprinter.NoHeaders temporarily allowed in project tests.
-		tp: tableprinter.New(ios, tableprinter.NoHeader),
 		opts: listOpts{
 			owner:  "monalisa",
 			closed: true,