Skip to content

Commit 9e0f62a

Browse files
committed
add VERY basic project to project sync
1 parent 13d3225 commit 9e0f62a

10 files changed

+762
-176
lines changed

cli/cmds.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package cli
33
import (
44
"fmt"
55

6-
"github.com/katbyte/ghp-repo-sync/version"
6+
"github.com/katbyte/ghp-repo-sync/version" // todo - should we rename this (again) to ghp-sync ? if it can do project <> project
77
"github.com/spf13/cobra"
88
"github.com/spf13/viper"
99
)
@@ -36,6 +36,7 @@ func Make(cmdName string) (*cobra.Command, error) {
3636

3737
root.AddCommand(&cobra.Command{
3838
Use: "version",
39+
Short: "Print the version",
3940
Args: cobra.NoArgs,
4041
SilenceErrors: true,
4142
Run: func(cmd *cobra.Command, args []string) {
@@ -45,6 +46,7 @@ func Make(cmdName string) (*cobra.Command, error) {
4546

4647
root.AddCommand(&cobra.Command{
4748
Use: "issues",
49+
Short: "Sync issues from a repo to a project",
4850
Args: cobra.NoArgs,
4951
SilenceErrors: true,
5052
PreRunE: ValidateParams([]string{"token", "repo", "project-owner", "project-number"}),
@@ -53,12 +55,22 @@ func Make(cmdName string) (*cobra.Command, error) {
5355

5456
root.AddCommand(&cobra.Command{
5557
Use: "prs",
58+
Short: "Sync PRs from a repo to a project",
5659
Args: cobra.NoArgs,
5760
SilenceErrors: true,
5861
PreRunE: ValidateParams([]string{"token", "repo", "project-owner", "project-number"}),
5962
RunE: CmdPRs,
6063
})
6164

65+
root.AddCommand(&cobra.Command{
66+
Use: "project source-project-owner source-project-number",
67+
Short: "Sync issues and PRs between two projects",
68+
Args: cobra.ExactArgs(2),
69+
SilenceErrors: true,
70+
PreRunE: ValidateParams([]string{"token", "project-owner", "project-number"}),
71+
RunE: CmdSync,
72+
})
73+
6274
// TODO add CLEAR command to reset a project?
6375

6476
if err := configureFlags(root); err != nil {

cli/cmds_issues.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,25 @@ func CmdIssues(_ *cobra.Command, _ []string) error {
1818
p := gh.NewProject(f.ProjectOwner, f.ProjectNumber, f.Token)
1919

2020
c.Printf("Looking up project details for <green>%s</>/<lightGreen>%d</>...\n", f.ProjectOwner, f.ProjectNumber)
21-
project, err := p.GetProjectDetails()
21+
project, err := p.GetProjectDetailsOld()
2222
if err != nil {
2323
c.Printf("\n\n <red>ERROR!!</> %s", err)
2424
return nil
2525
}
26-
pid := project.Data.Organization.ProjectV2.Id
26+
pid := project.Data.Organization.ProjectV2.ID
2727
c.Printf(" ID: <magenta>%s</>\n", pid)
2828

2929
statuses := map[string]string{}
3030
fields := map[string]string{}
3131

3232
for _, f := range project.Data.Organization.ProjectV2.Fields.Nodes {
33-
fields[f.Name] = f.Id
34-
c.Printf(" <lightBlue>%s</> <> <lightCyan>%s</>\n", f.Name, f.Id)
33+
fields[f.Name] = f.ID
34+
c.Printf(" <lightBlue>%s</> <> <lightCyan>%s</>\n", f.Name, f.ID)
3535

3636
if f.Name == "Status" {
3737
for _, s := range f.Options {
38-
statuses[s.Name] = s.Id
39-
c.Printf(" <blue>%s</> <> <cyan>%s</>\n", s.Name, s.Id)
38+
statuses[s.Name] = s.ID
39+
c.Printf(" <blue>%s</> <> <cyan>%s</>\n", s.Name, s.ID)
4040
}
4141
}
4242
}
@@ -107,7 +107,7 @@ func CmdIssues(_ *cobra.Command, _ []string) error {
107107
c.Printf(" open %d days\n", daysSinceCreation)
108108

109109
c.Printf(" syncing (<cyan>%s</>) to project.. ", issueNode)
110-
iid, err := p.AddToProject(pid, issueNode)
110+
iid, err := p.AddItemOld(pid, issueNode)
111111
if err != nil {
112112
c.Printf("\n\n <red>ERROR!!</> %s", err)
113113
continue

cli/cmds_project.go

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"github.com/katbyte/ghp-repo-sync/lib/gh"
6+
"github.com/spf13/cobra"
7+
"strconv"
8+
//nolint:misspell
9+
c "github.com/gookit/color"
10+
)
11+
12+
func CmdSync(_ *cobra.Command, args []string) error {
13+
f := GetFlags()
14+
15+
sourceProjectOwner := args[0]
16+
sourceProjectNumber, err := strconv.Atoi(args[1])
17+
if err != nil {
18+
c.Printf("\n\n <red>ERROR!!</> %s", err)
19+
return nil
20+
}
21+
22+
source := gh.NewProject(sourceProjectOwner, sourceProjectNumber, f.Token)
23+
destination := gh.NewProject(f.ProjectOwner, f.ProjectNumber, f.Token)
24+
25+
c.Printf("Looking up project details for <green>%s</>/<lightGreen>%d</>...\n", f.ProjectOwner, f.ProjectNumber)
26+
err = destination.LoadDetails()
27+
if err != nil {
28+
c.Printf("\n\n <red>ERROR!!</> %s", err)
29+
return nil
30+
}
31+
c.Printf(" ID: <magenta>%s</>\n", destination.ID)
32+
33+
// print the fields of the destination project
34+
for _, f := range destination.Fields {
35+
c.Printf(" <lightBlue>%s</> <> <lightCyan>%s</>\n", f.Name, f.ID)
36+
}
37+
c.Printf(" getting existing items.. ")
38+
dstItems, err := destination.GetItems()
39+
if err != nil {
40+
c.Printf("\n\n <red>ERROR!!</> %s", err)
41+
return nil
42+
}
43+
c.Printf(" <yellow>%d</>\n\n\n", len(dstItems))
44+
45+
dstItemNodeIDMap := map[string]gh.ProjectItem{}
46+
for _, item := range dstItems {
47+
dstItemNodeIDMap[item.NodeID] = item
48+
}
49+
50+
c.Printf("Getting items from source <green>%s</>/<lightGreen>%d</>...", source.Owner, source.Number)
51+
srcItems, err := source.GetItems()
52+
if err != nil {
53+
c.Printf("\n\n <red>ERROR!!</> %s", err)
54+
return nil
55+
}
56+
c.Printf(" <white>%d</>\n", len(srcItems))
57+
58+
for _, srcItem := range srcItems {
59+
c.Printf(" Item: <magenta>%s</> <lightMagenta>(%s)</> ", srcItem.ID, srcItem.NodeID)
60+
61+
// TODO filters, for now we just want to add all PRs with a due date
62+
if srcItem.DueDate == "" {
63+
c.Printf(" skipping, no due date\n")
64+
continue
65+
}
66+
67+
// parse the url (todo handle issues?)
68+
owner, name, _, number, err := gh.ParseGitHubURL(srcItem.URL)
69+
if err != nil {
70+
c.Printf("\n\n <red>ERROR!!</> parsing gh url %s: %s", srcItem.URL, err)
71+
return nil
72+
}
73+
74+
// get the pr via rest
75+
r, err := gh.NewRepo(owner+"/"+name, f.Token)
76+
if err != nil {
77+
c.Printf("\n\n <red>ERROR!!</> %s", err)
78+
return nil
79+
}
80+
81+
pr, err := r.GetPullRequest(number)
82+
if err != nil {
83+
c.Printf("\n\n <red>ERROR!!</> %s", err)
84+
return nil
85+
}
86+
87+
nodeID := *pr.NodeID
88+
dstItemId := ""
89+
c.Printf("<blue>%s</>/<lightBlue>%s</>#<lightCyan>%d</> ", owner, name, pr.GetNumber())
90+
if di, ok := dstItemNodeIDMap[nodeID]; ok {
91+
c.Printf(" already exists, ")
92+
dstItemId = di.ID
93+
} else {
94+
c.Printf(" <green>adding</> ")
95+
96+
iid, err := destination.AddItem(nodeID)
97+
if err != nil {
98+
c.Printf("\n\n <red>ERROR!!</> %s", err)
99+
continue
100+
}
101+
c.Printf("(<magenta>%s</>), setting status.. ", *iid)
102+
dstItemId = *iid
103+
104+
err = destination.SetItemStatus(dstItemId, "Unclaimed PR")
105+
if err != nil {
106+
c.Printf("\n\n <red>ERROR!!</> %s", err)
107+
continue
108+
}
109+
}
110+
111+
// update the other fields
112+
c.Printf("<blue>updating</>...")
113+
114+
//update status to "Unclaimed PR" & update request type to
115+
// TODO we can loop through the fields and build a more dynamic query from a function p.UpdateItemFields()
116+
q := `query=
117+
mutation (
118+
$project:ID!, $item:ID!,
119+
$requesttype_field:ID!, $requesttype_value:String!,
120+
$pr_field:ID!, $pr_value:String!,
121+
$user_field:ID!, $user_value:String!,
122+
$duedate_field: ID!, $duedate_value: Date!
123+
) {
124+
set_requesttype: updateProjectV2ItemFieldValue(input: {
125+
projectId: $project
126+
itemId: $item
127+
fieldId: $requesttype_field
128+
value: {
129+
text: $requesttype_value
130+
}
131+
}) {
132+
projectV2Item {
133+
id
134+
}
135+
}
136+
set_pr: updateProjectV2ItemFieldValue(input: {
137+
projectId: $project
138+
itemId: $item
139+
fieldId: $pr_field
140+
value: {
141+
text: $pr_value
142+
}
143+
}) {
144+
projectV2Item {
145+
id
146+
}
147+
}
148+
set_user: updateProjectV2ItemFieldValue(input: {
149+
projectId: $project
150+
itemId: $item
151+
fieldId: $user_field
152+
value: {
153+
text: $user_value
154+
}
155+
}) {
156+
projectV2Item {
157+
id
158+
}
159+
}
160+
set_duedate: updateProjectV2ItemFieldValue(input: {
161+
projectId: $project
162+
itemId: $item
163+
fieldId: $duedate_field
164+
value: {
165+
date: $duedate_value
166+
}
167+
}) {
168+
projectV2Item {
169+
id
170+
}
171+
}
172+
}
173+
`
174+
175+
p := [][]string{
176+
{"-f", "project=" + destination.ID},
177+
{"-f", "item=" + dstItemId},
178+
{"-f", "pr_field=" + destination.FieldIDs["PR#"]},
179+
{"-f", fmt.Sprintf("pr_value=%d", *pr.Number)}, // todo string + value
180+
{"-f", "user_field=" + destination.FieldIDs["User"]},
181+
{"-f", fmt.Sprintf("user_value=%s", pr.User.GetLogin())},
182+
{"-f", "requesttype_field=" + destination.FieldIDs["Request Type"]},
183+
{"-f", fmt.Sprintf("requesttype_value=%s", srcItem.RequestType)},
184+
{"-f", "duedate_field=" + destination.FieldIDs["Due Date"]},
185+
{"-f", fmt.Sprintf("duedate_value=%s", srcItem.DueDate)}, // Replace 'dueDate' with your due date value
186+
}
187+
188+
if !f.DryRun {
189+
out, err := r.GraphQLQuery(q, p)
190+
if err != nil {
191+
c.Printf("\n\n <red>ERROR!!</> %s\n%s", err, *out)
192+
return nil
193+
}
194+
}
195+
fmt.Println()
196+
}
197+
198+
return nil
199+
}

cli/cmds_prs.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,26 @@ func CmdPRs(_ *cobra.Command, _ []string) error {
1919
p := gh.NewProject(f.ProjectOwner, f.ProjectNumber, f.Token)
2020

2121
c.Printf("Looking up project details for <green>%s</>/<lightGreen>%d</>...\n", f.ProjectOwner, f.ProjectNumber)
22-
project, err := p.GetProjectDetails()
22+
project, err := p.GetProjectDetailsOld()
2323
if err != nil {
2424
c.Printf("\n\n <red>ERROR!!</> %s", err)
2525
return nil
2626
}
27-
pid := project.Data.Organization.ProjectV2.Id
27+
pid := project.Data.Organization.ProjectV2.ID
2828
c.Printf(" ID: <magenta>%s</>\n", pid)
2929

3030
statuses := map[string]string{}
3131
fields := map[string]string{}
3232

3333
// TODO write GetProjectFields
3434
for _, f := range project.Data.Organization.ProjectV2.Fields.Nodes {
35-
fields[f.Name] = f.Id
36-
c.Printf(" <lightBlue>%s</> <> <lightCyan>%s</>\n", f.Name, f.Id)
35+
fields[f.Name] = f.ID
36+
c.Printf(" <lightBlue>%s</> <> <lightCyan>%s</>\n", f.Name, f.ID)
3737

3838
if f.Name == "Status" {
3939
for _, s := range f.Options {
40-
statuses[s.Name] = s.Id
41-
c.Printf(" <blue>%s</> <> <cyan>%s</>\n", s.Name, s.Id)
40+
statuses[s.Name] = s.ID
41+
c.Printf(" <blue>%s</> <> <cyan>%s</>\n", s.Name, s.ID)
4242
}
4343
}
4444
}
@@ -74,7 +74,7 @@ func CmdPRs(_ *cobra.Command, _ []string) error {
7474

7575
// flat := strings.Replace(strings.Replace(q, "\n", " ", -1), "\t", "", -1)
7676
c.Printf("Syncing pr <lightCyan>%d</> (<cyan>%s</>) to project.. ", pr.GetNumber(), prNode)
77-
iid, err := p.AddToProject(pid, prNode)
77+
iid, err := p.AddItemOld(pid, prNode)
7878
if err != nil {
7979
c.Printf("\n\n <red>ERROR!!</> %s", err)
8080
continue

go.sum

-6
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
168168
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
169169
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
170170
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
171-
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
172171
github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI=
173172
github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28=
174173
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -202,7 +201,6 @@ github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw=
202201
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
203202
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
204203
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
205-
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
206204
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
207205
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
208206
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@@ -322,7 +320,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
322320
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
323321
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
324322
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
325-
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
326323
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
327324
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
328325
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -559,7 +556,6 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
559556
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
560557
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
561558
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
562-
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
563559
golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49 h1:TMjZDarEwf621XDryfitp/8awEhiZNiwgphKlTMGRIg=
564560
golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
565561
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -666,7 +662,6 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr
666662
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
667663
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
668664
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
669-
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
670665
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
671666
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
672667
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -765,7 +760,6 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
765760
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
766761
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
767762
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
768-
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
769763
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
770764
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
771765
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

0 commit comments

Comments
 (0)