Add clarifying comment in pkg/cmd/pr/edit/edit.go to note that missing assignee/reviewer search functions trigger a downstream fallback to legacy fetching. In pkg/cmd/pr/shared/editable.go remove a redundant line and add a TODO urging migration of non-interactive assignee updates to use the new logins input with ReplaceActorsForAssignable to avoid unnecessary fetching. These are comment and doc changes to clarify intent and future improvements.
586 lines
19 KiB
Go
586 lines
19 KiB
Go
package edit
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/api"
|
|
fd "github.com/cli/cli/v2/internal/featuredetection"
|
|
"github.com/cli/cli/v2/internal/gh"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
"github.com/cli/cli/v2/internal/prompter"
|
|
shared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/cli/cli/v2/pkg/set"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
type EditOptions struct {
|
|
HttpClient func() (*http.Client, error)
|
|
IO *iostreams.IOStreams
|
|
|
|
Finder shared.PRFinder
|
|
Surveyor Surveyor
|
|
Fetcher EditableOptionsFetcher
|
|
EditorRetriever EditorRetriever
|
|
Prompter shared.EditPrompter
|
|
Detector fd.Detector
|
|
BaseRepo func() (ghrepo.Interface, error)
|
|
|
|
SelectorArg string
|
|
Interactive bool
|
|
|
|
shared.Editable
|
|
}
|
|
|
|
func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command {
|
|
opts := &EditOptions{
|
|
IO: f.IOStreams,
|
|
HttpClient: f.HttpClient,
|
|
Surveyor: surveyor{P: f.Prompter},
|
|
Fetcher: fetcher{},
|
|
EditorRetriever: editorRetriever{config: f.Config},
|
|
Prompter: f.Prompter,
|
|
}
|
|
|
|
var bodyFile string
|
|
var removeMilestone bool
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "edit [<number> | <url> | <branch>]",
|
|
Short: "Edit a pull request",
|
|
Long: heredoc.Docf(`
|
|
Edit a pull request.
|
|
|
|
Without an argument, the pull request that belongs to the current branch
|
|
is selected.
|
|
|
|
Editing a pull request's projects requires authorization with the %[1]sproject%[1]s scope.
|
|
To authorize, run %[1]sgh auth refresh -s project%[1]s.
|
|
|
|
The %[1]s--add-assignee%[1]s and %[1]s--remove-assignee%[1]s flags both support
|
|
the following special values:
|
|
- %[1]s@me%[1]s: assign or unassign yourself
|
|
- %[1]s@copilot%[1]s: assign or unassign Copilot (not supported on GitHub Enterprise Server)
|
|
|
|
The %[1]s--add-reviewer%[1]s and %[1]s--remove-reviewer%[1]s flags support
|
|
the following special value:
|
|
- %[1]s@copilot%[1]s: request or remove review from Copilot (not supported on GitHub Enterprise Server)
|
|
`, "`"),
|
|
Example: heredoc.Doc(`
|
|
$ gh pr edit 23 --title "I found a bug" --body "Nothing works"
|
|
$ gh pr edit 23 --add-label "bug,help wanted" --remove-label "core"
|
|
$ gh pr edit 23 --add-reviewer monalisa,hubot --remove-reviewer myorg/team-name
|
|
$ gh pr edit 23 --add-reviewer "@copilot"
|
|
$ gh pr edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot
|
|
$ gh pr edit 23 --add-assignee "@copilot"
|
|
$ gh pr edit 23 --add-project "Roadmap" --remove-project v1,v2
|
|
$ gh pr edit 23 --milestone "Version 1"
|
|
$ gh pr edit 23 --remove-milestone
|
|
`),
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
opts.Finder = shared.NewFinder(f)
|
|
|
|
// support `-R, --repo` override
|
|
opts.BaseRepo = f.BaseRepo
|
|
|
|
if len(args) > 0 {
|
|
opts.SelectorArg = args[0]
|
|
}
|
|
|
|
if opts.SelectorArg != "" {
|
|
// If a URL is provided, we need to parse it to override the
|
|
// base repository, especially the hostname part. That's because
|
|
// we need a feature detector down in this command, and that
|
|
// needs to know the API host. If the command is run outside of
|
|
// a git repo, we cannot instantiate the detector unless we have
|
|
// already parsed the URL.
|
|
if baseRepo, _, _, err := shared.ParseURL(opts.SelectorArg); err == nil {
|
|
opts.BaseRepo = func() (ghrepo.Interface, error) {
|
|
return baseRepo, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
flags := cmd.Flags()
|
|
|
|
bodyProvided := flags.Changed("body")
|
|
bodyFileProvided := bodyFile != ""
|
|
|
|
if err := cmdutil.MutuallyExclusive(
|
|
"specify only one of `--body` or `--body-file`",
|
|
bodyProvided,
|
|
bodyFileProvided,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if bodyProvided || bodyFileProvided {
|
|
opts.Editable.Body.Edited = true
|
|
if bodyFileProvided {
|
|
b, err := cmdutil.ReadFile(bodyFile, opts.IO.In)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Editable.Body.Value = string(b)
|
|
}
|
|
}
|
|
|
|
if err := cmdutil.MutuallyExclusive(
|
|
"specify only one of `--milestone` or `--remove-milestone`",
|
|
flags.Changed("milestone"),
|
|
removeMilestone,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
if flags.Changed("title") {
|
|
opts.Editable.Title.Edited = true
|
|
}
|
|
if flags.Changed("body") {
|
|
opts.Editable.Body.Edited = true
|
|
}
|
|
if flags.Changed("base") {
|
|
opts.Editable.Base.Edited = true
|
|
}
|
|
if flags.Changed("add-reviewer") || flags.Changed("remove-reviewer") {
|
|
opts.Editable.Reviewers.Edited = true
|
|
}
|
|
if flags.Changed("add-assignee") || flags.Changed("remove-assignee") {
|
|
opts.Editable.Assignees.Edited = true
|
|
}
|
|
if flags.Changed("add-label") || flags.Changed("remove-label") {
|
|
opts.Editable.Labels.Edited = true
|
|
}
|
|
if flags.Changed("add-project") || flags.Changed("remove-project") {
|
|
opts.Editable.Projects.Edited = true
|
|
}
|
|
if flags.Changed("milestone") || removeMilestone {
|
|
opts.Editable.Milestone.Edited = true
|
|
|
|
// Note that when `--remove-milestone` is provided, the value of
|
|
// `opts.Editable.Milestone.Value` will automatically be empty,
|
|
// which results in milestone association removal. For reference,
|
|
// see the `Editable.MilestoneId` method.
|
|
}
|
|
|
|
if !opts.Editable.Dirty() {
|
|
opts.Interactive = true
|
|
}
|
|
|
|
if opts.Interactive && !opts.IO.CanPrompt() {
|
|
return cmdutil.FlagErrorf("--title, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively")
|
|
}
|
|
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
|
|
return editRun(opts)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVarP(&opts.Editable.Title.Value, "title", "t", "", "Set the new title.")
|
|
cmd.Flags().StringVarP(&opts.Editable.Body.Value, "body", "b", "", "Set the new body.")
|
|
cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
|
|
cmd.Flags().StringVarP(&opts.Editable.Base.Value, "base", "B", "", "Change the base `branch` for this pull request")
|
|
cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Add, "add-reviewer", nil, "Add reviewers by their `login`. Use \"@copilot\" to request review from Copilot.")
|
|
cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Remove, "remove-reviewer", nil, "Remove reviewers by their `login`. Use \"@copilot\" to remove review request from Copilot.")
|
|
cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Add, "add-assignee", nil, "Add assigned users by their `login`. Use \"@me\" to assign yourself, or \"@copilot\" to assign Copilot.")
|
|
cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Remove, "remove-assignee", nil, "Remove assigned users by their `login`. Use \"@me\" to unassign yourself, or \"@copilot\" to unassign Copilot.")
|
|
cmd.Flags().StringSliceVar(&opts.Editable.Labels.Add, "add-label", nil, "Add labels by `name`")
|
|
cmd.Flags().StringSliceVar(&opts.Editable.Labels.Remove, "remove-label", nil, "Remove labels by `name`")
|
|
cmd.Flags().StringSliceVar(&opts.Editable.Projects.Add, "add-project", nil, "Add the pull request to projects by `title`")
|
|
cmd.Flags().StringSliceVar(&opts.Editable.Projects.Remove, "remove-project", nil, "Remove the pull request from projects by `title`")
|
|
cmd.Flags().StringVarP(&opts.Editable.Milestone.Value, "milestone", "m", "", "Edit the milestone the pull request belongs to by `name`")
|
|
cmd.Flags().BoolVar(&removeMilestone, "remove-milestone", false, "Remove the milestone association from the pull request")
|
|
|
|
_ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "base")
|
|
|
|
for _, flagName := range []string{"add-reviewer", "remove-reviewer"} {
|
|
_ = cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
baseRepo, err := f.BaseRepo()
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveError
|
|
}
|
|
httpClient, err := f.HttpClient()
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveError
|
|
}
|
|
results, err := shared.RequestableReviewersForCompletion(httpClient, baseRepo)
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveError
|
|
}
|
|
return results, cobra.ShellCompDirectiveNoFileComp
|
|
})
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func editRun(opts *EditOptions) error {
|
|
httpClient, err := opts.HttpClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.Detector == nil {
|
|
baseRepo, err := opts.BaseRepo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
|
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
|
|
}
|
|
|
|
findOptions := shared.FindOptions{
|
|
Selector: opts.SelectorArg,
|
|
Fields: []string{"id", "author", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone"},
|
|
Detector: opts.Detector,
|
|
}
|
|
|
|
issueFeatures, err := opts.Detector.IssueFeatures()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if issueFeatures.ActorIsAssignable {
|
|
findOptions.Fields = append(findOptions.Fields, "assignedActors")
|
|
} else {
|
|
findOptions.Fields = append(findOptions.Fields, "assignees")
|
|
}
|
|
|
|
pr, repo, err := opts.Finder.Find(findOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
editable := opts.Editable
|
|
editable.Reviewers.Allowed = true
|
|
editable.Title.Default = pr.Title
|
|
editable.Body.Default = pr.Body
|
|
editable.Base.Default = pr.BaseRefName
|
|
editable.Reviewers.Default = pr.ReviewRequests.DisplayNames()
|
|
editable.Reviewers.DefaultLogins = pr.ReviewRequests.Logins()
|
|
if issueFeatures.ActorIsAssignable {
|
|
editable.Assignees.ActorAssignees = true
|
|
editable.Assignees.Default = pr.AssignedActors.DisplayNames()
|
|
editable.Assignees.DefaultLogins = pr.AssignedActors.Logins()
|
|
} else {
|
|
editable.Assignees.Default = pr.Assignees.Logins()
|
|
}
|
|
editable.Labels.Default = pr.Labels.Names()
|
|
editable.Projects.Default = append(pr.ProjectCards.ProjectNames(), pr.ProjectItems.ProjectTitles()...)
|
|
projectItems := map[string]string{}
|
|
for _, n := range pr.ProjectItems.Nodes {
|
|
projectItems[n.Project.ID] = n.ID
|
|
}
|
|
editable.Projects.ProjectItems = projectItems
|
|
if pr.Milestone != nil {
|
|
editable.Milestone.Default = pr.Milestone.Title
|
|
}
|
|
|
|
if opts.Interactive {
|
|
err = opts.Surveyor.FieldsToEdit(&editable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
apiClient := api.NewClientFromHTTP(httpClient)
|
|
|
|
// Wire up search functions for assignees and reviewers.
|
|
// When these aren't wired up, it triggers a downstream fallback
|
|
// to legacy reviewer/assignee fetching.
|
|
if issueFeatures.ActorIsAssignable {
|
|
editable.AssigneeSearchFunc = assigneeSearchFunc(apiClient, repo, &editable, pr.ID)
|
|
editable.ReviewerSearchFunc = reviewerSearchFunc(apiClient, repo, &editable, pr.ID)
|
|
}
|
|
|
|
opts.IO.StartProgressIndicator()
|
|
err = opts.Fetcher.EditableOptionsFetch(apiClient, repo, &editable, opts.Detector.ProjectsV1())
|
|
opts.IO.StopProgressIndicator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.Interactive {
|
|
// Remove PR author from reviewer options;
|
|
// REST API errors if author is included (GraphQL silently ignores).
|
|
if editable.Reviewers.Edited {
|
|
s := set.NewStringSet()
|
|
s.AddValues(editable.Reviewers.Options)
|
|
s.Remove(pr.Author.Login)
|
|
editable.Reviewers.Options = s.ToSlice()
|
|
}
|
|
|
|
editorCommand, err := opts.EditorRetriever.Retrieve()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = opts.Surveyor.EditFields(&editable, editorCommand)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
opts.IO.StartProgressIndicator()
|
|
err = updatePullRequest(httpClient, repo, pr.ID, pr.Number, editable)
|
|
opts.IO.StopProgressIndicator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintln(opts.IO.Out, pr.URL)
|
|
|
|
return nil
|
|
}
|
|
|
|
// assigneeSearchFunc is intended to be an arg for MultiSelectWithSearch
|
|
// to return potential assignee actors.
|
|
// It also contains an important enclosure to update the editable's
|
|
// assignable actors metadata for later ID resolution - this is required
|
|
// while we continue to use IDs for mutating assignees with the GQL API.
|
|
func assigneeSearchFunc(apiClient *api.Client, repo ghrepo.Interface, editable *shared.Editable, assignableID string) func(string) prompter.MultiSelectSearchResult {
|
|
searchFunc := func(input string) prompter.MultiSelectSearchResult {
|
|
actors, availableAssigneesCount, err := api.SuggestedAssignableActors(
|
|
apiClient,
|
|
repo,
|
|
assignableID,
|
|
input)
|
|
if err != nil {
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: nil,
|
|
Labels: nil,
|
|
MoreResults: 0,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
logins := make([]string, 0, len(actors))
|
|
displayNames := make([]string, 0, len(actors))
|
|
|
|
for _, a := range actors {
|
|
if a.Login() != "" {
|
|
logins = append(logins, a.Login())
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
if a.DisplayName() != "" {
|
|
displayNames = append(displayNames, a.DisplayName())
|
|
} else {
|
|
displayNames = append(displayNames, a.Login())
|
|
}
|
|
|
|
// Update the assignable actors metadata in the editable struct
|
|
// so that updating the PR later can resolve the actor ID.
|
|
editable.Metadata.AssignableActors = append(editable.Metadata.AssignableActors, a)
|
|
}
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: logins,
|
|
Labels: displayNames,
|
|
MoreResults: availableAssigneesCount,
|
|
Err: nil,
|
|
}
|
|
}
|
|
return searchFunc
|
|
}
|
|
|
|
// reviewerSearchFunc is intended to be an arg for MultiSelectWithSearch
|
|
// to return potential reviewer candidates (users, bots, and teams).
|
|
// It also updates the editable's metadata for later ID resolution.
|
|
func reviewerSearchFunc(apiClient *api.Client, repo ghrepo.Interface, editable *shared.Editable, prID string) func(string) prompter.MultiSelectSearchResult {
|
|
searchFunc := func(input string) prompter.MultiSelectSearchResult {
|
|
candidates, moreResults, err := api.SuggestedReviewerActors(
|
|
apiClient,
|
|
repo,
|
|
prID,
|
|
input)
|
|
if err != nil {
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: nil,
|
|
Labels: nil,
|
|
MoreResults: 0,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
keys := make([]string, 0, len(candidates))
|
|
labels := make([]string, 0, len(candidates))
|
|
|
|
for _, c := range candidates {
|
|
keys = append(keys, c.Login())
|
|
labels = append(labels, c.DisplayName())
|
|
|
|
// Update the teams metadata in the editable struct
|
|
// so that updating the PR later can resolve the team ID.
|
|
if team, ok := c.(api.ReviewerTeam); ok {
|
|
editable.Metadata.Teams = append(editable.Metadata.Teams, api.OrgTeam{
|
|
ID: "", // ID not needed for REST API reviewer mutations
|
|
Slug: team.Slug(),
|
|
})
|
|
}
|
|
}
|
|
return prompter.MultiSelectSearchResult{
|
|
Keys: keys,
|
|
Labels: labels,
|
|
MoreResults: moreResults,
|
|
Err: nil,
|
|
}
|
|
}
|
|
return searchFunc
|
|
}
|
|
|
|
func updatePullRequest(httpClient *http.Client, repo ghrepo.Interface, id string, number int, editable shared.Editable) error {
|
|
var wg errgroup.Group
|
|
wg.Go(func() error {
|
|
return shared.UpdateIssue(httpClient, repo, id, true, editable)
|
|
})
|
|
if editable.Reviewers.Edited {
|
|
wg.Go(func() error {
|
|
return updatePullRequestReviews(httpClient, repo, id, number, editable)
|
|
})
|
|
}
|
|
return wg.Wait()
|
|
}
|
|
|
|
func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, prID string, number int, editable shared.Editable) error {
|
|
if !editable.Reviewers.Edited {
|
|
return nil
|
|
}
|
|
|
|
client := api.NewClientFromHTTP(httpClient)
|
|
|
|
// Rebuild the Value slice from non-interactive flag input.
|
|
if len(editable.Reviewers.Add) != 0 || len(editable.Reviewers.Remove) != 0 {
|
|
add := editable.Reviewers.Add
|
|
remove := editable.Reviewers.Remove
|
|
|
|
// Replace @copilot with the Copilot reviewer login (only on github.com).
|
|
// Also use DefaultLogins (not Default display names) for computing the set.
|
|
var defaultLogins []string
|
|
if editable.Assignees.ActorAssignees {
|
|
copilotReplacer := shared.NewCopilotReviewerReplacer()
|
|
add = copilotReplacer.ReplaceSlice(add)
|
|
remove = copilotReplacer.ReplaceSlice(remove)
|
|
defaultLogins = editable.Reviewers.DefaultLogins
|
|
} else {
|
|
// On GHES, Default already contains logins (no display name distinction)
|
|
defaultLogins = editable.Reviewers.Default
|
|
}
|
|
|
|
s := set.NewStringSet()
|
|
s.AddValues(add)
|
|
s.AddValues(defaultLogins)
|
|
s.RemoveValues(remove)
|
|
editable.Reviewers.Value = s.ToSlice()
|
|
}
|
|
|
|
// On github.com, use the new GraphQL mutation which supports bots.
|
|
// On GHES, fall back to REST API.
|
|
if editable.Assignees.ActorAssignees {
|
|
return updatePullRequestReviewsGraphQL(client, repo, prID, editable)
|
|
}
|
|
return updatePullRequestReviewsREST(client, repo, number, editable)
|
|
}
|
|
|
|
// updatePullRequestReviewsGraphQL uses the RequestReviewsByLogin mutation.
|
|
// This mutation replaces the entire reviewer set (union: false).
|
|
func updatePullRequestReviewsGraphQL(client *api.Client, repo ghrepo.Interface, prID string, editable shared.Editable) error {
|
|
users, bots, teams := partitionReviewersByType(editable.Reviewers.Value)
|
|
return api.RequestReviewsByLogin(client, repo, prID, users, bots, teams, false)
|
|
}
|
|
|
|
// updatePullRequestReviewsREST uses the REST API to add/remove reviewers.
|
|
// This is the legacy path for GHES compatibility.
|
|
func updatePullRequestReviewsREST(client *api.Client, repo ghrepo.Interface, number int, editable shared.Editable) error {
|
|
addUsers, addBots, addTeams := partitionReviewersByType(editable.Reviewers.Value)
|
|
// REST API doesn't distinguish bots from users, so we need to combine them.
|
|
allAddUsers := append(addUsers, addBots...)
|
|
|
|
// Reviewers in Default but not in Value have been removed interactively.
|
|
var toRemove []string
|
|
for _, r := range editable.Reviewers.Default {
|
|
if !slices.Contains(editable.Reviewers.Value, r) {
|
|
toRemove = append(toRemove, r)
|
|
}
|
|
}
|
|
removeUsers, removeBots, removeTeams := partitionReviewersByType(toRemove)
|
|
allRemoveUsers := append(removeUsers, removeBots...)
|
|
|
|
wg := errgroup.Group{}
|
|
wg.Go(func() error {
|
|
return api.AddPullRequestReviews(client, repo, number, allAddUsers, addTeams)
|
|
})
|
|
wg.Go(func() error {
|
|
return api.RemovePullRequestReviews(client, repo, number, allRemoveUsers, removeTeams)
|
|
})
|
|
return wg.Wait()
|
|
}
|
|
|
|
type Surveyor interface {
|
|
FieldsToEdit(*shared.Editable) error
|
|
EditFields(*shared.Editable, string) error
|
|
}
|
|
|
|
type surveyor struct {
|
|
P shared.EditPrompter
|
|
}
|
|
|
|
func (s surveyor) FieldsToEdit(editable *shared.Editable) error {
|
|
return shared.FieldsToEditSurvey(s.P, editable)
|
|
}
|
|
|
|
func (s surveyor) EditFields(editable *shared.Editable, editorCmd string) error {
|
|
return shared.EditFieldsSurvey(s.P, editable, editorCmd)
|
|
}
|
|
|
|
type EditableOptionsFetcher interface {
|
|
EditableOptionsFetch(*api.Client, ghrepo.Interface, *shared.Editable, gh.ProjectsV1Support) error
|
|
}
|
|
|
|
type fetcher struct{}
|
|
|
|
func (f fetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable, projectsV1Support gh.ProjectsV1Support) error {
|
|
return shared.FetchOptions(client, repo, opts, projectsV1Support)
|
|
}
|
|
|
|
type EditorRetriever interface {
|
|
Retrieve() (string, error)
|
|
}
|
|
|
|
type editorRetriever struct {
|
|
config func() (gh.Config, error)
|
|
}
|
|
|
|
func (e editorRetriever) Retrieve() (string, error) {
|
|
return cmdutil.DetermineEditor(e.config)
|
|
}
|
|
|
|
// partitionReviewersByType splits reviewer identifiers into users, bots, and teams.
|
|
// Team identifiers are in the form "org/slug" and are returned as-is.
|
|
// Bot logins (currently only Copilot) are identified and returned separately.
|
|
func partitionReviewersByType(values []string) (users []string, bots []string, teams []string) {
|
|
for _, v := range values {
|
|
if v == "" {
|
|
continue
|
|
}
|
|
if strings.ContainsRune(v, '/') {
|
|
// Team: org/slug format, pass as-is
|
|
teams = append(teams, v)
|
|
} else if v == api.CopilotReviewerLogin {
|
|
bots = append(bots, v)
|
|
} else {
|
|
users = append(users, v)
|
|
}
|
|
}
|
|
return
|
|
}
|