Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ GitHub CLI is supported for users on GitHub.com, GitHub Enterprise Cloud, and Gi

## Documentation

For [installation options see below](#installation), for usage instructions [see the manual]( https://cli.github.com/manual/).
For [installation options see below](#installation), for usage instructions [see the manual](https://cli.github.com/manual/).

## Contributing

Expand Down
41 changes: 41 additions & 0 deletions api/queries_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,47 @@ func FetchRepository(client *Client, repo ghrepo.Interface, fields []string) (*R
return InitRepoHostname(result.Repository, repo.RepoHost()), nil
}

// IssueRepoInfo fetches only the repository fields needed for issue operations such as
// issue creation and transfer, avoiding fields like defaultBranchRef that require additional
// token permissions.
func IssueRepoInfo(client *Client, repo ghrepo.Interface) (*Repository, error) {
query := `
query IssueRepositoryInfo($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
name
owner { login }
hasIssuesEnabled
viewerPermission
}
}`
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"name": repo.RepoName(),
}

var result struct {
Repository *Repository
}
if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil {
return nil, err
}
// The GraphQL API should have returned an error in case of a missing repository, but this isn't
// guaranteed to happen when an authentication token with insufficient permissions is being used.
if result.Repository == nil {
return nil, GraphQLError{
GraphQLError: &ghAPI.GraphQLError{
Errors: []ghAPI.GraphQLErrorItem{{
Type: "NOT_FOUND",
Message: fmt.Sprintf("Could not resolve to a Repository with the name '%s/%s'.", repo.RepoOwner(), repo.RepoName()),
}},
},
}
}

return InitRepoHostname(result.Repository, repo.RepoHost()), nil
}

func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
query := `
fragment repo on Repository {
Expand Down
182 changes: 174 additions & 8 deletions api/queries_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,181 @@ func TestGitHubRepo_notFound(t *testing.T) {

client := newTestClient(httpReg)
repo, err := GitHubRepo(client, ghrepo.New("OWNER", "REPO"))
if err == nil {
t.Fatal("GitHubRepo did not return an error")
}
if wants := "GraphQL: Could not resolve to a Repository with the name 'OWNER/REPO'."; err.Error() != wants {
t.Errorf("GitHubRepo error: want %q, got %q", wants, err.Error())
}
if repo != nil {
t.Errorf("GitHubRepo: expected nil repo, got %v", repo)
require.EqualError(t, err, "GraphQL: Could not resolve to a Repository with the name 'OWNER/REPO'.")
assert.Nil(t, repo)
}

func TestGitHubRepo_success(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)

httpReg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login": "OWNER"},
"hasIssuesEnabled": true,
"description": "a cool repo",
"hasWikiEnabled": true,
"viewerPermission": "ADMIN",
"defaultBranchRef": {"name": "main"},
"parent": null,
"mergeCommitAllowed": true,
"rebaseMergeAllowed": true,
"squashMergeAllowed": false
} } }`))

client := newTestClient(httpReg)
repo, err := GitHubRepo(client, ghrepo.New("OWNER", "REPO"))
require.NoError(t, err)
assert.Equal(t, &Repository{
ID: "REPOID",
Name: "REPO",
Owner: RepositoryOwner{Login: "OWNER"},
HasIssuesEnabled: true,
Description: "a cool repo",
HasWikiEnabled: true,
ViewerPermission: "ADMIN",
DefaultBranchRef: BranchRef{Name: "main"},
MergeCommitAllowed: true,
RebaseMergeAllowed: true,
hostname: "github.com",
}, repo)
assert.True(t, repo.ViewerCanPush())
assert.True(t, repo.ViewerCanTriage())
}

func TestGitHubRepo_withParent(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)

