Merge branch 'trunk' into gh-attestation-tuf-client-retry
This commit is contained in:
commit
f2f769c23a
35 changed files with 2118 additions and 264 deletions
|
|
@ -12,7 +12,7 @@ For [installation options see below](#installation), for usage instructions [see
|
|||
|
||||
## Contributing
|
||||
|
||||
If anything feels off, or if you feel that some functionality is missing, please check out the [contributing page][contributing]. There you will find instructions for sharing your feedback, building the tool locally, and submitting pull requests to the project.
|
||||
If anything feels off or if you feel that some functionality is missing, please check out the [contributing page][contributing]. There you will find instructions for sharing your feedback, building the tool locally, and submitting pull requests to the project.
|
||||
|
||||
If you are a hubber and are interested in shipping new commands for the CLI, check out our [doc on internal contributions][intake-doc].
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ Additional Conda installation options available on the [gh-feedstock page](https
|
|||
| ----------------------------------- | ---------------- |
|
||||
| `curl -sS https://webi.sh/gh \| sh` | `webi gh@stable` |
|
||||
|
||||
For more information about the Webi installer see [its homepage](https://webinstall.dev/).
|
||||
For more information about the Webi installer, see [its homepage](https://webinstall.dev/).
|
||||
|
||||
#### Flox
|
||||
|
||||
|
|
@ -127,9 +127,9 @@ Download packaged binaries from the [releases page][].
|
|||
|
||||
#### Verification of binaries
|
||||
|
||||
Since version 2.50.0 `gh` has been producing [Build Provenance Attestation](https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/) enabling a cryptographically verifiable paper-trail back to the origin GitHub repository, git revision and build instructions used. The build provenance attestations are signed and relies on Public Good [Sigstore](https://www.sigstore.dev/) for PKI.
|
||||
Since version 2.50.0, `gh` has been producing [Build Provenance Attestation](https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/), enabling a cryptographically verifiable paper-trail back to the origin GitHub repository, git revision, and build instructions used. The build provenance attestations are signed and rely on Public Good [Sigstore](https://www.sigstore.dev/) for PKI.
|
||||
|
||||
There are two common ways to verify a downloaded release, depending if `gh` is already installed or not. If `gh` is installed, it's trivial to verify a new release:
|
||||
There are two common ways to verify a downloaded release, depending on whether `gh` is already installed or not. If `gh` is installed, it's trivial to verify a new release:
|
||||
|
||||
- **Option 1: Using `gh` if already installed:**
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ type Issue struct {
|
|||
Comments Comments
|
||||
Author Author
|
||||
Assignees Assignees
|
||||
AssignedActors AssignedActors
|
||||
Labels Labels
|
||||
ProjectCards ProjectCards
|
||||
ProjectItems ProjectItems
|
||||
|
|
@ -91,6 +92,61 @@ func (a Assignees) Logins() []string {
|
|||
return logins
|
||||
}
|
||||
|
||||
type AssignedActors struct {
|
||||
Nodes []Actor
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
func (a AssignedActors) Logins() []string {
|
||||
logins := make([]string, len(a.Nodes))
|
||||
for i, a := range a.Nodes {
|
||||
logins[i] = a.Login
|
||||
}
|
||||
return logins
|
||||
}
|
||||
|
||||
// DisplayNames returns a list of display names for the assigned actors.
|
||||
func (a AssignedActors) DisplayNames() []string {
|
||||
// These display names are used for populating the "default" assigned actors
|
||||
// from the AssignedActors type. But, this is only one piece of the puzzle
|
||||
// as later, other queries will fetch the full list of possible assignable
|
||||
// actors from the repository, and the two lists will be reconciled.
|
||||
//
|
||||
// It's important that the display names are the same between the defaults
|
||||
// (the values returned here) and the full list (the values returned by
|
||||
// other repository queries). Any discrepancy would result in an
|
||||
// "invalid default", which means an assigned actor will not be matched
|
||||
// to an assignable actor and not presented as a "default" selection.
|
||||
// Not being presented as a default would cause the actor to be potentially
|
||||
// unassigned if the edits were submitted.
|
||||
//
|
||||
// To prevent this, we need shared logic to look up an actor's display name.
|
||||
// However, our API types between assignedActors and the full list of
|
||||
// assignableActors are different. So, as an attempt to maintain
|
||||
// consistency we convert the assignedActors to the same types as the
|
||||
// repository's assignableActors, treating the assignableActors DisplayName
|
||||
// methods as the sources of truth.
|
||||
// TODO KW: make this comment less of a wall of text if needed.
|
||||
var displayNames []string
|
||||
for _, a := range a.Nodes {
|
||||
if a.TypeName == "User" {
|
||||
u := NewAssignableUser(
|
||||
a.ID,
|
||||
a.Login,
|
||||
a.Name,
|
||||
)
|
||||
displayNames = append(displayNames, u.DisplayName())
|
||||
} else if a.TypeName == "Bot" {
|
||||
b := NewAssignableBot(
|
||||
a.ID,
|
||||
a.Login,
|
||||
)
|
||||
displayNames = append(displayNames, b.DisplayName())
|
||||
}
|
||||
}
|
||||
return displayNames
|
||||
}
|
||||
|
||||
type Labels struct {
|
||||
Nodes []IssueLabel
|
||||
TotalCount int
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ type PullRequest struct {
|
|||
}
|
||||
|
||||
Assignees Assignees
|
||||
AssignedActors AssignedActors
|
||||
Labels Labels
|
||||
ProjectCards ProjectCards
|
||||
ProjectItems ProjectItems
|
||||
|
|
|
|||
|
|
@ -146,6 +146,18 @@ type GitHubUser struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Actor is a superset of User and Bot, among others.
|
||||
// At the time of writing, some of these fields
|
||||
// are not directly supported by the Actor type and
|
||||
// instead are only available on the User or Bot types
|
||||
// directly.
|
||||
type Actor struct {
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
TypeName string `json:"__typename"`
|
||||
}
|
||||
|
||||
// BranchRef is the branch name in a GitHub repository
|
||||
type BranchRef struct {
|
||||
Name string `json:"name"`
|
||||
|
|
@ -674,13 +686,14 @@ 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) {
|
||||
|
|
@ -688,12 +701,27 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) {
|
|||
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 strings.EqualFold(assigneeLogin, a.DisplayName()) {
|
||||
ids = append(ids, a.ID())
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf("'%s' not found", assigneeLogin)
|
||||
}
|
||||
|
|
@ -885,12 +913,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 +928,37 @@ 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)
|
||||
}
|
||||
result.AssignableUsers = users
|
||||
return err
|
||||
})
|
||||
if input.ActorAssignees {
|
||||
g.Go(func() error {
|
||||
actors, err := RepoAssignableActors(client, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching assignable actors: %w", err)
|
||||
}
|
||||
result.AssignableActors = actors
|
||||
|
||||
// Filter actors for users to use for pull request reviewers,
|
||||
// skip retrieving the same info through RepoAssignableUsers().
|
||||
var users []AssignableUser
|
||||
for _, a := range actors {
|
||||
if _, ok := a.(AssignableUser); !ok {
|
||||
continue
|
||||
}
|
||||
users = append(users, a.(AssignableUser))
|
||||
}
|
||||
result.AssignableUsers = users
|
||||
return nil
|
||||
})
|
||||
} 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 assignable users: %w", err)
|
||||
}
|
||||
result.AssignableUsers = users
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if input.Reviewers {
|
||||
|
|
@ -1070,12 +1122,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
|
||||
}{}
|
||||
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 +1183,99 @@ func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error)
|
|||
return projects, nil
|
||||
}
|
||||
|
||||
type RepoAssignee struct {
|
||||
ID string
|
||||
Login string
|
||||
Name string
|
||||
// Expected login for Copilot when retrieved as an Actor
|
||||
// This is returned from assignable actors and issue/pr assigned actors.
|
||||
// We use this to check if the actor is Copilot.
|
||||
const CopilotActorLogin = "copilot-swe-agent"
|
||||
|
||||
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 NewAssignableBot(id, login string) AssignableBot {
|
||||
return AssignableBot{
|
||||
id: id,
|
||||
login: login,
|
||||
}
|
||||
}
|
||||
|
||||
func (b AssignableBot) DisplayName() string {
|
||||
if b.login == CopilotActorLogin {
|
||||
return "Copilot (AI)"
|
||||
}
|
||||
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,7 +1290,7 @@ 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)
|
||||
|
|
@ -1169,7 +1298,15 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee,
|
|||
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 +1316,72 @@ 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 responseData struct {
|
||||
Repository struct {
|
||||
SuggestedActors struct {
|
||||
Nodes []struct {
|
||||
User struct {
|
||||
ID string
|
||||
Login string
|
||||
Name string
|
||||
TypeName string `graphql:"__typename"`
|
||||
} `graphql:"... on User"`
|
||||
Bot struct {
|
||||
ID string
|
||||
Login string
|
||||
TypeName string `graphql:"__typename"`
|
||||
} `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
|
||||
|
|
|
|||
|
|
@ -526,17 +526,17 @@ func Test_RepoMilestones(t *testing.T) {
|
|||
func TestDisplayName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
assignee RepoAssignee
|
||||
assignee AssignableUser
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "assignee with name",
|
||||
assignee: RepoAssignee{"123", "octocat123", "Octavious Cath"},
|
||||
assignee: AssignableUser{"123", "octocat123", "Octavious Cath"},
|
||||
want: "octocat123 (Octavious Cath)",
|
||||
},
|
||||
{
|
||||
name: "assignee without name",
|
||||
assignee: RepoAssignee{"123", "octocat123", ""},
|
||||
assignee: AssignableUser{"123", "octocat123", ""},
|
||||
want: "octocat123",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,25 @@ func shortenQuery(q string) string {
|
|||
return strings.Map(squeeze, q)
|
||||
}
|
||||
|
||||
var assignedActors = shortenQuery(`
|
||||
assignedActors(first: 10) {
|
||||
nodes {
|
||||
...on User {
|
||||
id,
|
||||
login,
|
||||
name,
|
||||
__typename
|
||||
}
|
||||
...on Bot {
|
||||
id,
|
||||
login,
|
||||
__typename
|
||||
}
|
||||
},
|
||||
totalCount
|
||||
}
|
||||
`)
|
||||
|
||||
var issueComments = shortenQuery(`
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
|
|
@ -366,6 +385,8 @@ func IssueGraphQL(fields []string) string {
|
|||
q = append(q, `headRepository{id,name}`)
|
||||
case "assignees":
|
||||
q = append(q, `assignees(first:100){nodes{id,login,name},totalCount}`)
|
||||
case "assignedActors":
|
||||
q = append(q, assignedActors)
|
||||
case "labels":
|
||||
q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`)
|
||||
case "projectCards":
|
||||
|
|
|
|||
24
go.mod
24
go.mod
|
|
@ -22,7 +22,7 @@ require (
|
|||
github.com/creack/pty v1.1.24
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.8
|
||||
github.com/gabriel-vasile/mimetype v1.4.9
|
||||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/golang/snappy v0.0.4
|
||||
github.com/google/go-cmp v0.7.0
|
||||
|
|
@ -45,17 +45,18 @@ require (
|
|||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
|
||||
github.com/sigstore/protobuf-specs v0.4.1
|
||||
github.com/sigstore/sigstore-go v0.7.4-0.20250508193737-d1f9d7fb2621
|
||||
github.com/sigstore/sigstore-go v1.0.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/theupdateframework/go-tuf/v2 v2.1.0
|
||||
github.com/theupdateframework/go-tuf/v2 v2.1.1
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
github.com/zalando/go-keyring v0.2.5
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/term v0.31.0
|
||||
golang.org/x/text v0.24.0
|
||||
google.golang.org/grpc v1.71.1
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/text v0.25.0
|
||||
google.golang.org/grpc v1.72.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
|
@ -158,7 +159,7 @@ require (
|
|||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
|
||||
github.com/sigstore/rekor v1.3.10 // indirect
|
||||
github.com/sigstore/sigstore v1.9.4 // indirect
|
||||
github.com/sigstore/timestamp-authority v1.2.6 // indirect
|
||||
github.com/sigstore/timestamp-authority v1.2.7 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
|
|
@ -172,7 +173,6 @@ require (
|
|||
github.com/transparency-dev/merkle v0.0.2 // indirect
|
||||
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
|
|
@ -183,8 +183,8 @@ require (
|
|||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
|
|
|
|||
76
go.sum
76
go.sum
|
|
@ -8,8 +8,8 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4
|
|||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs=
|
||||
cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo=
|
||||
cloud.google.com/go/kms v1.21.1 h1:r1Auo+jlfJSf8B7mUnVw5K0fI7jWyoUy65bV53VjKyk=
|
||||
cloud.google.com/go/kms v1.21.1/go.mod h1:s0wCyByc9LjTdCjG88toVs70U9W+cc6RKFc8zAqX7nE=
|
||||
cloud.google.com/go/kms v1.21.2 h1:c/PRUSMNQ8zXrc1sdAUnsenWWaNXN+PzTXfXOcSFdoE=
|
||||
cloud.google.com/go/kms v1.21.2/go.mod h1:8wkMtHV/9Z8mLXEXr1GK7xPSBdi6knuLXIhqjuWcI6w=
|
||||
cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
|
||||
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
|
|
@ -194,8 +194,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
|||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
|
||||
|
|
@ -255,8 +255,6 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
|||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
|
||||
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM=
|
||||
github.com/google/trillian v1.7.1 h1:+zX8jLM3524bAMPS+VxaDIDgsMv3/ty6DuLWerHXcek=
|
||||
github.com/google/trillian v1.7.1/go.mod h1:E1UMAHqpZCA8AQdrKdWmHmtUfSeiD0sDWD1cv00Xa+c=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
|
@ -463,18 +461,18 @@ github.com/sigstore/rekor v1.3.10 h1:/mSvRo4MZ/59ECIlARhyykAlQlkmeAQpvBPlmJtZOCU
|
|||
github.com/sigstore/rekor v1.3.10/go.mod h1:JvryKJ40O0XA48MdzYUPu0y4fyvqt0C4iSY7ri9iu3A=
|
||||
github.com/sigstore/sigstore v1.9.4 h1:64+OGed80+A4mRlNzRd055vFcgBeDghjZw24rPLZgDU=
|
||||
github.com/sigstore/sigstore v1.9.4/go.mod h1:Q7tGTC3gbtK7c3jcxEmGc2MmK4rRpIRzi3bxRFWKvEY=
|
||||
github.com/sigstore/sigstore-go v0.7.4-0.20250508193737-d1f9d7fb2621 h1:L6Z2E0TRzVdyqlUgle8l+5vOyVdOcA3o8vAKBdi+7t8=
|
||||
github.com/sigstore/sigstore-go v0.7.4-0.20250508193737-d1f9d7fb2621/go.mod h1:snTNzKeDdfjeMuaTt0gKgR3YYWvZ8yY0GIDv1SIRlHc=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.3 h1:ofTeeCNenFFqUxSziEOYh5TLMtHbHO6e8+9vT3Vf34A=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.3/go.mod h1:2D6TX/FEBMoaD86P5aYzhxRKUYPiWcOz+6EARsVnM3s=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.3 h1:2vhoi7q92JPOCrCR7AZ52lKLj1G+U+hdRnJX6/wN+qk=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.3/go.mod h1:nR4s/4sdbeHfe7RwEPL1NhwsC1ia72wDJOIMevxTMYY=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.3 h1:FtLuqkIQYvZwWWbtWHbuTbKhsILMeWnMg0VMf6xB4O4=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.3/go.mod h1:yZMHY5cEkNRkhZGGhMS6IAUgE0HcXja1xmil796wtqg=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.3 h1:f+gPRf7NVfHhJfloN672KKkNHWA7b0vAOSQZyBINHWw=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.3/go.mod h1:AjN/gspnXeMDFTOXlHzRJDs8xbkd30kH8VN9D8g4CZM=
|
||||
github.com/sigstore/timestamp-authority v1.2.6 h1:e8vcpfeeJfpkj+S6sLopbLSvqIzBB6OORslxxIN3tGI=
|
||||
github.com/sigstore/timestamp-authority v1.2.6/go.mod h1:X1NyVsRgiK8Bfguqe7Gr5ndyTOwAeGwU7l/vAjvF778=
|
||||
github.com/sigstore/sigstore-go v1.0.0 h1:4N07S2zLxf09nTRwaPKyAxbKzpM8WJYUS8lWWaYxneU=
|
||||
github.com/sigstore/sigstore-go v1.0.0/go.mod h1:UYsZ/XHE4eltv1o1Lu+n6poW1Z5to3f0+emvfXNxIN8=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.4 h1:kQqUJ1VuWdJltMkinFXAHTlJrzMRPoNgL+dy6WyJ/dA=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.4/go.mod h1:9miLz7c69vj/7VH7UpCKHDia41HCTIDJWJWf4Ex5yUk=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.4 h1:MHRm7YQuF4zFyoXRLgUdLaNxqVO6JlLGnkDUI9fm9ow=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.4/go.mod h1:899VNYSSnQ0QtcuhkW0gznzxn0cqhowTL3nzc/xnym8=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.4 h1:C2nSyTmTxpuamUmLCWWZwz+0Y1IQIig9XwAJ4UAn/SI=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.4/go.mod h1:vjDahU0sEw/WMkKkygZNH72EMg86iaFNLAaJFXhItXU=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.4 h1:t9yfb6yteIDv8CNRT6OHdqgTV6TSj+CdOtZP9dVhpsQ=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.4/go.mod h1:m7sQxVJmDa+rsmS1m6biQxaLX83pzNS7ThUEyjOqkCU=
|
||||
github.com/sigstore/timestamp-authority v1.2.7 h1:HP/VT4wnL4uzP0fVo3eHXlt0reuNgW3PLt78+BV0I5I=
|
||||
github.com/sigstore/timestamp-authority v1.2.7/go.mod h1:te4ThQ3Q/CX1bzVsf5mMN0K7Z/cgc2OcoEGxAJiFqqI=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
|
|
@ -501,14 +499,16 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
|
||||
github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.1.0 h1:NlcIR4rftUDILPMFS3TUPvB7BPw/L8E53fNlSPoT9MY=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.1.0/go.mod h1:V675cQGhZONR0OGQ8r1feO0uwtsTBYPDWHzAAPn5rjE=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.1.1 h1:OWcoHItwsGO+7m0wLa7FDWPR4oB1cj0zOr1kosE4G+I=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.1.1/go.mod h1:V675cQGhZONR0OGQ8r1feO0uwtsTBYPDWHzAAPn5rjE=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI=
|
||||
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis=
|
||||
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0=
|
||||
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw=
|
||||
github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0 h1:6nAX1aRGnkg2SEUMwO5toB2tQkP0Jd6cbmZ/K5Le1V0=
|
||||
github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0/go.mod h1:HOC5NWW1wBI2Vke1FGcRBvDATkEYE7AUDiYbXqi2sBw=
|
||||
github.com/tink-crypto/tink-go/v2 v2.4.0 h1:8VPZeZI4EeZ8P/vB6SIkhlStrJfivTJn+cQ4dtyHNh0=
|
||||
github.com/tink-crypto/tink-go/v2 v2.4.0/go.mod h1:l//evrF2Y3MjdbpNDNGnKgCpo5zSmvUvnQ4MU+yE2sw=
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
|
||||
|
|
@ -545,8 +545,8 @@ go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce
|
|||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.step.sm/crypto v0.61.0 h1:rW7He7LCzhOFn9JIf/XzgTjt4Djpf1KhdXHfbXUVFpY=
|
||||
go.step.sm/crypto v0.61.0/go.mod h1:rYubsWIX9j9xzi/aXXr2eFSzoTN3sklTAxJYucBqZaY=
|
||||
go.step.sm/crypto v0.63.0 h1:U1QGELQqJ85oDfeNFE2V52cow1rvy0m3MekG3wFmyXY=
|
||||
go.step.sm/crypto v0.63.0/go.mod h1:aj3LETmCZeSil1DMq3BlbhDBcN86+mmKrHZtXWyc0L4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
|
|
@ -555,8 +555,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
|||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
|
|
@ -565,14 +565,14 @@ golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -583,19 +583,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
@ -604,16 +604,16 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8=
|
||||
google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
|
||||
google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM=
|
||||
google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ=
|
||||
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
|
||||
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -142,8 +142,7 @@ func genMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
|
|||
fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine())
|
||||
}
|
||||
if hasLong {
|
||||
longWithEscapedPipe := strings.ReplaceAll(cmd.Long, "|", "|")
|
||||
fmt.Fprintf(w, "%s\n\n", longWithEscapedPipe)
|
||||
fmt.Fprintf(w, "%s\n\n", cmd.Long)
|
||||
}
|
||||
|
||||
for _, g := range root.GroupedCommands(cmd) {
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ type Detector interface {
|
|||
}
|
||||
|
||||
type IssueFeatures struct {
|
||||
StateReason bool
|
||||
StateReason bool
|
||||
ActorIsAssignable bool
|
||||
}
|
||||
|
||||
var allIssueFeatures = IssueFeatures{
|
||||
StateReason: true,
|
||||
StateReason: true,
|
||||
ActorIsAssignable: true,
|
||||
}
|
||||
|
||||
type PullRequestFeatures struct {
|
||||
|
|
@ -70,7 +72,8 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
|
|||
}
|
||||
|
||||
features := IssueFeatures{
|
||||
StateReason: false,
|
||||
StateReason: false,
|
||||
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
|
||||
}
|
||||
|
||||
var featureDetection struct {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ func TestIssueFeatures(t *testing.T) {
|
|||
name: "github.com",
|
||||
hostname: "github.com",
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: true,
|
||||
StateReason: true,
|
||||
ActorIsAssignable: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -31,7 +32,8 @@ func TestIssueFeatures(t *testing.T) {
|
|||
name: "ghec data residency (ghe.com)",
|
||||
hostname: "stampname.ghe.com",
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: true,
|
||||
StateReason: true,
|
||||
ActorIsAssignable: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -42,7 +44,8 @@ func TestIssueFeatures(t *testing.T) {
|
|||
`query Issue_fields\b`: `{"data": {}}`,
|
||||
},
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: false,
|
||||
StateReason: false,
|
||||
ActorIsAssignable: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package prompter_test
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -33,7 +34,7 @@ import (
|
|||
// but doesn't mandate that prompts always look exactly the same.
|
||||
func TestAccessiblePrompter(t *testing.T) {
|
||||
|
||||
beforePasswordSendTimeout := 20 * time.Microsecond
|
||||
beforePasswordSendTimeout := 100 * time.Microsecond
|
||||
|
||||
t.Run("Select", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
|
|
@ -54,6 +55,73 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
assert.Equal(t, 0, selectValue)
|
||||
})
|
||||
|
||||
t.Run("Select - blank input returns default value", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValue := "12345abcdefg"
|
||||
options := []string{"1", "2", dummyDefaultValue}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Input a number between 1 and 3:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Just press enter to accept the default
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
selectValue, err := p.Select("Select a number", dummyDefaultValue, options)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedIndex := slices.Index(options, dummyDefaultValue)
|
||||
assert.Equal(t, expectedIndex, selectValue)
|
||||
})
|
||||
|
||||
t.Run("Select - default value is in prompt and in readable format", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValue := "12345abcdefg"
|
||||
options := []string{"1", "2", dummyDefaultValue}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select a number (default: 12345abcdefg)")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Just press enter to accept the default
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
selectValue, err := p.Select("Select a number", dummyDefaultValue, options)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedIndex := slices.Index(options, dummyDefaultValue)
|
||||
assert.Equal(t, expectedIndex, selectValue)
|
||||
})
|
||||
|
||||
t.Run("Select - invalid defaults are excluded from prompt", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValue := "foo"
|
||||
options := []string{"1", "2"}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear without the invalid default value
|
||||
_, err := console.ExpectString("Select a number \r\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select option 2
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
selectValue, err := p.Select("Select a number", dummyDefaultValue, options)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, selectValue)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
|
@ -100,6 +168,62 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
assert.Equal(t, []int{1}, multiSelectValue)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect - default value is in prompt and in readable format", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValues := []string{"foo", "bar"}
|
||||
options := []string{"1", "2"}
|
||||
options = append(options, dummyDefaultValues...)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select a number (defaults: foo, bar)")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Don't select anything because the defaults should be selected.
|
||||
|
||||
// This confirms selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
multiSelectValues, err := p.MultiSelect("Select a number", dummyDefaultValues, options)
|
||||
require.NoError(t, err)
|
||||
var expectedIndices []int
|
||||
|
||||
// Get the indices of the default values within the options slice
|
||||
// as that's what we expect the prompter to return when no selections are made.
|
||||
for _, defaultValue := range dummyDefaultValues {
|
||||
expectedIndices = append(expectedIndices, slices.Index(options, defaultValue))
|
||||
}
|
||||
assert.Equal(t, expectedIndices, multiSelectValues)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect - invalid defaults are excluded from prompt", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValues := []string{"foo", "bar"}
|
||||
options := []string{"1", "2"}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear without the invalid default values
|
||||
_, err := console.ExpectString("Select a number \r\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Not selecting anything will fail because there are no defaults.
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// This confirms selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
multiSelectValues, err := p.MultiSelect("Select a number", dummyDefaultValues, options)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1}, multiSelectValues)
|
||||
})
|
||||
|
||||
t.Run("Input", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
|
@ -140,6 +264,26 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
assert.Equal(t, dummyDefaultValue, inputValue)
|
||||
})
|
||||
|
||||
t.Run("Input - default value is in prompt and in readable format", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValue := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Enter some characters (default: 12345abcdefg)")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter nothing
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
inputValue, err := p.Input("Enter some characters", dummyDefaultValue)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, dummyDefaultValue, inputValue)
|
||||
})
|
||||
|
||||
t.Run("Password", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
|
@ -164,7 +308,12 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
// Ensure the dummy password is not printed to the screen,
|
||||
// asserting that echo mode is disabled.
|
||||
_, err = console.ExpectString(" \r\n\r\n")
|
||||
//
|
||||
// Note that since console.ExpectString returns successful if the
|
||||
// expected string matches any part of the stream, we have to use an
|
||||
// anchored regexp (i.e., with ^ and $) to make sure the password/token
|
||||
// is not printed at all.
|
||||
_, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$"))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
|
|
@ -206,6 +355,26 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
require.Equal(t, false, confirmValue)
|
||||
})
|
||||
|
||||
t.Run("Confirm - default value is in prompt and in readable format", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
defaultValue := true
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Are you sure (default: yes)")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter nothing
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
confirmValue, err := p.Confirm("Are you sure", defaultValue)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, defaultValue, confirmValue)
|
||||
})
|
||||
|
||||
t.Run("AuthToken", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
|
@ -230,7 +399,12 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
// Ensure the dummy password is not printed to the screen,
|
||||
// asserting that echo mode is disabled.
|
||||
_, err = console.ExpectString(" \r\n\r\n")
|
||||
//
|
||||
// Note that since console.ExpectString returns successful if the
|
||||
// expected string matches any part of the stream, we have to use an
|
||||
// anchored regexp (i.e., with ^ and $) to make sure the password/token
|
||||
// is not printed at all.
|
||||
_, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$"))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
|
|
@ -252,6 +426,10 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
_, err = console.ExpectString("token is required")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the retry prompt
|
||||
_, err = console.ExpectString("Paste your authentication token:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait to ensure huh has time to set the echo mode
|
||||
time.Sleep(beforePasswordSendTimeout)
|
||||
|
||||
|
|
@ -266,7 +444,12 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
// Ensure the dummy password is not printed to the screen,
|
||||
// asserting that echo mode is disabled.
|
||||
_, err = console.ExpectString(" \r\n\r\n")
|
||||
//
|
||||
// Note that since console.ExpectString returns successful if the
|
||||
// expected string matches any part of the stream, we have to use an
|
||||
// anchored regexp (i.e., with ^ and $) to make sure the password/token
|
||||
// is not printed at all.
|
||||
_, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$"))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -77,10 +77,40 @@ func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form {
|
|||
WithOutput(p.stdout)
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, error) {
|
||||
// addDefaultsToPrompt adds default values to the prompt string.
|
||||
func (p *accessiblePrompter) addDefaultsToPrompt(prompt string, defaultValues []string) string {
|
||||
// Removing empty defaults from the slice.
|
||||
defaultValues = slices.DeleteFunc(defaultValues, func(s string) bool {
|
||||
return s == ""
|
||||
})
|
||||
|
||||
// Pluralizing the prompt if there are multiple default values.
|
||||
if len(defaultValues) == 1 {
|
||||
prompt = fmt.Sprintf("%s (default: %s)", prompt, defaultValues[0])
|
||||
} else if len(defaultValues) > 1 {
|
||||
prompt = fmt.Sprintf("%s (defaults: %s)", prompt, strings.Join(defaultValues, ", "))
|
||||
}
|
||||
|
||||
// Zero-length defaultValues means return prompt unchanged.
|
||||
return prompt
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) Select(prompt, defaultValue string, options []string) (int, error) {
|
||||
var result int
|
||||
|
||||
// Remove invalid default values from the defaults slice.
|
||||
if !slices.Contains(options, defaultValue) {
|
||||
defaultValue = ""
|
||||
}
|
||||
|
||||
prompt = p.addDefaultsToPrompt(prompt, []string{defaultValue})
|
||||
formOptions := []huh.Option[int]{}
|
||||
for i, o := range options {
|
||||
// If this option is the default value, assign its index
|
||||
// to the result variable. huh will treat it as a default selection.
|
||||
if defaultValue == o {
|
||||
result = i
|
||||
}
|
||||
formOptions = append(formOptions, huh.NewOption(o, i))
|
||||
}
|
||||
|
||||
|
|
@ -99,12 +129,18 @@ func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, er
|
|||
|
||||
func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
|
||||
var result []int
|
||||
|
||||
// Remove invalid default values from the defaults slice.
|
||||
defaults = slices.DeleteFunc(defaults, func(s string) bool {
|
||||
return !slices.Contains(options, s)
|
||||
})
|
||||
|
||||
prompt = p.addDefaultsToPrompt(prompt, defaults)
|
||||
formOptions := make([]huh.Option[int], len(options))
|
||||
for i, o := range options {
|
||||
// If this option is in the defaults slice,
|
||||
// let's add its index to the result slice and huh
|
||||
// will treat it as a default selection.
|
||||
// TODO: does an invalid default value constitute a panic?
|
||||
if slices.Contains(defaults, o) {
|
||||
result = append(result, i)
|
||||
}
|
||||
|
|
@ -131,7 +167,7 @@ func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, optio
|
|||
|
||||
func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) {
|
||||
result := defaultValue
|
||||
prompt = fmt.Sprintf("%s (%s)", prompt, defaultValue)
|
||||
prompt = p.addDefaultsToPrompt(prompt, []string{defaultValue})
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
|
|
@ -167,6 +203,13 @@ func (p *accessiblePrompter) Password(prompt string) (string, error) {
|
|||
|
||||
func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
|
||||
result := defaultValue
|
||||
|
||||
if defaultValue {
|
||||
prompt = p.addDefaultsToPrompt(prompt, []string{"yes"})
|
||||
} else {
|
||||
prompt = p.addDefaultsToPrompt(prompt, []string{"no"})
|
||||
}
|
||||
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
|
|
@ -174,6 +217,7 @@ func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, er
|
|||
Value(&result),
|
||||
),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ type SigstoreVerifier interface {
|
|||
type LiveSigstoreVerifier struct {
|
||||
Logger *io.Handler
|
||||
NoPublicGood bool
|
||||
PublicGood *verify.SignedEntityVerifier
|
||||
GitHub *verify.SignedEntityVerifier
|
||||
Custom map[string]*verify.SignedEntityVerifier
|
||||
PublicGood *verify.Verifier
|
||||
GitHub *verify.Verifier
|
||||
Custom map[string]*verify.Verifier
|
||||
}
|
||||
|
||||
var ErrNoAttestationsVerified = errors.New("no attestations were verified")
|
||||
|
|
@ -88,13 +88,13 @@ func NewLiveSigstoreVerifier(config SigstoreConfig) (*LiveSigstoreVerifier, erro
|
|||
return liveVerifier, nil
|
||||
}
|
||||
|
||||
func createCustomVerifiers(trustedRoot string, noPublicGood bool) (map[string]*verify.SignedEntityVerifier, error) {
|
||||
func createCustomVerifiers(trustedRoot string, noPublicGood bool) (map[string]*verify.Verifier, error) {
|
||||
customTrustRoots, err := os.ReadFile(trustedRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read file %s: %v", trustedRoot, err)
|
||||
}
|
||||
|
||||
verifiers := make(map[string]*verify.SignedEntityVerifier)
|
||||
verifiers := make(map[string]*verify.Verifier)
|
||||
reader := bufio.NewReader(bytes.NewReader(customTrustRoots))
|
||||
var line []byte
|
||||
var readError error
|
||||
|
|
@ -191,7 +191,7 @@ func getBundleIssuer(b *bundle.Bundle) (string, error) {
|
|||
return leafCert.Issuer.Organization[0], nil
|
||||
}
|
||||
|
||||
func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEntityVerifier, error) {
|
||||
func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.Verifier, error) {
|
||||
// if no custom trusted root is set, return either the Public Good or GitHub verifier
|
||||
// If the chosen verifier has not yet been created, create it as a LiveSigstoreVerifier field for use in future calls
|
||||
if v.Custom != nil {
|
||||
|
|
@ -293,7 +293,7 @@ func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve
|
|||
return results, nil
|
||||
}
|
||||
|
||||
func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) {
|
||||
func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) {
|
||||
// All we know about this trust root is its configuration so make some
|
||||
// educated guesses as to what the policy should be.
|
||||
verifierConfig := []verify.VerifierOption{}
|
||||
|
|
@ -308,7 +308,7 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerif
|
|||
verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1))
|
||||
}
|
||||
|
||||
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verifierConfig...)
|
||||
gv, err := verify.NewVerifier(trustedRoot, verifierConfig...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create custom verifier: %v", err)
|
||||
}
|
||||
|
|
@ -316,7 +316,7 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerif
|
|||
return gv, nil
|
||||
}
|
||||
|
||||
func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string], hc *http.Client) (*verify.SignedEntityVerifier, error) {
|
||||
func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string], hc *http.Client) (*verify.Verifier, error) {
|
||||
var tr string
|
||||
|
||||
opts := GitHubTUFOptions(tufMetadataDir, hc)
|
||||
|
|
@ -341,8 +341,8 @@ func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string], hc *
|
|||
return newGitHubVerifierWithTrustedRoot(trustedRoot)
|
||||
}
|
||||
|
||||
func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) {
|
||||
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1))
|
||||
func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) {
|
||||
gv, err := verify.NewVerifier(trustedRoot, verify.WithSignedTimestamps(1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GitHub verifier: %v", err)
|
||||
}
|
||||
|
|
@ -350,7 +350,7 @@ func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Si
|
|||
return gv, nil
|
||||
}
|
||||
|
||||
func newPublicGoodVerifier(tufMetadataDir o.Option[string], hc *http.Client) (*verify.SignedEntityVerifier, error) {
|
||||
func newPublicGoodVerifier(tufMetadataDir o.Option[string], hc *http.Client) (*verify.Verifier, error) {
|
||||
opts := DefaultOptionsWithCacheSetting(tufMetadataDir, hc)
|
||||
client, err := tuf.New(opts)
|
||||
if err != nil {
|
||||
|
|
@ -364,8 +364,8 @@ func newPublicGoodVerifier(tufMetadataDir o.Option[string], hc *http.Client) (*v
|
|||
return newPublicGoodVerifierWithTrustedRoot(trustedRoot)
|
||||
}
|
||||
|
||||
func newPublicGoodVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) {
|
||||
sv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
|
||||
func newPublicGoodVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) {
|
||||
sv, err := verify.NewVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Public Good verifier: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ import (
|
|||
func NewCmdConfig(f *cmdutil.Factory) *cobra.Command {
|
||||
longDoc := strings.Builder{}
|
||||
longDoc.WriteString("Display or change configuration settings for gh.\n\n")
|
||||
longDoc.WriteString("Current respected settings:\n\n")
|
||||
longDoc.WriteString("Current respected settings:\n")
|
||||
for _, co := range config.Options {
|
||||
longDoc.WriteString(fmt.Sprintf("- `%s`: %s", co.Key, co.Description))
|
||||
if len(co.AllowedValues) > 0 {
|
||||
longDoc.WriteString(fmt.Sprintf(" {%s}", strings.Join(co.AllowedValues, "|")))
|
||||
longDoc.WriteString(fmt.Sprintf(" `{%s}`", strings.Join(co.AllowedValues, " | ")))
|
||||
}
|
||||
if co.DefaultValue != "" {
|
||||
longDoc.WriteString(fmt.Sprintf(" (default %s)", co.DefaultValue))
|
||||
longDoc.WriteString(fmt.Sprintf(" (default `%s`)", co.DefaultValue))
|
||||
}
|
||||
longDoc.WriteRune('\n')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,11 +60,17 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
|
||||
Editing issues' 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)
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
$ gh issue edit 23 --title "I found a bug" --body "Nothing works"
|
||||
$ gh issue edit 23 --add-label "bug,help wanted" --remove-label "core"
|
||||
$ gh issue edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot
|
||||
$ gh issue edit 23 --add-assignee "@copilot"
|
||||
$ gh issue edit 23 --add-project "Roadmap" --remove-project v1,v2
|
||||
$ gh issue edit 23 --milestone "Version 1"
|
||||
$ gh issue edit 23 --remove-milestone
|
||||
|
|
@ -197,9 +203,24 @@ func editRun(opts *EditOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
if opts.Detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
||||
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
|
||||
}
|
||||
|
||||
issueFeatures, err := opts.Detector.IssueFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lookupFields := []string{"id", "number", "title", "body", "url"}
|
||||
if editable.Assignees.Edited {
|
||||
lookupFields = append(lookupFields, "assignees")
|
||||
if issueFeatures.ActorIsAssignable {
|
||||
editable.Assignees.ActorAssignees = true
|
||||
lookupFields = append(lookupFields, "assignedActors")
|
||||
} else {
|
||||
lookupFields = append(lookupFields, "assignees")
|
||||
}
|
||||
}
|
||||
if editable.Labels.Edited {
|
||||
lookupFields = append(lookupFields, "labels")
|
||||
|
|
@ -207,11 +228,6 @@ func editRun(opts *EditOptions) error {
|
|||
if editable.Projects.Edited {
|
||||
// TODO projectsV1Deprecation
|
||||
// Remove this section as we should no longer add projectCards
|
||||
if opts.Detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
||||
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
|
||||
}
|
||||
|
||||
projectsV1Support := opts.Detector.ProjectsV1()
|
||||
if projectsV1Support == gh.ProjectsV1Supported {
|
||||
lookupFields = append(lookupFields, "projectCards")
|
||||
|
|
@ -254,7 +270,14 @@ func editRun(opts *EditOptions) error {
|
|||
|
||||
editable.Title.Default = issue.Title
|
||||
editable.Body.Default = issue.Body
|
||||
editable.Assignees.Default = issue.Assignees.Logins()
|
||||
// We use Actors as the default assignees if Actors are assignable
|
||||
// on this GitHub host.
|
||||
if editable.Assignees.ActorAssignees {
|
||||
editable.Assignees.Default = issue.AssignedActors.DisplayNames()
|
||||
editable.Assignees.DefaultLogins = issue.AssignedActors.Logins()
|
||||
} else {
|
||||
editable.Assignees.Default = issue.Assignees.Logins()
|
||||
}
|
||||
editable.Labels.Default = issue.Labels.Names()
|
||||
editable.Projects.Default = append(issue.ProjectCards.ProjectNames(), issue.ProjectItems.ProjectTitles()...)
|
||||
projectItems := map[string]string{}
|
||||
|
|
|
|||
|
|
@ -118,9 +118,11 @@ func TestNewCmdEdit(t *testing.T) {
|
|||
output: EditOptions{
|
||||
IssueNumbers: []int{23},
|
||||
Editable: prShared.Editable{
|
||||
Assignees: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Edited: true,
|
||||
Assignees: prShared.EditableAssignees{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -132,9 +134,11 @@ func TestNewCmdEdit(t *testing.T) {
|
|||
output: EditOptions{
|
||||
IssueNumbers: []int{23},
|
||||
Editable: prShared.Editable{
|
||||
Assignees: prShared.EditableSlice{
|
||||
Remove: []string{"monalisa", "hubot"},
|
||||
Edited: true,
|
||||
Assignees: prShared.EditableAssignees{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Remove: []string{"monalisa", "hubot"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -354,10 +358,12 @@ func Test_editRun(t *testing.T) {
|
|||
Value: "new body",
|
||||
Edited: true,
|
||||
},
|
||||
Assignees: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
Assignees: prShared.EditableAssignees{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
Labels: prShared.EditableSlice{
|
||||
Add: []string{"feature", "TODO", "bug"},
|
||||
|
|
@ -388,6 +394,7 @@ func Test_editRun(t *testing.T) {
|
|||
mockIssueProjectItemsGet(t, reg)
|
||||
mockRepoMetadata(t, reg)
|
||||
mockIssueUpdate(t, reg)
|
||||
mockIssueUpdateActorAssignees(t, reg)
|
||||
mockIssueUpdateLabels(t, reg)
|
||||
mockProjectV2ItemUpdate(t, reg)
|
||||
},
|
||||
|
|
@ -399,10 +406,12 @@ func Test_editRun(t *testing.T) {
|
|||
IssueNumbers: []int{456, 123},
|
||||
Interactive: false,
|
||||
Editable: prShared.Editable{
|
||||
Assignees: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
Assignees: prShared.EditableAssignees{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
Labels: prShared.EditableSlice{
|
||||
Add: []string{"feature", "TODO", "bug"},
|
||||
|
|
@ -433,6 +442,8 @@ func Test_editRun(t *testing.T) {
|
|||
mockIssueProjectItemsGet(t, reg)
|
||||
mockIssueUpdate(t, reg)
|
||||
mockIssueUpdate(t, reg)
|
||||
mockIssueUpdateActorAssignees(t, reg)
|
||||
mockIssueUpdateActorAssignees(t, reg)
|
||||
mockIssueUpdateLabels(t, reg)
|
||||
mockIssueUpdateLabels(t, reg)
|
||||
mockProjectV2ItemUpdate(t, reg)
|
||||
|
|
@ -449,10 +460,12 @@ func Test_editRun(t *testing.T) {
|
|||
IssueNumbers: []int{123, 9999},
|
||||
Interactive: false,
|
||||
Editable: prShared.Editable{
|
||||
Assignees: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
Assignees: prShared.EditableAssignees{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
Labels: prShared.EditableSlice{
|
||||
Add: []string{"feature", "TODO", "bug"},
|
||||
|
|
@ -494,10 +507,12 @@ func Test_editRun(t *testing.T) {
|
|||
IssueNumbers: []int{123, 456},
|
||||
Interactive: false,
|
||||
Editable: prShared.Editable{
|
||||
Assignees: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
Assignees: prShared.EditableAssignees{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
Milestone: prShared.EditableString{
|
||||
Value: "GA",
|
||||
|
|
@ -509,14 +524,14 @@ func Test_editRun(t *testing.T) {
|
|||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
// Should only be one fetch of metadata.
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
|
||||
httpmock.GraphQL(`query RepositoryAssignableActors\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "assignableUsers": {
|
||||
{ "data": { "repository": { "suggestedActors": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID" },
|
||||
{ "login": "MonaLisa", "id": "MONAID" }
|
||||
{ "login": "hubot", "id": "HUBOTID", "__typename": "Bot" },
|
||||
{ "login": "MonaLisa", "id": "MONAID", "__typename": "User" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
"pageInfo": { "hasNextPage": false, "endCursor": "Mg" }
|
||||
} } } }
|
||||
`))
|
||||
reg.Register(
|
||||
|
|
@ -534,6 +549,14 @@ func Test_editRun(t *testing.T) {
|
|||
mockIssueNumberGet(t, reg, 123)
|
||||
mockIssueNumberGet(t, reg, 456)
|
||||
// Updating 123 should succeed.
|
||||
reg.Register(
|
||||
httpmock.GraphQLMutationMatcher(`mutation ReplaceActorsForAssignable\b`, func(m map[string]interface{}) bool {
|
||||
return m["assignableId"] == "123"
|
||||
}),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }`,
|
||||
func(inputs map[string]interface{}) {}),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQLMutationMatcher(`mutation IssueUpdate\b`, func(m map[string]interface{}) bool {
|
||||
return m["id"] == "123"
|
||||
|
|
@ -544,8 +567,8 @@ func Test_editRun(t *testing.T) {
|
|||
)
|
||||
// Updating 456 should fail.
|
||||
reg.Register(
|
||||
httpmock.GraphQLMutationMatcher(`mutation IssueUpdate\b`, func(m map[string]interface{}) bool {
|
||||
return m["id"] == "456"
|
||||
httpmock.GraphQLMutationMatcher(`mutation ReplaceActorsForAssignable\b`, func(m map[string]interface{}) bool {
|
||||
return m["assignableId"] == "456"
|
||||
}),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "errors": [ { "message": "test error" } ] }`,
|
||||
|
|
@ -591,11 +614,129 @@ func Test_editRun(t *testing.T) {
|
|||
mockIssueProjectItemsGet(t, reg)
|
||||
mockRepoMetadata(t, reg)
|
||||
mockIssueUpdate(t, reg)
|
||||
mockIssueUpdateActorAssignees(t, reg)
|
||||
mockIssueUpdateLabels(t, reg)
|
||||
mockProjectV2ItemUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issue/123\n",
|
||||
},
|
||||
{
|
||||
name: "interactive prompts with actor assignee display names when actors available",
|
||||
input: &EditOptions{
|
||||
IssueNumbers: []int{123},
|
||||
Interactive: true,
|
||||
FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error {
|
||||
eo.Assignees.Edited = true
|
||||
return nil
|
||||
},
|
||||
EditFieldsSurvey: func(p prShared.EditPrompter, eo *prShared.Editable, _ string) error {
|
||||
// Checking that the display name is being used in the prompt.
|
||||
require.Equal(t, eo.Assignees.Default, []string{"hubot", "MonaLisa (Mona Display Name)"})
|
||||
|
||||
// Mocking a selection of only MonaLisa in the prompt.
|
||||
eo.Assignees.Value = []string{"MonaLisa (Mona Display Name)"}
|
||||
return nil
|
||||
},
|
||||
FetchOptions: prShared.FetchOptions,
|
||||
DetermineEditor: func() (string, error) { return "vim", nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockIsssueNumberGetWithAssignedActors(t, reg, 123)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryAssignableActors\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "suggestedActors": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID", "__typename": "Bot" },
|
||||
{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
mockIssueUpdate(t, reg)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
// Checking that despite the display name being returned
|
||||
// from the EditFieldsSurvey, the ID is still
|
||||
// used in the mutation.
|
||||
require.Contains(t, inputs["actorIds"], "MONAID")
|
||||
}),
|
||||
)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issue/123\n",
|
||||
},
|
||||
{
|
||||
name: "interactive prompts with user assignee logins when actors unavailable",
|
||||
input: &EditOptions{
|
||||
IssueNumbers: []int{123},
|
||||
Interactive: true,
|
||||
FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error {
|
||||
eo.Assignees.Edited = true
|
||||
return nil
|
||||
},
|
||||
EditFieldsSurvey: func(p prShared.EditPrompter, eo *prShared.Editable, _ string) error {
|
||||
// Checking that only the login is used in the prompt (no display name)
|
||||
require.Equal(t, eo.Assignees.Default, []string{"hubot", "MonaLisa"})
|
||||
|
||||
// Mocking a selection of only MonaLisa in the prompt.
|
||||
eo.Assignees.Value = []string{"MonaLisa"}
|
||||
return nil
|
||||
},
|
||||
FetchOptions: prShared.FetchOptions,
|
||||
DetermineEditor: func() (string, error) { return "vim", nil },
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(fmt.Sprintf(`
|
||||
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
|
||||
"id": "%[1]d",
|
||||
"number": %[1]d,
|
||||
"url": "https://github.com/OWNER/REPO/issue/123",
|
||||
"assignees": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "HUBOTID",
|
||||
"login": "hubot",
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"id": "MONAID",
|
||||
"login": "MonaLisa",
|
||||
"name": "Mona Display Name"
|
||||
}
|
||||
],
|
||||
"totalCount": 2
|
||||
}
|
||||
} } } }`, 123)),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "assignableUsers": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID", "name": "" },
|
||||
{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation IssueUpdate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "updateIssue": { "__typename": "" } } }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
// Checking that we still assigned the expected ID.
|
||||
require.Contains(t, inputs["assigneeIds"], "MONAID")
|
||||
}),
|
||||
)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issue/123\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -654,6 +795,34 @@ func mockIssueNumberGet(_ *testing.T, reg *httpmock.Registry, number int) {
|
|||
)
|
||||
}
|
||||
|
||||
func mockIsssueNumberGetWithAssignedActors(_ *testing.T, reg *httpmock.Registry, number int) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(fmt.Sprintf(`
|
||||
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
|
||||
"id": "%[1]d",
|
||||
"number": %[1]d,
|
||||
"url": "https://github.com/OWNER/REPO/issue/%[1]d",
|
||||
"assignedActors": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "HUBOTID",
|
||||
"login": "hubot",
|
||||
"__typename": "Bot"
|
||||
},
|
||||
{
|
||||
"id": "MONAID",
|
||||
"login": "MonaLisa",
|
||||
"name": "Mona Display Name",
|
||||
"__typename": "User"
|
||||
}
|
||||
],
|
||||
"totalCount": 2
|
||||
}
|
||||
} } } }`, number)),
|
||||
)
|
||||
}
|
||||
|
||||
func mockIssueProjectItemsGet(_ *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueProjectItems\b`),
|
||||
|
|
@ -670,16 +839,17 @@ func mockIssueProjectItemsGet(_ *testing.T, reg *httpmock.Registry) {
|
|||
|
||||
func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
|
||||
httpmock.GraphQL(`query RepositoryAssignableActors\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "assignableUsers": {
|
||||
{ "data": { "repository": { "suggestedActors": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID" },
|
||||
{ "login": "MonaLisa", "id": "MONAID" }
|
||||
{ "login": "hubot", "id": "HUBOTID", "__typename": "Bot" },
|
||||
{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryLabelList\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -767,6 +937,15 @@ func mockIssueUpdate(t *testing.T, reg *httpmock.Registry) {
|
|||
)
|
||||
}
|
||||
|
||||
func mockIssueUpdateActorAssignees(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }`,
|
||||
func(inputs map[string]interface{}) {}),
|
||||
)
|
||||
}
|
||||
|
||||
func mockIssueUpdateLabels(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation LabelAdd\b`),
|
||||
|
|
@ -791,6 +970,85 @@ func mockProjectV2ItemUpdate(t *testing.T, reg *httpmock.Registry) {
|
|||
)
|
||||
}
|
||||
|
||||
func TestActorIsAssignable(t *testing.T) {
|
||||
t.Run("when actors are assignable, query includes assignedActors", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`assignedActors`),
|
||||
// Simulate a GraphQL error to early exit the test.
|
||||
httpmock.StatusStringResponse(500, ""),
|
||||
)
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
// Ignore the error because we don't care.
|
||||
_ = editRun(&EditOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
IssueNumbers: []int{123},
|
||||
Editable: prShared.Editable{
|
||||
Assignees: prShared.EditableAssignees{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
reg.Verify(t)
|
||||
})
|
||||
|
||||
t.Run("when actors are not assignable, query includes assignees instead", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
// This test should NOT include assignedActors in the query
|
||||
reg.Exclude(t, httpmock.GraphQL(`assignedActors`))
|
||||
// It should include the regular assignees field
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`assignees`),
|
||||
// Simulate a GraphQL error to early exit the test.
|
||||
httpmock.StatusStringResponse(500, ""),
|
||||
)
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
// Ignore the error because we're not really interested in it.
|
||||
_ = editRun(&EditOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
IssueNumbers: []int{123},
|
||||
Editable: prShared.Editable{
|
||||
Assignees: prShared.EditableAssignees{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Add: []string{"monalisa", "octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO projectsV1Deprecation
|
||||
// Remove this test.
|
||||
func TestProjectsV1Deprecation(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package edit
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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"
|
||||
shared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
|
|
@ -25,6 +27,8 @@ type EditOptions struct {
|
|||
Fetcher EditableOptionsFetcher
|
||||
EditorRetriever EditorRetriever
|
||||
Prompter shared.EditPrompter
|
||||
Detector fd.Detector
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
SelectorArg string
|
||||
Interactive bool
|
||||
|
|
@ -56,12 +60,21 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
|
||||
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 do not support
|
||||
these special values.
|
||||
`, "`"),
|
||||
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-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
|
||||
|
|
@ -69,6 +82,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.SelectorArg = args[0]
|
||||
|
|
@ -192,8 +206,36 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
func editRun(opts *EditOptions) error {
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "assignees", "labels", "projectCards", "projectItems", "milestone"},
|
||||
Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone"},
|
||||
Detector: opts.Detector,
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -205,7 +247,12 @@ func editRun(opts *EditOptions) error {
|
|||
editable.Body.Default = pr.Body
|
||||
editable.Base.Default = pr.BaseRefName
|
||||
editable.Reviewers.Default = pr.ReviewRequests.Logins()
|
||||
editable.Assignees.Default = pr.Assignees.Logins()
|
||||
if issueFeatures.ActorIsAssignable {
|
||||
editable.Assignees.ActorAssignees = true
|
||||
editable.Assignees.Default = pr.AssignedActors.DisplayNames()
|
||||
} 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{}
|
||||
|
|
@ -224,10 +271,6 @@ func editRun(opts *EditOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
|
|
@ -278,8 +321,7 @@ func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, id
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (userIds == nil || len(*userIds) == 0) &&
|
||||
(teamIds == nil || len(*teamIds) == 0) {
|
||||
if userIds == nil && teamIds == nil {
|
||||
return nil
|
||||
}
|
||||
union := githubv4.Boolean(false)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
shared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -165,9 +166,11 @@ func TestNewCmdEdit(t *testing.T) {
|
|||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
Editable: shared.Editable{
|
||||
Assignees: shared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Edited: true,
|
||||
Assignees: shared.EditableAssignees{
|
||||
EditableSlice: shared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -179,9 +182,11 @@ func TestNewCmdEdit(t *testing.T) {
|
|||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
Editable: shared.Editable{
|
||||
Assignees: shared.EditableSlice{
|
||||
Remove: []string{"monalisa", "hubot"},
|
||||
Edited: true,
|
||||
Assignees: shared.EditableAssignees{
|
||||
EditableSlice: shared.EditableSlice{
|
||||
Remove: []string{"monalisa", "hubot"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -359,10 +364,12 @@ func Test_editRun(t *testing.T) {
|
|||
Remove: []string{"dependabot"},
|
||||
Edited: true,
|
||||
},
|
||||
Assignees: shared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
Assignees: shared.EditableAssignees{
|
||||
EditableSlice: shared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
Labels: shared.EditableSlice{
|
||||
Add: []string{"feature", "TODO", "bug"},
|
||||
|
|
@ -386,6 +393,7 @@ func Test_editRun(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
mockRepoMetadata(reg, false)
|
||||
mockPullRequestUpdate(reg)
|
||||
mockPullRequestUpdateActorAssignees(reg)
|
||||
mockPullRequestReviewersUpdate(reg)
|
||||
mockPullRequestUpdateLabels(reg)
|
||||
mockProjectV2ItemUpdate(reg)
|
||||
|
|
@ -413,10 +421,12 @@ func Test_editRun(t *testing.T) {
|
|||
Value: "base-branch-name",
|
||||
Edited: true,
|
||||
},
|
||||
Assignees: shared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
Assignees: shared.EditableAssignees{
|
||||
EditableSlice: shared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
Labels: shared.EditableSlice{
|
||||
Add: []string{"feature", "TODO", "bug"},
|
||||
|
|
@ -440,11 +450,73 @@ func Test_editRun(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
mockRepoMetadata(reg, true)
|
||||
mockPullRequestUpdate(reg)
|
||||
mockPullRequestUpdateActorAssignees(reg)
|
||||
mockPullRequestUpdateLabels(reg)
|
||||
mockProjectV2ItemUpdate(reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive remove all reviewers",
|
||||
input: &EditOptions{
|
||||
SelectorArg: "123",
|
||||
Finder: shared.NewMockFinder("123", &api.PullRequest{
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
}, ghrepo.New("OWNER", "REPO")),
|
||||
Interactive: false,
|
||||
Editable: shared.Editable{
|
||||
Title: shared.EditableString{
|
||||
Value: "new title",
|
||||
Edited: true,
|
||||
},
|
||||
Body: shared.EditableString{
|
||||
Value: "new body",
|
||||
Edited: true,
|
||||
},
|
||||
Base: shared.EditableString{
|
||||
Value: "base-branch-name",
|
||||
Edited: true,
|
||||
},
|
||||
Reviewers: shared.EditableSlice{
|
||||
Remove: []string{"OWNER/core", "OWNER/external", "monalisa", "hubot", "dependabot"},
|
||||
Edited: true,
|
||||
},
|
||||
Assignees: shared.EditableAssignees{
|
||||
EditableSlice: shared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
Labels: shared.EditableSlice{
|
||||
Add: []string{"feature", "TODO", "bug"},
|
||||
Remove: []string{"docs"},
|
||||
Edited: true,
|
||||
},
|
||||
Projects: shared.EditableProjects{
|
||||
EditableSlice: shared.EditableSlice{
|
||||
Add: []string{"Cleanup", "CleanupV2"},
|
||||
Remove: []string{"Roadmap", "RoadmapV2"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
Milestone: shared.EditableString{
|
||||
Value: "GA",
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
Fetcher: testFetcher{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
mockRepoMetadata(reg, false)
|
||||
mockPullRequestUpdate(reg)
|
||||
mockPullRequestReviewersUpdate(reg)
|
||||
mockPullRequestUpdateLabels(reg)
|
||||
mockPullRequestUpdateActorAssignees(reg)
|
||||
mockProjectV2ItemUpdate(reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123\n",
|
||||
},
|
||||
{
|
||||
name: "interactive",
|
||||
input: &EditOptions{
|
||||
|
|
@ -460,6 +532,7 @@ func Test_editRun(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
mockRepoMetadata(reg, false)
|
||||
mockPullRequestUpdate(reg)
|
||||
mockPullRequestUpdateActorAssignees(reg)
|
||||
mockPullRequestReviewersUpdate(reg)
|
||||
mockPullRequestUpdateLabels(reg)
|
||||
mockProjectV2ItemUpdate(reg)
|
||||
|
|
@ -481,11 +554,72 @@ func Test_editRun(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
mockRepoMetadata(reg, true)
|
||||
mockPullRequestUpdate(reg)
|
||||
mockPullRequestUpdateActorAssignees(reg)
|
||||
mockPullRequestUpdateLabels(reg)
|
||||
mockProjectV2ItemUpdate(reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123\n",
|
||||
},
|
||||
{
|
||||
name: "interactive remove all reviewers",
|
||||
input: &EditOptions{
|
||||
SelectorArg: "123",
|
||||
Finder: shared.NewMockFinder("123", &api.PullRequest{
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
}, ghrepo.New("OWNER", "REPO")),
|
||||
Interactive: true,
|
||||
Surveyor: testSurveyor{removeAllReviewers: true},
|
||||
Fetcher: testFetcher{},
|
||||
EditorRetriever: testEditorRetriever{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
mockRepoMetadata(reg, false)
|
||||
mockPullRequestUpdate(reg)
|
||||
mockPullRequestReviewersUpdate(reg)
|
||||
mockPullRequestUpdateActorAssignees(reg)
|
||||
mockPullRequestUpdateLabels(reg)
|
||||
mockProjectV2ItemUpdate(reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123\n",
|
||||
},
|
||||
{
|
||||
name: "Legacy assignee users are fetched and updated on unsupported GitHub Hosts",
|
||||
input: &EditOptions{
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
SelectorArg: "123",
|
||||
Finder: shared.NewMockFinder("123", &api.PullRequest{
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
}, ghrepo.New("OWNER", "REPO")),
|
||||
Interactive: false,
|
||||
Editable: shared.Editable{
|
||||
Assignees: shared.EditableAssignees{
|
||||
EditableSlice: shared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Fetcher: testFetcher{},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
// Notice there is no call to mockReplaceActorsForAssignable()
|
||||
// and no GraphQL call to RepositoryAssignableActors below.
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "assignableUsers": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID" },
|
||||
{ "login": "MonaLisa", "id": "MONAID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
mockPullRequestUpdate(reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -499,9 +633,11 @@ func Test_editRun(t *testing.T) {
|
|||
tt.httpStubs(reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
|
||||
baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
||||
|
||||
tt.input.IO = ios
|
||||
tt.input.HttpClient = httpClient
|
||||
tt.input.BaseRepo = baseRepo
|
||||
|
||||
err := editRun(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -513,16 +649,16 @@ func Test_editRun(t *testing.T) {
|
|||
|
||||
func mockRepoMetadata(reg *httpmock.Registry, skipReviewers bool) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
|
||||
httpmock.GraphQL(`query RepositoryAssignableActors\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "assignableUsers": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID" },
|
||||
{ "login": "MonaLisa", "id": "MONAID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
{ "data": { "repository": { "suggestedActors": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID", "__typename": "Bot" },
|
||||
{ "login": "MonaLisa", "id": "MONAID", "__typename": "User" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryLabelList\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -625,6 +761,15 @@ func mockPullRequestUpdate(reg *httpmock.Registry) {
|
|||
httpmock.StringResponse(`{}`))
|
||||
}
|
||||
|
||||
func mockPullRequestUpdateActorAssignees(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }`,
|
||||
func(inputs map[string]interface{}) {}),
|
||||
)
|
||||
}
|
||||
|
||||
func mockPullRequestReviewersUpdate(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestUpdateRequestReviews\b`),
|
||||
|
|
@ -657,7 +802,8 @@ func mockProjectV2ItemUpdate(reg *httpmock.Registry) {
|
|||
|
||||
type testFetcher struct{}
|
||||
type testSurveyor struct {
|
||||
skipReviewers bool
|
||||
skipReviewers bool
|
||||
removeAllReviewers bool
|
||||
}
|
||||
type testEditorRetriever struct{}
|
||||
|
||||
|
|
@ -682,7 +828,11 @@ func (s testSurveyor) EditFields(e *shared.Editable, _ string) error {
|
|||
e.Title.Value = "new title"
|
||||
e.Body.Value = "new body"
|
||||
if !s.skipReviewers {
|
||||
e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"}
|
||||
if s.removeAllReviewers {
|
||||
e.Reviewers.Remove = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external", "dependabot"}
|
||||
} else {
|
||||
e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"}
|
||||
}
|
||||
}
|
||||
e.Assignees.Value = []string{"monalisa", "hubot"}
|
||||
e.Labels.Value = []string{"feature", "TODO", "bug"}
|
||||
|
|
@ -696,3 +846,73 @@ func (s testSurveyor) EditFields(e *shared.Editable, _ string) error {
|
|||
func (t testEditorRetriever) Retrieve() (string, error) {
|
||||
return "vim", nil
|
||||
}
|
||||
|
||||
// TODO projectsV1Deprecation
|
||||
// Remove this test.
|
||||
func TestProjectsV1Deprecation(t *testing.T) {
|
||||
t.Run("when projects v1 is supported, is included in query", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`projectCards`),
|
||||
// Simulate a GraphQL error to early exit the test.
|
||||
httpmock.StatusStringResponse(500, ""),
|
||||
)
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Ignore the error because we have no way to really stub it without
|
||||
// fully stubbing a GQL error structure in the request body.
|
||||
_ = editRun(&EditOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
|
||||
Finder: shared.NewFinder(f),
|
||||
|
||||
SelectorArg: "https://github.com/cli/cli/pull/123",
|
||||
})
|
||||
|
||||
// Verify that our request contained projectCards
|
||||
reg.Verify(t)
|
||||
})
|
||||
|
||||
t.Run("when projects v1 is not supported, is not included in query", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Exclude(t, httpmock.GraphQL(`projectCards`))
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Ignore the error because we have no way to really stub it without
|
||||
// fully stubbing a GQL error structure in the request body.
|
||||
_ = editRun(&EditOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
|
||||
Finder: shared.NewFinder(f),
|
||||
|
||||
SelectorArg: "https://github.com/cli/cli/pull/123",
|
||||
})
|
||||
|
||||
// Verify that our request did not contain projectCards
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ func RequestableReviewersForCompletion(httpClient *http.Client, repo ghrepo.Inte
|
|||
|
||||
results := []string{}
|
||||
for _, user := range metadata.AssignableUsers {
|
||||
if strings.EqualFold(user.Login, metadata.CurrentLogin) {
|
||||
if strings.EqualFold(user.Login(), metadata.CurrentLogin) {
|
||||
continue
|
||||
}
|
||||
if user.Name != "" {
|
||||
results = append(results, fmt.Sprintf("%s\t%s", user.Login, user.Name))
|
||||
if user.Name() != "" {
|
||||
results = append(results, fmt.Sprintf("%s\t%s", user.Login(), user.Name()))
|
||||
} else {
|
||||
results = append(results, user.Login)
|
||||
results = append(results, user.Login())
|
||||
}
|
||||
}
|
||||
for _, team := range metadata.Teams {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ type Editable struct {
|
|||
Body EditableString
|
||||
Base EditableString
|
||||
Reviewers EditableSlice
|
||||
Assignees EditableSlice
|
||||
Assignees EditableAssignees
|
||||
Labels EditableSlice
|
||||
Projects EditableProjects
|
||||
Milestone EditableString
|
||||
|
|
@ -38,6 +38,14 @@ type EditableSlice struct {
|
|||
Allowed bool
|
||||
}
|
||||
|
||||
// EditableAssignees is a special case of EditableSlice.
|
||||
// It contains a flag to indicate whether the assignees are actors or not.
|
||||
type EditableAssignees struct {
|
||||
EditableSlice
|
||||
ActorAssignees bool
|
||||
DefaultLogins []string // For disambiguating actors from display names
|
||||
}
|
||||
|
||||
// ProjectsV2 mutations require a mapping of an item ID to a project ID.
|
||||
// Keep that map along with standard EditableSlice data.
|
||||
type EditableProjects struct {
|
||||
|
|
@ -105,21 +113,56 @@ func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]str
|
|||
if !e.Assignees.Edited {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If assignees came in from command line flags, we need to
|
||||
// curate the final list of assignees from the default list.
|
||||
if len(e.Assignees.Add) != 0 || len(e.Assignees.Remove) != 0 {
|
||||
meReplacer := NewMeReplacer(client, repo.RepoHost())
|
||||
s := set.NewStringSet()
|
||||
s.AddValues(e.Assignees.Default)
|
||||
add, err := meReplacer.ReplaceSlice(e.Assignees.Add)
|
||||
copilotReplacer := NewCopilotReplacer()
|
||||
|
||||
replaceSpecialAssigneeNames := func(value []string) ([]string, error) {
|
||||
replaced, err := meReplacer.ReplaceSlice(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Only suppported for actor assignees.
|
||||
if e.Assignees.ActorAssignees {
|
||||
replaced = copilotReplacer.ReplaceSlice(replaced)
|
||||
}
|
||||
|
||||
return replaced, nil
|
||||
}
|
||||
|
||||
assigneeSet := set.NewStringSet()
|
||||
|
||||
// This check below is required because in a non-interactive flow,
|
||||
// the user gives us a login and not the DisplayName, and when
|
||||
// we have actor assignees e.Assignees.Default will contain
|
||||
// DisplayNames and not logins (this is to accommodate special actor
|
||||
// display names in the interactive flow).
|
||||
// So, we need to add the default logins here instead of the DisplayNames.
|
||||
// Otherwise, the value the user provided won't be found in the
|
||||
// set to be added or removed, causing unexpected behavior.
|
||||
if e.Assignees.ActorAssignees {
|
||||
assigneeSet.AddValues(e.Assignees.DefaultLogins)
|
||||
} else {
|
||||
assigneeSet.AddValues(e.Assignees.Default)
|
||||
}
|
||||
|
||||
add, err := replaceSpecialAssigneeNames(e.Assignees.Add)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.AddValues(add)
|
||||
remove, err := meReplacer.ReplaceSlice(e.Assignees.Remove)
|
||||
assigneeSet.AddValues(add)
|
||||
|
||||
remove, err := replaceSpecialAssigneeNames(e.Assignees.Remove)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.RemoveValues(remove)
|
||||
e.Assignees.Value = s.ToSlice()
|
||||
assigneeSet.RemoveValues(remove)
|
||||
|
||||
e.Assignees.Value = assigneeSet.ToSlice()
|
||||
}
|
||||
a, err := e.Metadata.MembersToIDs(e.Assignees.Value)
|
||||
return &a, err
|
||||
|
|
@ -245,6 +288,14 @@ func (es *EditableSlice) clone() EditableSlice {
|
|||
return cpy
|
||||
}
|
||||
|
||||
func (ea *EditableAssignees) clone() EditableAssignees {
|
||||
return EditableAssignees{
|
||||
EditableSlice: ea.EditableSlice.clone(),
|
||||
ActorAssignees: ea.ActorAssignees,
|
||||
DefaultLogins: ea.DefaultLogins,
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *EditableProjects) clone() EditableProjects {
|
||||
return EditableProjects{
|
||||
EditableSlice: ep.EditableSlice.clone(),
|
||||
|
|
@ -378,12 +429,13 @@ func FieldsToEditSurvey(p EditPrompter, editable *Editable) error {
|
|||
|
||||
func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) error {
|
||||
input := api.RepoMetadataInput{
|
||||
Reviewers: editable.Reviewers.Edited,
|
||||
Assignees: editable.Assignees.Edited,
|
||||
Labels: editable.Labels.Edited,
|
||||
ProjectsV1: editable.Projects.Edited,
|
||||
ProjectsV2: editable.Projects.Edited,
|
||||
Milestones: editable.Milestone.Edited,
|
||||
Reviewers: editable.Reviewers.Edited,
|
||||
Assignees: editable.Assignees.Edited,
|
||||
ActorAssignees: editable.Assignees.ActorAssignees,
|
||||
Labels: editable.Labels.Edited,
|
||||
ProjectsV1: editable.Projects.Edited,
|
||||
ProjectsV2: editable.Projects.Edited,
|
||||
Milestones: editable.Milestone.Edited,
|
||||
}
|
||||
metadata, err := api.RepoMetadata(client, repo, input)
|
||||
if err != nil {
|
||||
|
|
@ -392,7 +444,11 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable)
|
|||
|
||||
var users []string
|
||||
for _, u := range metadata.AssignableUsers {
|
||||
users = append(users, u.Login)
|
||||
users = append(users, u.Login())
|
||||
}
|
||||
var actors []string
|
||||
for _, a := range metadata.AssignableActors {
|
||||
actors = append(actors, a.DisplayName())
|
||||
}
|
||||
var teams []string
|
||||
for _, t := range metadata.Teams {
|
||||
|
|
@ -416,7 +472,11 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable)
|
|||
|
||||
editable.Metadata = *metadata
|
||||
editable.Reviewers.Options = append(users, teams...)
|
||||
editable.Assignees.Options = users
|
||||
if editable.Assignees.ActorAssignees {
|
||||
editable.Assignees.Options = actors
|
||||
} else {
|
||||
editable.Assignees.Options = users
|
||||
}
|
||||
editable.Labels.Options = labels
|
||||
editable.Projects.Options = projects
|
||||
editable.Milestone.Options = milestones
|
||||
|
|
|
|||
|
|
@ -60,25 +60,78 @@ func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR
|
|||
|
||||
if dirtyExcludingLabels(options) {
|
||||
wg.Go(func() error {
|
||||
return replaceIssueFields(httpClient, repo, id, isPR, options)
|
||||
// updateIssue mutation does not support Actors so assignment needs to
|
||||
// be in a separate request when our assignees are Actors.
|
||||
// Note: this is intentionally done synchronously with updating
|
||||
// other issue fields to ensure consistency with how legacy
|
||||
// user assignees are handled.
|
||||
// https://github.com/cli/cli/pull/10960#discussion_r2086725348
|
||||
if options.Assignees.Edited && options.Assignees.ActorAssignees {
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
assigneeIds, err := options.AssigneeIds(apiClient, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = replaceActorAssigneesForEditable(apiClient, repo, id, assigneeIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := replaceIssueFields(httpClient, repo, id, isPR, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return wg.Wait()
|
||||
}
|
||||
|
||||
func replaceIssueFields(httpClient *http.Client, repo ghrepo.Interface, id string, isPR bool, options Editable) error {
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
assigneeIds, err := options.AssigneeIds(apiClient, repo)
|
||||
func replaceActorAssigneesForEditable(apiClient *api.Client, repo ghrepo.Interface, id string, assigneeIds *[]string) error {
|
||||
type ReplaceActorsForAssignableInput struct {
|
||||
AssignableID githubv4.ID `json:"assignableId"`
|
||||
ActorIDs []githubv4.ID `json:"actorIds"`
|
||||
}
|
||||
|
||||
params := ReplaceActorsForAssignableInput{
|
||||
AssignableID: githubv4.ID(id),
|
||||
ActorIDs: *ghIds(assigneeIds),
|
||||
}
|
||||
|
||||
var mutation struct {
|
||||
ReplaceActorsForAssignable struct {
|
||||
TypeName string `graphql:"__typename"`
|
||||
} `graphql:"replaceActorsForAssignable(input: $input)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{"input": params}
|
||||
err := apiClient.Mutate(repo.RepoHost(), "ReplaceActorsForAssignable", &mutation, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceIssueFields(httpClient *http.Client, repo ghrepo.Interface, id string, isPR bool, options Editable) error {
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
projectIds, err := options.ProjectIds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var assigneeIds *[]string
|
||||
if !options.Assignees.ActorAssignees {
|
||||
assigneeIds, err = options.AssigneeIds(apiClient, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
milestoneId, err := options.MilestoneId()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -312,3 +312,26 @@ func (r *MeReplacer) ReplaceSlice(handles []string) ([]string, error) {
|
|||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// CopilotReplacer resolves usages of `@copilot` to Copilot's login.
|
||||
type CopilotReplacer struct{}
|
||||
|
||||
func NewCopilotReplacer() *CopilotReplacer {
|
||||
return &CopilotReplacer{}
|
||||
}
|
||||
|
||||
func (r *CopilotReplacer) replace(handle string) string {
|
||||
if strings.EqualFold(handle, "@copilot") {
|
||||
return api.CopilotActorLogin
|
||||
}
|
||||
return handle
|
||||
}
|
||||
|
||||
// ReplaceSlice replaces usages of `@copilot` in a slice with Copilot's login.
|
||||
func (r *CopilotReplacer) ReplaceSlice(handles []string) []string {
|
||||
res := make([]string, len(handles))
|
||||
for i, h := range handles {
|
||||
res[i] = r.replace(h)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,6 +187,67 @@ func TestMeReplacer_Replace(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCopilotReplacer_ReplaceSlice(t *testing.T) {
|
||||
type args struct {
|
||||
handles []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "replaces @copilot with copilot-swe-agent",
|
||||
args: args{
|
||||
handles: []string{"monalisa", "@copilot", "hubot"},
|
||||
},
|
||||
want: []string{"monalisa", "copilot-swe-agent", "hubot"},
|
||||
},
|
||||
{
|
||||
name: "handles no @copilot mentions",
|
||||
args: args{
|
||||
handles: []string{"monalisa", "user", "hubot"},
|
||||
},
|
||||
want: []string{"monalisa", "user", "hubot"},
|
||||
},
|
||||
{
|
||||
name: "replaces multiple @copilot mentions",
|
||||
args: args{
|
||||
handles: []string{"@copilot", "user", "@copilot"},
|
||||
},
|
||||
want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"},
|
||||
},
|
||||
{
|
||||
name: "handles @copilot case-insensitively",
|
||||
args: args{
|
||||
handles: []string{"@Copilot", "user", "@CoPiLoT"},
|
||||
},
|
||||
want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"},
|
||||
},
|
||||
{
|
||||
name: "handles nil slice",
|
||||
args: args{
|
||||
handles: nil,
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "handles empty slice",
|
||||
args: args{
|
||||
handles: []string{},
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := NewCopilotReplacer()
|
||||
got := r.ReplaceSlice(tt.args.handles)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_QueryHasStateClause(t *testing.T) {
|
||||
tests := []struct {
|
||||
searchQuery string
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface
|
|||
|
||||
var reviewers []string
|
||||
for _, u := range metadataResult.AssignableUsers {
|
||||
if u.Login != metadataResult.CurrentLogin {
|
||||
if u.Login() != metadataResult.CurrentLogin {
|
||||
reviewers = append(reviewers, u.DisplayName())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ func TestMetadataSurvey_selectAll(t *testing.T) {
|
|||
|
||||
fetcher := &metadataFetcher{
|
||||
metadataResult: &api.RepoMetadataResult{
|
||||
AssignableUsers: []api.RepoAssignee{
|
||||
{Login: "hubot"},
|
||||
{Login: "monalisa"},
|
||||
AssignableUsers: []api.AssignableUser{
|
||||
api.NewAssignableUser("", "hubot", ""),
|
||||
api.NewAssignableUser("", "monalisa", ""),
|
||||
},
|
||||
Labels: []api.RepoLabel{
|
||||
{Name: "help wanted"},
|
||||
|
|
|
|||
25
pkg/cmd/preview/preview.go
Normal file
25
pkg/cmd/preview/preview.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package preview
|
||||
|
||||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
cmdPrompter "github.com/cli/cli/v2/pkg/cmd/preview/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdPreview(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "preview <command>",
|
||||
Short: "Execute previews for gh features",
|
||||
Long: heredoc.Doc(`
|
||||
Preview commands are for testing, demonstrative, and development purposes only.
|
||||
They should be considered unstable and can change at any time.
|
||||
`),
|
||||
}
|
||||
|
||||
cmdutil.DisableAuthCheck(cmd)
|
||||
|
||||
cmd.AddCommand(cmdPrompter.NewCmdPrompter(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
236
pkg/cmd/preview/prompter/prompter.go
Normal file
236
pkg/cmd/preview/prompter/prompter.go
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
package prompter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type prompterOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
Config func() (gh.Config, error)
|
||||
|
||||
PromptsToRun []func(prompter.Prompter, *iostreams.IOStreams) error
|
||||
}
|
||||
|
||||
func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobra.Command {
|
||||
opts := &prompterOptions{
|
||||
IO: f.IOStreams,
|
||||
Config: f.Config,
|
||||
}
|
||||
|
||||
const (
|
||||
selectPrompt = "select"
|
||||
multiSelectPrompt = "multi-select"
|
||||
inputPrompt = "input"
|
||||
passwordPrompt = "password"
|
||||
confirmPrompt = "confirm"
|
||||
authTokenPrompt = "auth-token"
|
||||
confirmDeletionPrompt = "confirm-deletion"
|
||||
inputHostnamePrompt = "input-hostname"
|
||||
markdownEditorPrompt = "markdown-editor"
|
||||
)
|
||||
|
||||
prompterTypeFuncMap := map[string]func(prompter.Prompter, *iostreams.IOStreams) error{
|
||||
selectPrompt: runSelect,
|
||||
multiSelectPrompt: runMultiSelect,
|
||||
inputPrompt: runInput,
|
||||
passwordPrompt: runPassword,
|
||||
confirmPrompt: runConfirm,
|
||||
authTokenPrompt: runAuthToken,
|
||||
confirmDeletionPrompt: runConfirmDeletion,
|
||||
inputHostnamePrompt: runInputHostname,
|
||||
markdownEditorPrompt: runMarkdownEditor,
|
||||
}
|
||||
|
||||
allPromptsOrder := []string{
|
||||
selectPrompt,
|
||||
multiSelectPrompt,
|
||||
inputPrompt,
|
||||
passwordPrompt,
|
||||
confirmPrompt,
|
||||
authTokenPrompt,
|
||||
confirmDeletionPrompt,
|
||||
inputHostnamePrompt,
|
||||
markdownEditorPrompt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "prompter [prompt type]",
|
||||
Short: "Execute a test program to preview the prompter",
|
||||
Long: heredoc.Doc(`
|
||||
Execute a test program to preview the prompter.
|
||||
Without an argument, all prompts will be run.
|
||||
|
||||
Available prompt types:
|
||||
- select
|
||||
- multi-select
|
||||
- input
|
||||
- password
|
||||
- confirm
|
||||
- auth-token
|
||||
- confirm-deletion
|
||||
- input-hostname
|
||||
- markdown-editor
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
// All prompts, in a fixed order
|
||||
for _, promptType := range allPromptsOrder {
|
||||
f := prompterTypeFuncMap[promptType]
|
||||
opts.PromptsToRun = append(opts.PromptsToRun, f)
|
||||
}
|
||||
} else {
|
||||
// Only the one specified
|
||||
for _, arg := range args {
|
||||
f, ok := prompterTypeFuncMap[arg]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown prompter type: %q", arg)
|
||||
}
|
||||
opts.PromptsToRun = append(opts.PromptsToRun, f)
|
||||
}
|
||||
}
|
||||
|
||||
return prompterRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func prompterRun(opts *prompterOptions) error {
|
||||
editor, err := cmdutil.DetermineEditor(opts.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := prompter.New(editor, opts.IO)
|
||||
|
||||
for _, f := range opts.PromptsToRun {
|
||||
if err := f(p, opts.IO); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSelect(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Single Select")
|
||||
cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"}
|
||||
favorite, err := p.Select("Favorite cuisine?", "Italian", cuisines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(io.Out, "Favorite cuisine: %s\n", cuisines[favorite])
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMultiSelect(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Multi Select")
|
||||
cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"}
|
||||
favorites, err := p.MultiSelect("Favorite cuisines?", []string{}, cuisines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range favorites {
|
||||
fmt.Fprintf(io.Out, "Favorite cuisine: %s\n", cuisines[f])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runInput(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Text Input")
|
||||
text, err := p.Input("Favorite meal?", "Breakfast")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(io.Out, "You typed: %s\n", text)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runPassword(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Password Input")
|
||||
safeword, err := p.Password("Safe word?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(io.Out, "Safe word: %s\n", safeword)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runConfirm(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Confirmation")
|
||||
confirmation, err := p.Confirm("Are you sure?", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(io.Out, "Confirmation: %t\n", confirmation)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAuthToken(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Auth Token (can't be blank)")
|
||||
token, err := p.AuthToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(io.Out, "Auth token: %s\n", token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runConfirmDeletion(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Deletion Confirmation")
|
||||
err := p.ConfirmDeletion("delete-me")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(io.Out, "Item deleted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runInputHostname(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
fmt.Fprintln(io.Out, "Demonstrating Hostname")
|
||||
hostname, err := p.InputHostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(io.Out, "Hostname: %s\n", hostname)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMarkdownEditor(p prompter.Prompter, io *iostreams.IOStreams) error {
|
||||
defaultText := "default text value"
|
||||
|
||||
fmt.Fprintln(io.Out, "Demonstrating Markdown Editor with blanks allowed and default text")
|
||||
editorText, err := p.MarkdownEditor("Edit your text:", defaultText, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(io.Out, "Returned text: %s\n\n", editorText)
|
||||
|
||||
fmt.Fprintln(io.Out, "Demonstrating Markdown Editor with blanks disallowed and default text")
|
||||
editorText2, err := p.MarkdownEditor("Edit your text:", defaultText, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(io.Out, "Returned text: %s\n\n", editorText2)
|
||||
|
||||
fmt.Fprintln(io.Out, "Demonstrating Markdown Editor with blanks disallowed and no default text")
|
||||
editorText3, err := p.MarkdownEditor("Edit your text:", "", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(io.Out, "Returned text: %s\n", editorText3)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -165,10 +165,24 @@ func downloadRun(opts *DownloadOptions) error {
|
|||
var toDownload []shared.ReleaseAsset
|
||||
isArchive := false
|
||||
if opts.ArchiveType != "" {
|
||||
var archiveURL = release.ZipballURL
|
||||
var archiveURL string
|
||||
if opts.ArchiveType == "tar.gz" {
|
||||
archiveURL = release.TarballURL
|
||||
} else {
|
||||
archiveURL = release.ZipballURL
|
||||
}
|
||||
|
||||
if archiveURL == "" {
|
||||
errMessage := fmt.Sprintf(
|
||||
"release %q with tag %q, does not have a %q archive asset.",
|
||||
release.Name, release.TagName, opts.ArchiveType,
|
||||
)
|
||||
if release.IsDraft {
|
||||
errMessage += " Most likely, this is because it is a draft."
|
||||
}
|
||||
return errors.New(errMessage)
|
||||
}
|
||||
|
||||
// create pseudo-Asset with no name and pointing to ZipBallURL or TarBallURL
|
||||
toDownload = append(toDownload, shared.ReleaseAsset{APIURL: archiveURL})
|
||||
isArchive = true
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ func Test_downloadRun(t *testing.T) {
|
|||
name string
|
||||
isTTY bool
|
||||
opts DownloadOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
wantErr string
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
|
|
@ -196,6 +197,24 @@ func Test_downloadRun(t *testing.T) {
|
|||
Destination: ".",
|
||||
Concurrency: 2,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
|
||||
"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
|
||||
}`)
|
||||
|
||||
reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
|
||||
reg.Register(httpmock.REST("GET", "assets/3456"), httpmock.StringResponse(`3456`))
|
||||
reg.Register(httpmock.REST("GET", "assets/5678"), httpmock.StringResponse(`5678`))
|
||||
},
|
||||
wantStdout: ``,
|
||||
wantStderr: ``,
|
||||
wantFiles: []string{
|
||||
|
|
@ -213,6 +232,23 @@ func Test_downloadRun(t *testing.T) {
|
|||
Destination: "tmp/assets",
|
||||
Concurrency: 2,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
|
||||
"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
|
||||
}`)
|
||||
|
||||
reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
|
||||
reg.Register(httpmock.REST("GET", "assets/3456"), httpmock.StringResponse(`3456`))
|
||||
},
|
||||
wantStdout: ``,
|
||||
wantStderr: ``,
|
||||
wantFiles: []string{
|
||||
|
|
@ -229,6 +265,20 @@ func Test_downloadRun(t *testing.T) {
|
|||
Destination: ".",
|
||||
Concurrency: 2,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
|
||||
"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
|
||||
}`)
|
||||
},
|
||||
wantStdout: ``,
|
||||
wantStderr: ``,
|
||||
wantErr: "no assets match the file pattern",
|
||||
|
|
@ -242,6 +292,30 @@ func Test_downloadRun(t *testing.T) {
|
|||
Destination: "tmp/packages",
|
||||
Concurrency: 2,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
|
||||
"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
|
||||
}`)
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST(
|
||||
"GET",
|
||||
"repos/OWNER/REPO/zipball/v1.2.3",
|
||||
),
|
||||
httpmock.WithHeader(
|
||||
httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=zipball.zip",
|
||||
),
|
||||
)
|
||||
},
|
||||
wantStdout: ``,
|
||||
wantStderr: ``,
|
||||
wantFiles: []string{
|
||||
|
|
@ -257,6 +331,30 @@ func Test_downloadRun(t *testing.T) {
|
|||
Destination: "tmp/packages",
|
||||
Concurrency: 2,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
|
||||
"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
|
||||
}`)
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST(
|
||||
"GET",
|
||||
"repos/OWNER/REPO/tarball/v1.2.3",
|
||||
),
|
||||
httpmock.WithHeader(
|
||||
httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=tarball.tgz",
|
||||
),
|
||||
)
|
||||
},
|
||||
wantStdout: ``,
|
||||
wantStderr: ``,
|
||||
wantFiles: []string{
|
||||
|
|
@ -273,6 +371,30 @@ func Test_downloadRun(t *testing.T) {
|
|||
Concurrency: 2,
|
||||
ArchiveType: "tar.gz",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
|
||||
"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
|
||||
}`)
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST(
|
||||
"GET",
|
||||
"repos/OWNER/REPO/tarball/v1.2.3",
|
||||
),
|
||||
httpmock.WithHeader(
|
||||
httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=tarball.tgz",
|
||||
),
|
||||
)
|
||||
},
|
||||
wantStdout: ``,
|
||||
wantStderr: ``,
|
||||
wantFiles: []string{
|
||||
|
|
@ -289,6 +411,22 @@ func Test_downloadRun(t *testing.T) {
|
|||
Concurrency: 2,
|
||||
FilePatterns: []string{"*windows-32bit.zip"},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
|
||||
"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
|
||||
}`)
|
||||
|
||||
reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
|
||||
},
|
||||
wantStdout: ``,
|
||||
wantStderr: ``,
|
||||
wantFiles: []string{
|
||||
|
|
@ -305,9 +443,85 @@ func Test_downloadRun(t *testing.T) {
|
|||
Concurrency: 2,
|
||||
FilePatterns: []string{"*windows-32bit.zip"},
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
|
||||
"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
|
||||
}`)
|
||||
|
||||
reg.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
|
||||
},
|
||||
wantStdout: `1234`,
|
||||
wantStderr: ``,
|
||||
},
|
||||
{
|
||||
name: "draft release with null tarball_url and zipball_url",
|
||||
isTTY: true,
|
||||
opts: DownloadOptions{
|
||||
TagName: "v1.2.3",
|
||||
ArchiveType: "tar.gz",
|
||||
Destination: "tmp/packages",
|
||||
Concurrency: 2,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"tag_name": "v1.2.3",
|
||||
"name": "patch-36",
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": null,
|
||||
"zipball_url": null,
|
||||
"draft": true
|
||||
}`)
|
||||
},
|
||||
wantStdout: ``,
|
||||
wantStderr: ``,
|
||||
wantErr: "release \"patch-36\" with tag \"v1.2.3\", does not have a \"tar.gz\" archive asset. Most likely, this is because it is a draft.",
|
||||
},
|
||||
{
|
||||
name: "non-draft release with null tarball_url and zipball_url",
|
||||
isTTY: true,
|
||||
opts: DownloadOptions{
|
||||
TagName: "v1.2.3",
|
||||
ArchiveType: "tar.gz",
|
||||
Destination: "tmp/packages",
|
||||
Concurrency: 2,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
shared.StubFetchRelease(t, reg, "OWNER", "REPO", "v1.2.3", `{
|
||||
"tag_name": "v1.2.3",
|
||||
"name": "patch-36",
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": null,
|
||||
"zipball_url": null,
|
||||
"draft": false
|
||||
}`)
|
||||
},
|
||||
wantStdout: ``,
|
||||
wantStderr: ``,
|
||||
wantErr: "release \"patch-36\" with tag \"v1.2.3\", does not have a \"tar.gz\" archive asset.",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -324,41 +538,11 @@ func Test_downloadRun(t *testing.T) {
|
|||
ios.SetStderrTTY(tt.isTTY)
|
||||
|
||||
fakeHTTP := &httpmock.Registry{}
|
||||
shared.StubFetchRelease(t, fakeHTTP, "OWNER", "REPO", tt.opts.TagName, `{
|
||||
"assets": [
|
||||
{ "name": "windows-32bit.zip", "size": 12,
|
||||
"url": "https://api.github.com/assets/1234" },
|
||||
{ "name": "windows-64bit.zip", "size": 34,
|
||||
"url": "https://api.github.com/assets/3456" },
|
||||
{ "name": "linux.tgz", "size": 56,
|
||||
"url": "https://api.github.com/assets/5678" }
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/OWNER/REPO/tarball/v1.2.3",
|
||||
"zipball_url": "https://api.github.com/repos/OWNER/REPO/zipball/v1.2.3"
|
||||
}`)
|
||||
fakeHTTP.Register(httpmock.REST("GET", "assets/1234"), httpmock.StringResponse(`1234`))
|
||||
fakeHTTP.Register(httpmock.REST("GET", "assets/3456"), httpmock.StringResponse(`3456`))
|
||||
fakeHTTP.Register(httpmock.REST("GET", "assets/5678"), httpmock.StringResponse(`5678`))
|
||||
defer fakeHTTP.Verify(t)
|
||||
|
||||
fakeHTTP.Register(
|
||||
httpmock.REST(
|
||||
"GET",
|
||||
"repos/OWNER/REPO/tarball/v1.2.3",
|
||||
),
|
||||
httpmock.WithHeader(
|
||||
httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=tarball.tgz",
|
||||
),
|
||||
)
|
||||
|
||||
fakeHTTP.Register(
|
||||
httpmock.REST(
|
||||
"GET",
|
||||
"repos/OWNER/REPO/zipball/v1.2.3",
|
||||
),
|
||||
httpmock.WithHeader(
|
||||
httpmock.StringResponse("somedata"), "content-disposition", "attachment; filename=zipball.zip",
|
||||
),
|
||||
)
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(fakeHTTP)
|
||||
}
|
||||
|
||||
tt.opts.IO = ios
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
)
|
||||
|
||||
func TestDedent(t *testing.T) {
|
||||
|
|
@ -44,3 +57,68 @@ func TestDedent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since our online docs website renders pages by using the kramdown (a superset
|
||||
// of Markdown) engine, we have to check against some known quirks of the
|
||||
// syntax.
|
||||
func TestKramdownCompatibleDocs(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
||||
Browser: &browser.Stub{},
|
||||
ExtensionManager: &extensions.ExtensionManagerMock{
|
||||
ListFunc: func() []extensions.Extension {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cmd, err := NewCmdRoot(f, "N/A", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
var walk func(*cobra.Command)
|
||||
walk = func(cmd *cobra.Command) {
|
||||
name := fmt.Sprintf("%q: test pipes are in code blocks", cmd.UseLine())
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assertPipesAreInCodeBlocks(t, cmd)
|
||||
})
|
||||
for _, child := range cmd.Commands() {
|
||||
walk(child)
|
||||
}
|
||||
}
|
||||
|
||||
walk(cmd)
|
||||
}
|
||||
|
||||
// If not in a code block or a code span, kramdown treats pipes ("|") as table
|
||||
// column separators, even if there's no table header, or left/right table row
|
||||
// borders (i.e. lines starting and ending with a pipe).
|
||||
//
|
||||
// We need to assert there's no pipe in the text unless it's in a code-block or
|
||||
// code-span.
|
||||
//
|
||||
// (See https://github.com/cli/cli/issues/10348)
|
||||
func assertPipesAreInCodeBlocks(t *testing.T, cmd *cobra.Command) {
|
||||
md := goldmark.New()
|
||||
reader := text.NewReader([]byte(cmd.Long))
|
||||
doc := md.Parser().Parse(reader)
|
||||
|
||||
var checkNode func(node ast.Node)
|
||||
checkNode = func(node ast.Node) {
|
||||
if node.Kind() == ast.KindCodeSpan || node.Kind() == ast.KindCodeBlock {
|
||||
return
|
||||
}
|
||||
|
||||
if node.Kind() == ast.KindText {
|
||||
text := string(node.(*ast.Text).Segment.Value(reader.Source()))
|
||||
require.NotContains(t, text, "|", `found pipe ("|") in plain text in %q docs`, cmd.CommandPath())
|
||||
}
|
||||
|
||||
for child := node.FirstChild(); child != nil; child = child.NextSibling() {
|
||||
checkNode(child)
|
||||
}
|
||||
}
|
||||
|
||||
checkNode(doc)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
labelCmd "github.com/cli/cli/v2/pkg/cmd/label"
|
||||
orgCmd "github.com/cli/cli/v2/pkg/cmd/org"
|
||||
prCmd "github.com/cli/cli/v2/pkg/cmd/pr"
|
||||
previewCmd "github.com/cli/cli/v2/pkg/cmd/preview"
|
||||
projectCmd "github.com/cli/cli/v2/pkg/cmd/project"
|
||||
releaseCmd "github.com/cli/cli/v2/pkg/cmd/release"
|
||||
repoCmd "github.com/cli/cli/v2/pkg/cmd/repo"
|
||||
|
|
@ -141,6 +142,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
|
|||
cmd.AddCommand(statusCmd.NewCmdStatus(f, nil))
|
||||
cmd.AddCommand(codespaceCmd.NewCmdCodespace(f))
|
||||
cmd.AddCommand(projectCmd.NewCmdProject(f))
|
||||
cmd.AddCommand(previewCmd.NewCmdPreview(f))
|
||||
|
||||
// below here at the commands that require the "intelligent" BaseRepo resolver
|
||||
repoResolvingCmdFactory := *f
|
||||
|
|
|
|||
|
|
@ -47,6 +47,48 @@ func RenderJobs(cs *iostreams.ColorScheme, jobs []Job, verbose bool) string {
|
|||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func RenderJobsCompact(cs *iostreams.ColorScheme, jobs []Job) string {
|
||||
lines := []string{}
|
||||
for _, job := range jobs {
|
||||
elapsed := job.CompletedAt.Sub(job.StartedAt)
|
||||
elapsedStr := fmt.Sprintf(" in %s", elapsed)
|
||||
if elapsed < 0 {
|
||||
elapsedStr = ""
|
||||
}
|
||||
symbol, symbolColor := Symbol(cs, job.Status, job.Conclusion)
|
||||
id := cs.Cyanf("%d", job.ID)
|
||||
lines = append(lines, fmt.Sprintf("%s %s%s (ID %s)", symbolColor(symbol), cs.Bold(job.Name), elapsedStr, id))
|
||||
|
||||
if job.Status == Completed && job.Conclusion == Success {
|
||||
continue
|
||||
}
|
||||
|
||||
var inProgressStepLine string
|
||||
var failedStepLines []string
|
||||
|
||||
for _, step := range job.Steps {
|
||||
stepSymbol, stepSymColor := Symbol(cs, step.Status, step.Conclusion)
|
||||
stepLine := fmt.Sprintf(" %s %s", stepSymColor(stepSymbol), step.Name)
|
||||
|
||||
if IsFailureState(step.Conclusion) {
|
||||
failedStepLines = append(failedStepLines, stepLine)
|
||||
}
|
||||
|
||||
if step.Status == InProgress {
|
||||
inProgressStepLine = stepLine
|
||||
}
|
||||
}
|
||||
|
||||
lines = append(lines, failedStepLines...)
|
||||
|
||||
if inProgressStepLine != "" {
|
||||
lines = append(lines, inProgressStepLine)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func RenderAnnotations(cs *iostreams.ColorScheme, annotations []Annotation) string {
|
||||
lines := []string{}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ type WatchOptions struct {
|
|||
RunID string
|
||||
Interval int
|
||||
ExitStatus bool
|
||||
Compact bool
|
||||
|
||||
Prompt bool
|
||||
|
||||
|
|
@ -48,6 +49,9 @@ func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Comm
|
|||
Long: heredoc.Docf(`
|
||||
Watch a run until it completes, showing its progress.
|
||||
|
||||
By default, all steps are displayed. The %[1]s--compact%[1]s option can be used to only
|
||||
show the relevant/failed steps.
|
||||
|
||||
This command does not support authenticating via fine grained PATs
|
||||
as it is not currently possible to create a PAT with the %[1]schecks:read%[1]s permission.
|
||||
`, "`"),
|
||||
|
|
@ -55,6 +59,9 @@ func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Comm
|
|||
# Watch a run until it's done
|
||||
$ gh run watch
|
||||
|
||||
# Watch a run in compact mode
|
||||
$ gh run watch --compact
|
||||
|
||||
# Run some other command when the run is finished
|
||||
$ gh run watch && notify-send 'run is done!'
|
||||
`),
|
||||
|
|
@ -78,6 +85,7 @@ func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Comm
|
|||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&opts.ExitStatus, "exit-status", false, "Exit with non-zero status if run fails")
|
||||
cmd.Flags().BoolVar(&opts.Compact, "compact", false, "Show only relevant/failed steps")
|
||||
cmd.Flags().IntVarP(&opts.Interval, "interval", "i", defaultInterval, "Refresh interval in seconds")
|
||||
|
||||
return cmd
|
||||
|
|
@ -252,8 +260,11 @@ func renderRun(out io.Writer, opts WatchOptions, client *api.Client, repo ghrepo
|
|||
}
|
||||
|
||||
fmt.Fprintln(out, cs.Bold("JOBS"))
|
||||
|
||||
fmt.Fprintln(out, shared.RenderJobs(cs, jobs, true))
|
||||
if opts.Compact {
|
||||
fmt.Fprintln(out, shared.RenderJobsCompact(cs, jobs))
|
||||
} else {
|
||||
fmt.Fprintln(out, shared.RenderJobs(cs, jobs, true))
|
||||
}
|
||||
|
||||
if missingAnnotationsPermissions {
|
||||
fmt.Fprintln(out)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,15 @@ func TestNewCmdWatch(t *testing.T) {
|
|||
ExitStatus: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "compact status",
|
||||
cli: "1234 --compact",
|
||||
wants: WatchOptions{
|
||||
Interval: defaultInterval,
|
||||
RunID: "1234",
|
||||
Compact: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue