-
Notifications
You must be signed in to change notification settings - Fork 8.3k
gh issue edit: actors are assignable to issues
#10960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8ebbd1d
38e52db
ee9d169
0efdfed
e0afc91
218152f
29241cb
73e5589
cff2fa7
3579282
3bed778
261297f
21bd797
1e5c3c7
dfd6007
712eeab
08cd1dc
5dc854c
da40e08
ea85b92
bcd47f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -146,6 +146,13 @@ type GitHubUser struct { | |
| Name string `json:"name"` | ||
| } | ||
|
|
||
| // Actor is a superset of User and Bot | ||
| // At the time of writing, it does not support Name. | ||
| type Actor struct { | ||
| ID string `json:"id"` | ||
| Login string `json:"login"` | ||
| } | ||
|
BagToad marked this conversation as resolved.
|
||
|
|
||
| // BranchRef is the branch name in a GitHub repository | ||
| type BranchRef struct { | ||
| Name string `json:"name"` | ||
|
|
@@ -674,26 +681,37 @@ func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Reposit | |
| } | ||
|
|
||
| type RepoMetadataResult struct { | ||
| CurrentLogin string | ||
| AssignableUsers []RepoAssignee | ||
| Labels []RepoLabel | ||
| Projects []RepoProject | ||
| ProjectsV2 []ProjectV2 | ||
| Milestones []RepoMilestone | ||
| Teams []OrgTeam | ||
| CurrentLogin string | ||
| AssignableUsers []AssignableUser | ||
| AssignableActors []AssignableActor | ||
| Labels []RepoLabel | ||
| Projects []RepoProject | ||
| ProjectsV2 []ProjectV2 | ||
| Milestones []RepoMilestone | ||
| Teams []OrgTeam | ||
| } | ||
|
|
||
| func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { | ||
| var ids []string | ||
| for _, assigneeLogin := range names { | ||
| found := false | ||
| for _, u := range m.AssignableUsers { | ||
| if strings.EqualFold(assigneeLogin, u.Login) { | ||
| ids = append(ids, u.ID) | ||
| if strings.EqualFold(assigneeLogin, u.Login()) { | ||
| ids = append(ids, u.ID()) | ||
| found = true | ||
| break | ||
| } | ||
| } | ||
|
|
||
| // Look for ID in assignable actors if not found in assignable users | ||
| for _, a := range m.AssignableActors { | ||
| if strings.EqualFold(assigneeLogin, a.Login()) { | ||
| ids = append(ids, a.ID()) | ||
| found = true | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if !found { | ||
| return nil, fmt.Errorf("'%s' not found", assigneeLogin) | ||
| } | ||
|
|
@@ -885,12 +903,13 @@ func (m *RepoMetadataResult) Merge(m2 *RepoMetadataResult) { | |
| } | ||
|
|
||
| type RepoMetadataInput struct { | ||
| Assignees bool | ||
| Reviewers bool | ||
| Labels bool | ||
| ProjectsV1 bool | ||
| ProjectsV2 bool | ||
| Milestones bool | ||
| Assignees bool | ||
| ActorAssignees bool | ||
| Reviewers bool | ||
| Labels bool | ||
| ProjectsV1 bool | ||
| ProjectsV2 bool | ||
| Milestones bool | ||
| } | ||
|
|
||
| // RepoMetadata pre-fetches the metadata for attaching to issues and pull requests | ||
|
|
@@ -899,14 +918,51 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput | |
| var g errgroup.Group | ||
|
|
||
| if input.Assignees || input.Reviewers { | ||
| g.Go(func() error { | ||
| users, err := RepoAssignableUsers(client, repo) | ||
| if err != nil { | ||
| err = fmt.Errorf("error fetching assignees: %w", err) | ||
|
|
||
| if input.ActorAssignees { | ||
|
BagToad marked this conversation as resolved.
|
||
| g.Go(func() error { | ||
| actors, err := RepoAssignableActors(client, repo) | ||
| if err != nil { | ||
| err = fmt.Errorf("error fetching assignees: %w", err) | ||
| } | ||
| result.AssignableActors = actors | ||
| return err | ||
| }) | ||
|
|
||
| // If reviewers are also requested, we still need to fetch the assignable users | ||
| // since commands use assignable users for reviewers too, | ||
| // but Actors are not supported for requesting review (need to confirm this). | ||
| // TODO KW: find out how to do this in the above query so we don't need to | ||
| // run two potentially expensive queries. When we fetch Actors, this | ||
| // should still return Users - Users are distinguishable from other Actors | ||
| // by having a name property. Maybe we can use the Name to filter out | ||
| // non-user Actors and populate the users list for reviewers based on | ||
| // that. | ||
| // Note: this only matters for `gh pr` flows, which currently does not | ||
| // request actor assignees, so we probably won't hit this until | ||
| // `gh pr` requests actor assignees. | ||
| if input.Reviewers { | ||
| g.Go(func() error { | ||
| users, err := RepoAssignableUsers(client, repo) | ||
| if err != nil { | ||
| err = fmt.Errorf("error fetching assignees: %w", err) | ||
| } | ||
| result.AssignableUsers = users | ||
| return err | ||
| }) | ||
| } | ||
| result.AssignableUsers = users | ||
| return err | ||
| }) | ||
| } else { | ||
| // Not using Actors, fetch legacy assignable users. | ||
| g.Go(func() error { | ||
| users, err := RepoAssignableUsers(client, repo) | ||
| if err != nil { | ||
| err = fmt.Errorf("error fetching assignees: %w", err) | ||
| } | ||
| result.AssignableUsers = users | ||
| return err | ||
| }) | ||
| } | ||
|
|
||
| } | ||
|
|
||
| if input.Reviewers { | ||
|
|
@@ -1070,12 +1126,16 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes | |
| result.Teams = append(result.Teams, t) | ||
| } | ||
| default: | ||
| user := RepoAssignee{} | ||
| user := struct { | ||
| Id string | ||
| Login string | ||
| Name string | ||
| }{} | ||
|
Comment on lines
+1129
to
+1133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @williammartin : what are your thoughts of working around the sealed interface, preventing us from leveraging one of the declared types for this?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a good question. Gut feeling is that I don't have a problem with it. If it mattered a lot we could implement https://pkg.go.dev/encoding/json#Unmarshaler on |
||
| err := json.Unmarshal(v, &user) | ||
| if err != nil { | ||
| return result, err | ||
| } | ||
| result.AssignableUsers = append(result.AssignableUsers, user) | ||
| result.AssignableUsers = append(result.AssignableUsers, NewAssignableUser(user.Id, user.Login, user.Name)) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1127,26 +1187,84 @@ func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) | |
| return projects, nil | ||
| } | ||
|
|
||
| type RepoAssignee struct { | ||
| ID string | ||
| Login string | ||
| Name string | ||
| type AssignableActor interface { | ||
| DisplayName() string | ||
| ID() string | ||
| Login() string | ||
|
|
||
| sealedAssignableActor() | ||
| } | ||
|
|
||
| // Always a user | ||
| type AssignableUser struct { | ||
| id string | ||
| login string | ||
| name string | ||
| } | ||
|
|
||
| func NewAssignableUser(id, login, name string) AssignableUser { | ||
| return AssignableUser{ | ||
| id: id, | ||
| login: login, | ||
| name: name, | ||
| } | ||
| } | ||
|
|
||
| // DisplayName returns a formatted string that uses Login and Name to be displayed e.g. 'Login (Name)' or 'Login' | ||
| func (ra RepoAssignee) DisplayName() string { | ||
| if ra.Name != "" { | ||
| return fmt.Sprintf("%s (%s)", ra.Login, ra.Name) | ||
| func (u AssignableUser) DisplayName() string { | ||
| if u.name != "" { | ||
| return fmt.Sprintf("%s (%s)", u.login, u.name) | ||
| } | ||
| return ra.Login | ||
| return u.login | ||
| } | ||
|
|
||
| func (u AssignableUser) ID() string { | ||
| return u.id | ||
| } | ||
|
|
||
| func (u AssignableUser) Login() string { | ||
| return u.login | ||
| } | ||
|
|
||
| func (u AssignableUser) Name() string { | ||
| return u.name | ||
| } | ||
|
|
||
| func (u AssignableUser) sealedAssignableActor() {} | ||
|
|
||
| type AssignableBot struct { | ||
| id string | ||
| login string | ||
| } | ||
|
|
||
| func (b AssignableBot) DisplayName() string { | ||
| return b.Login() | ||
| } | ||
|
|
||
| func (b AssignableBot) ID() string { | ||
| return b.id | ||
| } | ||
|
|
||
| func (b AssignableBot) Login() string { | ||
| return b.login | ||
| } | ||
|
|
||
| func (b AssignableBot) Name() string { | ||
| return "" | ||
| } | ||
|
|
||
| func (b AssignableBot) sealedAssignableActor() {} | ||
|
|
||
| // RepoAssignableUsers fetches all the assignable users for a repository | ||
| func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, error) { | ||
| func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]AssignableUser, error) { | ||
| type responseData struct { | ||
| Repository struct { | ||
| AssignableUsers struct { | ||
| Nodes []RepoAssignee | ||
| Nodes []struct { | ||
| ID string | ||
| Login string | ||
| Name string | ||
| } | ||
| PageInfo struct { | ||
| HasNextPage bool | ||
| EndCursor string | ||
|
|
@@ -1161,15 +1279,23 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, | |
| "endCursor": (*githubv4.String)(nil), | ||
| } | ||
|
|
||
| var users []RepoAssignee | ||
| var users []AssignableUser | ||
| for { | ||
| var query responseData | ||
| err := client.Query(repo.RepoHost(), "RepositoryAssignableUsers", &query, variables) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| users = append(users, query.Repository.AssignableUsers.Nodes...) | ||
| for _, node := range query.Repository.AssignableUsers.Nodes { | ||
| user := AssignableUser{ | ||
| id: node.ID, | ||
| login: node.Login, | ||
| name: node.Name, | ||
| } | ||
|
|
||
| users = append(users, user) | ||
| } | ||
| if !query.Repository.AssignableUsers.PageInfo.HasNextPage { | ||
| break | ||
| } | ||
|
|
@@ -1179,6 +1305,76 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, | |
| return users, nil | ||
| } | ||
|
|
||
| // RepoAssignableActors fetches all the assignable actors for a repository on | ||
| // GitHub hosts that support Actor assignees. | ||
| func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]AssignableActor, error) { | ||
| type assignableUser struct { | ||
| ID string | ||
| Login string | ||
| Name string | ||
| TypeName string `graphql:"__typename"` | ||
| } | ||
|
|
||
| type assignableBot struct { | ||
| ID string | ||
| Login string | ||
| TypeName string `graphql:"__typename"` | ||
| } | ||
|
|
||
| type responseData struct { | ||
| Repository struct { | ||
| SuggestedActors struct { | ||
| Nodes []struct { | ||
| User assignableUser `graphql:"... on User"` | ||
| Bot assignableBot `graphql:"... on Bot"` | ||
| } | ||
| PageInfo struct { | ||
| HasNextPage bool | ||
| EndCursor string | ||
| } | ||
| } `graphql:"suggestedActors(first: 100, after: $endCursor, capabilities: CAN_BE_ASSIGNED)"` | ||
| } `graphql:"repository(owner: $owner, name: $name)"` | ||
| } | ||
|
|
||
| variables := map[string]interface{}{ | ||
| "owner": githubv4.String(repo.RepoOwner()), | ||
| "name": githubv4.String(repo.RepoName()), | ||
| "endCursor": (*githubv4.String)(nil), | ||
| } | ||
|
|
||
| var actors []AssignableActor | ||
| for { | ||
| var query responseData | ||
| err := client.Query(repo.RepoHost(), "RepositoryAssignableActors", &query, variables) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| for _, node := range query.Repository.SuggestedActors.Nodes { | ||
| if node.User.TypeName == "User" { | ||
| actor := AssignableUser{ | ||
| id: node.User.ID, | ||
| login: node.User.Login, | ||
| name: node.User.Name, | ||
| } | ||
| actors = append(actors, actor) | ||
| } else if node.Bot.TypeName == "Bot" { | ||
| actor := AssignableBot{ | ||
| id: node.Bot.ID, | ||
| login: node.Bot.Login, | ||
| } | ||
| actors = append(actors, actor) | ||
| } | ||
| } | ||
|
|
||
| if !query.Repository.SuggestedActors.PageInfo.HasNextPage { | ||
| break | ||
| } | ||
| variables["endCursor"] = githubv4.String(query.Repository.SuggestedActors.PageInfo.EndCursor) | ||
| } | ||
| return actors, nil | ||
| } | ||
|
|
||
| type RepoLabel struct { | ||
| ID string | ||
| Name string | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.