httpReg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login": "OWNER"},
"hasIssuesEnabled": true,
"description": "",
"hasWikiEnabled": false,
"viewerPermission": "READ",
"defaultBranchRef": {"name": "main"},
"parent": {
"id": "PARENTID",
"name": "PARENT-REPO",
"owner": {"login": "PARENT-OWNER"},
"hasIssuesEnabled": true,
"description": "parent repo",
"hasWikiEnabled": true,
"viewerPermission": "READ",
"defaultBranchRef": {"name": "develop"}
},
"mergeCommitAllowed": false,
"rebaseMergeAllowed": false,
"squashMergeAllowed": true
} } }`))

client := newTestClient(httpReg)
repo, err := GitHubRepo(client, ghrepo.New("OWNER", "REPO"))
require.NoError(t, err)
wantParent := &Repository{
ID: "PARENTID",
Name: "PARENT-REPO",
Owner: RepositoryOwner{Login: "PARENT-OWNER"},
HasIssuesEnabled: true,
Description: "parent repo",
HasWikiEnabled: true,
ViewerPermission: "READ",
DefaultBranchRef: BranchRef{Name: "develop"},
hostname: "github.com",
}
assert.Equal(t, &Repository{
ID: "REPOID",
Name: "REPO",
Owner: RepositoryOwner{Login: "OWNER"},
HasIssuesEnabled: true,
ViewerPermission: "READ",
DefaultBranchRef: BranchRef{Name: "main"},
Parent: wantParent,
SquashMergeAllowed: true,
hostname: "github.com",
}, repo)
assert.False(t, repo.ViewerCanPush())
assert.False(t, repo.ViewerCanTriage())
}

func TestIssueRepoInfo_notFound(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)

httpReg.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`{ "data": { "repository": null } }`))

client := newTestClient(httpReg)
repo, err := IssueRepoInfo(client, ghrepo.New("OWNER", "REPO"))
require.EqualError(t, err, "GraphQL: Could not resolve to a Repository with the name 'OWNER/REPO'.")
assert.Nil(t, repo)
}

func TestIssueRepoInfo_success(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)

httpReg.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login": "OWNER"},
"hasIssuesEnabled": true,
"viewerPermission": "WRITE"
} } }`))

client := newTestClient(httpReg)
repo, err := IssueRepoInfo(client, ghrepo.New("OWNER", "REPO"))
require.NoError(t, err)
assert.Equal(t, &Repository{
ID: "REPOID",
Name: "REPO",
Owner: RepositoryOwner{Login: "OWNER"},
HasIssuesEnabled: true,
ViewerPermission: "WRITE",
hostname: "github.com",
}, repo)
assert.True(t, repo.ViewerCanTriage())
}

func TestIssueRepoInfo_issuesDisabled(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)

httpReg.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login": "OWNER"},
"hasIssuesEnabled": false,
"viewerPermission": "READ"
} } }`))

client := newTestClient(httpReg)
repo, err := IssueRepoInfo(client, ghrepo.New("OWNER", "REPO"))
require.NoError(t, err)
assert.Equal(t, &Repository{
ID: "REPOID",
Name: "REPO",
Owner: RepositoryOwner{Login: "OWNER"},
ViewerPermission: "READ",
hostname: "github.com",
}, repo)
assert.False(t, repo.ViewerCanTriage())
}

func Test_RepoMetadata(t *testing.T) {
Expand Down
32 changes: 16 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/gdamore/tcell/v2 v2.13.8
github.com/golang/snappy v1.0.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.7
github.com/google/go-containerregistry v0.21.3
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-version v1.8.0
Expand All @@ -49,19 +49,18 @@ require (
github.com/theupdateframework/go-tuf/v2 v2.4.1
github.com/vmihailenco/msgpack/v5 v5.4.1
github.com/yuin/goldmark v1.7.16
github.com/zalando/go-keyring v0.2.6
golang.org/x/crypto v0.48.0
github.com/zalando/go-keyring v0.2.8
golang.org/x/crypto v0.49.0
golang.org/x/sync v0.20.0
golang.org/x/term v0.40.0
golang.org/x/text v0.34.0
golang.org/x/term v0.41.0
golang.org/x/text v0.35.0
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v3 v3.0.1
)

require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
Expand All @@ -83,13 +82,13 @@ require (
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cli/browser v1.3.0 // indirect
github.com/cli/shurcooL-graphql v0.0.4 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/cli v29.2.0+incompatible // indirect
github.com/docker/cli v29.3.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
Expand Down Expand Up @@ -120,7 +119,7 @@ require (
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-openapi/validate v0.25.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
Expand All @@ -133,7 +132,7 @@ require (
github.com/itchyny/gojq v0.12.17 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.17 // indirect
Expand Down Expand Up @@ -162,7 +161,7 @@ require (
github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect
github.com/sigstore/sigstore v1.10.4 // indirect
github.com/sigstore/timestamp-authority/v2 v2.0.3 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/thlib/go-timezone-local v0.0.6 // indirect
Expand All @@ -178,10 +177,11 @@ require (
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/tools v0.41.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
gotest.tools/v3 v3.5.2 // indirect
)
Loading
Loading