diff --git a/pkg/cmd/project/link/link.go b/pkg/cmd/project/link/link.go index 47b26492c..f1eb1e633 100644 --- a/pkg/cmd/project/link/link.go +++ b/pkg/cmd/project/link/link.go @@ -19,8 +19,10 @@ type linkOpts struct { number int32 owner string repo string + team string projectID string repoID string + teamID string format string exporter cmdutil.Exporter } @@ -38,14 +40,23 @@ type linkProjectToRepoMutation struct { } `graphql:"linkProjectV2ToRepository(input:$input)"` } +type linkProjectToTeamMutation struct { + LinkProjectV2ToTeam struct { + Team queries.Team `graphql:"team"` + } `graphql:"linkProjectV2ToTeam(input:$input)"` +} + func NewCmdLink(f *cmdutil.Factory, runF func(config linkConfig) error) *cobra.Command { opts := linkOpts{} linkCmd := &cobra.Command{ - Short: "Link a project to a repository", + Short: "Link a project to a repository or a team", Use: "link [] [flag]", Example: heredoc.Doc(` - # link monalisa's project "1" to her repository "my_repo" - gh project link 1 --repo my_repo + # link monalisa's project 1 to her repository "my_repo" + gh project link 1 --owner monalisa --repo my_repo + + # link monalisa's project 1 to her team "my_team" + gh project link 1 --owner my_organization --team my_team `), RunE: func(cmd *cobra.Command, args []string) error { client, err := client.New(f) @@ -68,8 +79,10 @@ func NewCmdLink(f *cmdutil.Factory, runF func(config linkConfig) error) *cobra.C io: f.IOStreams, } - if config.opts.repo == "" { - return fmt.Errorf("required flag: repo") + if config.opts.repo != "" && config.opts.team != "" { + return fmt.Errorf("specify only one repo or team") + } else if config.opts.repo == "" && config.opts.team == "" { + return fmt.Errorf("specify at least one repo or team") } // allow testing of the command without actually running it @@ -82,6 +95,7 @@ func NewCmdLink(f *cmdutil.Factory, runF func(config linkConfig) error) *cobra.C linkCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.") linkCmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "The repository to be linked to this project") + linkCmd.Flags().StringVarP(&opts.team, "team", "T", "", "The team to be linked to this project") cmdutil.AddFormatFlags(linkCmd, &opts.exporter) return linkCmd @@ -106,22 +120,51 @@ func runLink(config linkConfig) error { } c := api.NewClientFromHTTP(httpClient) - repo, err := api.GitHubRepo(c, ghrepo.New(owner.Login, config.opts.repo)) - if err != nil { - return err - } - config.opts.repoID = repo.ID + if config.opts.repo != "" { + repo, err := api.GitHubRepo(c, ghrepo.New(owner.Login, config.opts.repo)) + if err != nil { + return err + } + config.opts.repoID = repo.ID - query, variable := linkRepoArgs(config) - err = config.client.Mutate("LinkProjectV2ToRepository", query, variable) - if err != nil { - return err - } + query, variable := linkRepoArgs(config) + err = config.client.Mutate("LinkProjectV2ToRepository", query, variable) + if err != nil { + return err + } - if config.opts.exporter != nil { - return config.opts.exporter.Write(config.io, query.LinkProjectV2ToRepository.Repository) + if config.opts.exporter != nil { + return config.opts.exporter.Write(config.io, query.LinkProjectV2ToRepository.Repository) + } + return printResults(config, query.LinkProjectV2ToRepository.Repository.URL) + + } else if config.opts.team != "" { + teams, err := api.OrganizationTeams(c, ghrepo.New(owner.Login, "")) + if err != nil { + return err + } + for _, team := range teams { + if team.Slug == config.opts.team { + config.opts.teamID = team.ID + break + } + } + if config.opts.teamID == "" { + return fmt.Errorf("can't find team %s", config.opts.team) + } + + query, variable := linkTeamArgs(config) + err = config.client.Mutate("LinkProjectV2ToTeam", query, variable) + if err != nil { + return err + } + + if config.opts.exporter != nil { + return config.opts.exporter.Write(config.io, query.LinkProjectV2ToTeam.Team) + } + return printResults(config, query.LinkProjectV2ToTeam.Team.URL) } - return printResults(config, query.LinkProjectV2ToRepository.Repository) + return fmt.Errorf("specify at least one repo or team") } func linkRepoArgs(config linkConfig) (*linkProjectToRepoMutation, map[string]interface{}) { @@ -133,11 +176,20 @@ func linkRepoArgs(config linkConfig) (*linkProjectToRepoMutation, map[string]int } } -func printResults(config linkConfig, repo queries.Repository) error { +func linkTeamArgs(config linkConfig) (*linkProjectToTeamMutation, map[string]interface{}) { + return &linkProjectToTeamMutation{}, map[string]interface{}{ + "input": githubv4.LinkProjectV2ToTeamInput{ + ProjectID: githubv4.ID(config.opts.projectID), + TeamID: githubv4.ID(config.opts.teamID), + }, + } +} + +func printResults(config linkConfig, url string) error { if !config.io.IsStdoutTTY() { return nil } - _, err := fmt.Fprintf(config.io.Out, "%s\n", repo.URL) + _, err := fmt.Fprintf(config.io.Out, "%s\n", url) return err } diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go index 8bfde6bfa..1edb2841a 100644 --- a/pkg/cmd/project/shared/queries/queries.go +++ b/pkg/cmd/project/shared/queries/queries.go @@ -109,6 +109,11 @@ type Repository struct { URL string } +type Team struct { + ID string + URL string +} + // Project is a ProjectV2 GraphQL object https://docs.github.com/en/graphql/reference/objects#projectv2. type Project struct { Number int32 diff --git a/pkg/cmd/project/unlink/unlink.go b/pkg/cmd/project/unlink/unlink.go index bf014fec2..ad21b9aba 100644 --- a/pkg/cmd/project/unlink/unlink.go +++ b/pkg/cmd/project/unlink/unlink.go @@ -19,8 +19,10 @@ type unlinkOpts struct { number int32 owner string repo string + team string projectID string repoID string + teamID string format string exporter cmdutil.Exporter } @@ -38,14 +40,23 @@ type unlinkProjectFromRepoMutation struct { } `graphql:"unlinkProjectV2FromRepository(input:$input)"` } +type unlinkProjectFromTeamMutation struct { + UnlinkProjectV2FromTeam struct { + Team queries.Team `graphql:"team"` + } `graphql:"unlinkProjectV2FromTeam(input:$input)"` +} + func NewCmdUnlink(f *cmdutil.Factory, runF func(config unlinkConfig) error) *cobra.Command { opts := unlinkOpts{} linkCmd := &cobra.Command{ - Short: "Unlink a project from a repository", + Short: "Unlink a project from a repository or a team", Use: "unlink [] [flag]", Example: heredoc.Doc(` - # unlink monalisa's project "1" from her repository "my_repo" - gh project unlink 1 --repo my_repo + # unlink monalisa's project 1 from her repository "my_repo" + gh project unlink 1 --owner monalisa --repo my_repo + + # unlink monalisa's project 1 from her team "my_team" + gh project unlink 1 --owner my_organization --team my_team `), RunE: func(cmd *cobra.Command, args []string) error { client, err := client.New(f) @@ -68,8 +79,10 @@ func NewCmdUnlink(f *cmdutil.Factory, runF func(config unlinkConfig) error) *cob io: f.IOStreams, } - if config.opts.repo == "" { - return fmt.Errorf("required flag: repo") + if config.opts.repo != "" && config.opts.team != "" { + return fmt.Errorf("specify only one repo or team") + } else if config.opts.repo == "" && config.opts.team == "" { + return fmt.Errorf("specify at least one repo or team") } // allow testing of the command without actually running it @@ -82,6 +95,7 @@ func NewCmdUnlink(f *cmdutil.Factory, runF func(config unlinkConfig) error) *cob linkCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.") linkCmd.Flags().StringVarP(&opts.repo, "repo", "R", "", "The repository to be unlinked from this project") + linkCmd.Flags().StringVarP(&opts.team, "team", "T", "", "The team to be unlinked from this project") cmdutil.AddFormatFlags(linkCmd, &opts.exporter) return linkCmd @@ -106,22 +120,51 @@ func runUnlink(config unlinkConfig) error { } c := api.NewClientFromHTTP(httpClient) - repo, err := api.GitHubRepo(c, ghrepo.New(owner.Login, config.opts.repo)) - if err != nil { - return err - } - config.opts.repoID = repo.ID + if config.opts.repo != "" { + repo, err := api.GitHubRepo(c, ghrepo.New(owner.Login, config.opts.repo)) + if err != nil { + return err + } + config.opts.repoID = repo.ID - query, variable := unlinkRepoArgs(config) - err = config.client.Mutate("UnlinkProjectV2FromRepository", query, variable) - if err != nil { - return err - } + query, variable := unlinkRepoArgs(config) + err = config.client.Mutate("UnlinkProjectV2FromRepository", query, variable) + if err != nil { + return err + } - if config.opts.exporter != nil { - return config.opts.exporter.Write(config.io, query.UnlinkProjectV2FromRepository.Repository) + if config.opts.exporter != nil { + return config.opts.exporter.Write(config.io, query.UnlinkProjectV2FromRepository.Repository) + } + return printResults(config, query.UnlinkProjectV2FromRepository.Repository.URL) + + } else if config.opts.team != "" { + teams, err := api.OrganizationTeams(c, ghrepo.New(owner.Login, "")) + if err != nil { + return err + } + for _, team := range teams { + if team.Slug == config.opts.team { + config.opts.teamID = team.ID + break + } + } + if config.opts.teamID == "" { + return fmt.Errorf("can't find team %s", config.opts.team) + } + + query, variable := unlinkTeamArgs(config) + err = config.client.Mutate("UnlinkProjectV2FromTeam", query, variable) + if err != nil { + return err + } + + if config.opts.exporter != nil { + return config.opts.exporter.Write(config.io, query.UnlinkProjectV2FromTeam.Team) + } + return printResults(config, query.UnlinkProjectV2FromTeam.Team.URL) } - return printResults(config, query.UnlinkProjectV2FromRepository.Repository) + return fmt.Errorf("specify at least one repo or team") } func unlinkRepoArgs(config unlinkConfig) (*unlinkProjectFromRepoMutation, map[string]interface{}) { @@ -133,11 +176,20 @@ func unlinkRepoArgs(config unlinkConfig) (*unlinkProjectFromRepoMutation, map[st } } -func printResults(config unlinkConfig, repo queries.Repository) error { +func unlinkTeamArgs(config unlinkConfig) (*unlinkProjectFromTeamMutation, map[string]interface{}) { + return &unlinkProjectFromTeamMutation{}, map[string]interface{}{ + "input": githubv4.UnlinkProjectV2FromTeamInput{ + ProjectID: githubv4.ID(config.opts.projectID), + TeamID: githubv4.ID(config.opts.teamID), + }, + } +} + +func printResults(config unlinkConfig, url string) error { if !config.io.IsStdoutTTY() { return nil } - _, err := fmt.Fprintf(config.io.Out, "%s\n", repo.URL) + _, err := fmt.Fprintf(config.io.Out, "%s\n", url) return err }