Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8ebbd1d
feat(fd): add ActorIsAssignable to IssueFeatures
BagToad May 7, 2025
38e52db
feat(issue edit): fetch currently assigned actors
BagToad May 8, 2025
ee9d169
feat(issue edit): fetch assignable actors
BagToad May 9, 2025
0efdfed
feat(issue edit): support assigning actors to issues
BagToad May 10, 2025
e0afc91
chore(issue edit): comments cleanup
BagToad May 10, 2025
218152f
fix(issue edit): resolve race condition in actor assignment
BagToad May 10, 2025
29241cb
refactor(issue edit): improve actor type handling
BagToad May 12, 2025
73e5589
doc(issue): comment why assignable actors disabled
BagToad May 12, 2025
cff2fa7
doc(repo queries): clarify reviewer actor fetching logic
BagToad May 12, 2025
3579282
refactor(issue edit): rename AssignedActors to ActorAssignees
BagToad May 13, 2025
3bed778
refactor(issue edit): rename loop variable for clarity
BagToad May 13, 2025
261297f
refactor(issue edit): add assignedActors to lookupFields
BagToad May 13, 2025
21bd797
fix(issue edit): use double quotes for assignedActors
BagToad May 13, 2025
1e5c3c7
fix(issue edit): revert rename of ActorAssignees
BagToad May 13, 2025
dfd6007
refactor(api): rename assignable user types and methods
BagToad May 14, 2025
712eeab
doc(api): remove needless comment
BagToad May 14, 2025
08cd1dc
feat(issue edit): replacing actor assignee is done synchronously with…
BagToad May 14, 2025
5dc854c
doc(issue edit): clarify synchronous handling of assignees
BagToad May 14, 2025
da40e08
doc(api): code comment typo
BagToad May 15, 2025
ea85b92
refactor(api): remove needless parenthesis
BagToad May 15, 2025
bcd47f1
fix(api): correct var name capitalization
BagToad May 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions api/queries_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Issue struct {
Comments Comments
Author Author
Assignees Assignees
AssignedActors AssignedActors
Labels Labels
ProjectCards ProjectCards
ProjectItems ProjectItems
Expand Down Expand Up @@ -91,6 +92,22 @@ func (a Assignees) Logins() []string {
return logins
}

type AssignedActors struct {
Edges []struct {
Node Actor
}
TotalCount int
}

// TODO kw: Display names for actors with special display names.
Comment thread
BagToad marked this conversation as resolved.
func (a AssignedActors) Logins() []string {
logins := make([]string, len(a.Edges))
for i, a := range a.Edges {
logins[i] = a.Node.Login
}
return logins
}

type Labels struct {
Nodes []IssueLabel
TotalCount int
Expand Down
268 changes: 232 additions & 36 deletions api/queries_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Comment thread
BagToad marked this conversation as resolved.

// BranchRef is the branch name in a GitHub repository
type BranchRef struct {
Name string `json:"name"`
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Comment thread
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 {
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 AssignableUser.

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))
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand Down
Loading
Loading