From 2691401dc6908389c7c59891ccdf320aff9fbf5f Mon Sep 17 00:00:00 2001 From: shuntaka9576 Date: Tue, 19 Mar 2024 11:46:41 +0900 Subject: [PATCH] update --- cli/view.go | 33 ++++++++++++++ cmd/p2/main.go | 65 ++++++++++++++++++-------- gh/p2_items.go | 24 ++++++++++ gh/query.go | 54 ++++++++++++++++++++++ gh/type.go | 7 +++ go.mod | 2 + go.sum | 4 ++ p2_items.go | 121 +++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 292 insertions(+), 18 deletions(-) create mode 100644 cli/view.go create mode 100644 gh/p2_items.go create mode 100644 p2_items.go diff --git a/cli/view.go b/cli/view.go new file mode 100644 index 0000000..d096c6b --- /dev/null +++ b/cli/view.go @@ -0,0 +1,33 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os" + + ghp2 "github.com/shuntaka9576/gh-p2" +) + +type ViewParamas struct { + ProjectId string + Field string +} + +func (c *Cmd) View(params *ViewParamas) error { + res, err := c.Client.GetProjectItems(&ghp2.GetProjectItemsParams{ + ProjectId: params.ProjectId, + }) + + if err != nil { + return err + } + + json, err := json.Marshal(res) + if err != nil { + return err + } + + fmt.Fprintf(os.Stdout, "%s\n", json) + + return nil +} diff --git a/cmd/p2/main.go b/cmd/p2/main.go index 4ce5e41..c9301bd 100644 --- a/cmd/p2/main.go +++ b/cmd/p2/main.go @@ -28,6 +28,10 @@ var CLI struct { Draft bool `short:"d" name:"draft" help:"Due to GitHub specifications, the --label and --repo options cannot be used together."` Assignees []string `short:"a" name:"assignees" help:"Specify the GitHub account ID to be assigned."` } `cmd:"" help:"Option to create an issue or draft issue directly in Project V2."` + View struct { + ProjectTitle string `short:"p" required:"" name:"project-title" help:"Specify the title of ProjectV2."` + Field string `short:"f" name:"Specify ProjectV2 custom fields in the format {keyName}:{valueName}. e.g. Status:Todo, Point:3, date:2022-08-29."` + } `cmd:"" help:"Option to view an issue or draft issue in Project V2. Support only SingleSelectValue."` } func main() { @@ -58,28 +62,29 @@ func main() { } var projectId string - for _, project := range res.Projects() { - if project.Title == CLI.Create.ProjectTitle { - projectId = project.Id - } - } - if projectId == "" { - fmt.Fprintf(os.Stderr, "Not found project name: %s\n", CLI.Create.ProjectTitle) - if len(res.Projects()) > 0 { - fmt.Fprintf(os.Stderr, "The following project names are available and can be specified in the project-title(-p) flag.\n") - for _, project := range res.Projects() { - fmt.Fprintf(os.Stderr, " * %s\n", project.Title) + switch kontext.Command() { + case "create": + for _, project := range res.Projects() { + if project.Title == CLI.Create.ProjectTitle { + projectId = project.Id } - } else { - fmt.Fprintf(os.Stderr, "There are no ProjectV2 resources available for this organization or user.\n") } - os.Exit(1) - } + if projectId == "" { + fmt.Fprintf(os.Stderr, "Not found project name: %s\n", CLI.Create.ProjectTitle) + if len(res.Projects()) > 0 { + fmt.Fprintf(os.Stderr, "The following project names are available and can be specified in the project-title(-p) flag.\n") + for _, project := range res.Projects() { + fmt.Fprintf(os.Stderr, " * %s\n", project.Title) + } + } else { + fmt.Fprintf(os.Stderr, "There are no ProjectV2 resources available for this organization or user.\n") + } + + os.Exit(1) + } - switch kontext.Command() { - case "create": err = c.Create(&cli.CreateParamas{ ProjectId: projectId, Title: CLI.Create.Title, @@ -90,8 +95,32 @@ func main() { Draft: CLI.Create.Draft, Assignees: CLI.Create.Assignees, }) - } + case "view": + for _, project := range res.Projects() { + if project.Title == CLI.View.ProjectTitle { + projectId = project.Id + } + } + + if projectId == "" { + fmt.Fprintf(os.Stderr, "Not found project name: %s\n", CLI.Create.ProjectTitle) + if len(res.Projects()) > 0 { + fmt.Fprintf(os.Stderr, "The following project names are available and can be specified in the project-title(-p) flag.\n") + for _, project := range res.Projects() { + fmt.Fprintf(os.Stderr, " * %s\n", project.Title) + } + } else { + fmt.Fprintf(os.Stderr, "There are no ProjectV2 resources available for this organization or user.\n") + } + os.Exit(1) + } + + err = c.View(&cli.ViewParamas{ + ProjectId: projectId, + Field: CLI.View.Field, + }) + } if err != nil { fmt.Fprintf(os.Stderr, "%s", err) diff --git a/gh/p2_items.go b/gh/p2_items.go new file mode 100644 index 0000000..b16c8e4 --- /dev/null +++ b/gh/p2_items.go @@ -0,0 +1,24 @@ +package gh + +import ( + "github.com/cli/go-gh" +) + +type GetProjectItemsParams struct { + ProjectId string + Cursor *string +} + +func GetProjectItems(params *GetProjectItemsParams) (*[]byte, error) { + ghql := "query=" + GetProjectItemsQuery(params.ProjectId, params.Cursor) + args := append(graphqlArgs, ghql) + + stdOut, _, err := gh.Exec(args...) + if err != nil { + return nil, err + } + + bytes := stdOut.Bytes() + + return &bytes, nil +} diff --git a/gh/query.go b/gh/query.go index 337459a..cfa59d0 100644 --- a/gh/query.go +++ b/gh/query.go @@ -38,6 +38,60 @@ func GetListQuery(clientType ClientType, name string) string { return query } +func GetProjectItemsQuery(projectId string, cursor *string) string { + var afterClause string + if cursor != nil { + afterClause = fmt.Sprintf(`, after: "%s"`, *cursor) + } + + query := fmt.Sprintf(`query{ + node(id: "%s") { + ... on ProjectV2 { + title + items(first: 20%s) { + pageInfo { + hasNextPage + endCursor + } + nodes { + id + createdAt + updatedAt + isArchived + content { + __typename + } + fieldValues(first: 20) { + nodes { + __typename + ... on ProjectV2ItemFieldSingleSelectValue { + name + optionId + field { + ... on ProjectV2SingleSelectField { + id + name + } + } + } + } + } + content { + ... on Issue { + title + state + url + } + } + } + } + } + } +}`, projectId, afterClause) + + return query +} + func GetProjectFieldsQuery(projectId string) string { query := fmt.Sprintf(`query{ node(id: "%s") { diff --git a/gh/type.go b/gh/type.go index ce781ae..9efe037 100644 --- a/gh/type.go +++ b/gh/type.go @@ -24,3 +24,10 @@ const ( TITLE PROJECT_V2_DATA_TYPE = "TITLE" TRACKS PROJECT_V2_DATA_TYPE = "TRACKS" ) + +type ITEM_TYPE = string + +const ( + ISSUE ITEM_TYPE = "ISSUE" + DRAFT_ISSUE ITEM_TYPE = "DRAFT_ISSUE" +) diff --git a/go.mod b/go.mod index 35e0783..071687a 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,9 @@ require ( github.com/cli/safeexec v1.0.0 // indirect github.com/cli/shurcooL-graphql v0.0.2 // indirect github.com/henvic/httpretty v0.0.6 // indirect + github.com/k0kubun/pp v3.0.1+incompatible // indirect github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/muesli/termenv v0.12.0 // indirect diff --git a/go.sum b/go.sum index 77d6782..b69bb67 100644 --- a/go.sum +++ b/go.sum @@ -26,11 +26,15 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= +github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= +github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= diff --git a/p2_items.go b/p2_items.go new file mode 100644 index 0000000..6899972 --- /dev/null +++ b/p2_items.go @@ -0,0 +1,121 @@ +package ghp2 + +import ( + "encoding/json" + + "github.com/shuntaka9576/gh-p2/gh" +) + +type GetProjectItemsParams struct { + ProjectId string +} + +type GetProjectItemsGhRes struct { + Data struct { + Node struct { + Title string `json:"title"` + Items struct { + PageInfo struct { + HasNextPage bool `json:"hasNextPage"` + EndCursor string `json:"endCursor"` + } `json:"pageInfo"` + Nodes []struct { + ID string `json:"id"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + IsArchived bool `json:"isArchived"` + Content struct { + TypeName string `json:"__typename,omitempty"` + Title string `json:"title,omitempty"` + State string `json:"state,omitempty"` + URL string `json:"url,omitempty"` + Body string `json:"body,omitempty"` + } `json:"content"` + FieldValues struct { + Nodes []struct { + TypeName string `json:"__typename,omitempty"` + Name string `json:"name,omitempty"` + Filed struct { + Name string `json:"name,omitempty"` + } `json:"field"` + } `json:"nodes"` + } `json:"fieldValues"` + } `json:"nodes"` + } `json:"items"` + } `json:"node"` + } `json:"data"` +} + +type GetProjectItemsRes struct { + Title string `json:"title"` + IssueItems []IssueItem `json:"items"` +} + +type IssueItem struct { + Title string `json:"title"` + SingleSelectValues map[string]string `json:"singleFiledValues"` + ItemType gh.ITEM_TYPE `json:"type"` + Body string `json:"body"` +} + +func (c *Client) GetProjectItems(params *GetProjectItemsParams) (*GetProjectItemsRes, error) { + var cursor *string + res := &GetProjectItemsRes{} + + for { + payload, err := gh.GetProjectItems(&gh.GetProjectItemsParams{ + ProjectId: params.ProjectId, + Cursor: cursor, + }) + if err != nil { + return nil, err + } + + parsed := &GetProjectItemsGhRes{} + err = json.Unmarshal(*payload, parsed) + if err != nil { + return nil, err + } + + if res.Title == "" { // Set the title only once + res.Title = parsed.Data.Node.Title + } + + for _, node := range parsed.Data.Node.Items.Nodes { + if node.IsArchived { + continue + } + + item := IssueItem{ + Title: node.Content.Title, + Body: node.Content.Body, + } + + switch node.Content.TypeName { + case "DraftIssue": + item.ItemType = gh.DRAFT_ISSUE + case "Issue": + item.ItemType = gh.ISSUE + default: + return nil, err + } + + singleSelectValues := map[string]string{} + for _, fieldValue := range node.FieldValues.Nodes { + if fieldValue.TypeName == "ProjectV2ItemFieldSingleSelectValue" { + singleSelectValues[fieldValue.Filed.Name] = fieldValue.Name + } + } + item.SingleSelectValues = singleSelectValues + + res.IssueItems = append(res.IssueItems, item) + } + + if !parsed.Data.Node.Items.PageInfo.HasNextPage { + break + } + cursor = &parsed.Data.Node.Items.PageInfo.EndCursor + } + + return res, nil +}