Thanks to visit codestin.com
Credit goes to github.com

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
10 changes: 10 additions & 0 deletions internal/featuredetection/detector_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ func (md *DisabledDetectorMock) SearchFeatures() (SearchFeatures, error) {
return advancedIssueSearchNotSupported, nil
}

func (md *DisabledDetectorMock) ReleaseFeatures() (ReleaseFeatures, error) {
return ReleaseFeatures{}, nil
}

type EnabledDetectorMock struct{}

func (md *EnabledDetectorMock) IssueFeatures() (IssueFeatures, error) {
Expand All @@ -46,6 +50,12 @@ func (md *EnabledDetectorMock) SearchFeatures() (SearchFeatures, error) {
return advancedIssueSearchNotSupported, nil
}

func (md *EnabledDetectorMock) ReleaseFeatures() (ReleaseFeatures, error) {
return ReleaseFeatures{
ImmutableReleases: true,
}, nil
}

type AdvancedIssueSearchDetectorMock struct {
EnabledDetectorMock
searchFeatures SearchFeatures
Expand Down
35 changes: 35 additions & 0 deletions internal/featuredetection/feature_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Detector interface {
RepositoryFeatures() (RepositoryFeatures, error)
ProjectsV1() gh.ProjectsV1Support
SearchFeatures() (SearchFeatures, error)
ReleaseFeatures() (ReleaseFeatures, error)
}

type IssueFeatures struct {
Expand Down Expand Up @@ -93,6 +94,10 @@ var advancedIssueSearchSupportedAsOnlyBackend = SearchFeatures{
AdvancedIssueSearchAPIOptIn: false,
}

type ReleaseFeatures struct {
ImmutableReleases bool
}

type detector struct {
host string
httpClient *http.Client
Expand Down Expand Up @@ -358,6 +363,36 @@ func (d *detector) SearchFeatures() (SearchFeatures, error) {
return feature, nil
}

func (d *detector) ReleaseFeatures() (ReleaseFeatures, error) {
// TODO: immutableReleaseFullSupport
// Once all supported GHES versions fully support immutable releases, we can
// remove this function, of course, unless there will be other release-related
// features that are not available on all GH hosts.

var releaseFeatureDetection struct {
Release struct {
Fields []struct {
Name string
} `graphql:"fields"`
} `graphql:"Release: __type(name: \"Release\")"`
}

gql := api.NewClientFromHTTP(d.httpClient)
if err := gql.Query(d.host, "Release_fields", &releaseFeatureDetection, nil); err != nil {
return ReleaseFeatures{}, err
}

for _, field := range releaseFeatureDetection.Release.Fields {
if field.Name == "immutable" {
return ReleaseFeatures{
ImmutableReleases: true,
}, nil
}
}

return ReleaseFeatures{}, nil
}

func resolveEnterpriseVersion(httpClient *http.Client, host string) (*version.Version, error) {
var metaResponse struct {
InstalledVersion string `json:"installed_version"`
Expand Down
111 changes: 111 additions & 0 deletions internal/featuredetection/feature_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,3 +585,114 @@ func TestAdvancedIssueSearchSupport(t *testing.T) {
})
}
}

func TestReleaseFeatures(t *testing.T) {
withImmutableReleaseSupport := `{"data":{"Release":{"fields":[{"name":"author"},{"name":"name"},{"name":"immutable"}]}}}`
withoutImmutableReleaseSupport := `{"data":{"Release":{"fields":[{"name":"author"},{"name":"name"}]}}}`

tests := []struct {
name string
hostname string
httpStubs func(*httpmock.Registry)
wantFeatures ReleaseFeatures
}{
{
// This is not a real case as `github.com` supports immutable releases.
name: "github.com, immutable releases unsupported",
hostname: "github.com",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query Release_fields\b`),
httpmock.StringResponse(withoutImmutableReleaseSupport),
)
},
wantFeatures: ReleaseFeatures{
ImmutableReleases: false,
},
},
{
name: "github.com, immutable releases supported",
hostname: "github.com",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query Release_fields\b`),
httpmock.StringResponse(withImmutableReleaseSupport),
)
},
wantFeatures: ReleaseFeatures{
ImmutableReleases: true,
},
},
{
// This is not a real case as `github.com` supports immutable releases.
name: "ghec data residency (ghe.com), immutable releases unsupported",
hostname: "stampname.ghe.com",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query Release_fields\b`),
httpmock.StringResponse(withoutImmutableReleaseSupport),
)
},
wantFeatures: ReleaseFeatures{
ImmutableReleases: false,
},
},
{
name: "ghec data residency (ghe.com), immutable releases supported",
hostname: "stampname.ghe.com",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query Release_fields\b`),
httpmock.StringResponse(withImmutableReleaseSupport),
)
},
wantFeatures: ReleaseFeatures{
ImmutableReleases: true,
},
},
{
name: "GHE, immutable releases unsupported",
hostname: "git.my.org",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query Release_fields\b`),
httpmock.StringResponse(withoutImmutableReleaseSupport),
)
},
wantFeatures: ReleaseFeatures{
ImmutableReleases: false,
},
},
{
name: "GHE, immutable releases supported",
hostname: "git.my.org",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query Release_fields\b`),
httpmock.StringResponse(withImmutableReleaseSupport),
)
},
wantFeatures: ReleaseFeatures{
ImmutableReleases: true,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
reg := &httpmock.Registry{}
if tt.httpStubs != nil {
tt.httpStubs(reg)
}
httpClient := &http.Client{}
httpmock.ReplaceTripper(httpClient, reg)

detector := NewDetector(httpClient, tt.hostname)

features, err := detector.ReleaseFeatures()
require.NoError(t, err)
require.Equal(t, tt.wantFeatures, features)
})
}
}
110 changes: 109 additions & 1 deletion pkg/cmd/release/list/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/cli/cli/v2/api"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/shurcooL/githubv4"
Expand All @@ -17,6 +18,7 @@ var releaseFields = []string{
"isDraft",
"isLatest",
"isPrerelease",
"isImmutable",
"createdAt",
"publishedAt",
}
Expand All @@ -25,6 +27,7 @@ type Release struct {
Name string
TagName string
IsDraft bool
IsImmutable bool `graphql:"immutable"`
IsLatest bool
IsPrerelease bool
CreatedAt time.Time
Expand All @@ -35,7 +38,27 @@ func (r *Release) ExportData(fields []string) map[string]interface{} {
return cmdutil.StructExportData(r, fields)
}

func fetchReleases(httpClient *http.Client, repo ghrepo.Interface, limit int, excludeDrafts bool, excludePreReleases bool, order string) ([]Release, error) {
func fetchReleases(httpClient *http.Client, repo ghrepo.Interface, limit int, excludeDrafts bool, excludePreReleases bool, order string, releaseFeatures fd.ReleaseFeatures) ([]Release, error) {
// TODO: immutableReleaseFullSupport
// This is a temporary workaround until all supported GHES versions fully
// support immutable releases, which would probably be when GHES 3.18 goes
// EOL. At that point we can remove this if statement.
//
// Note 1: This could have been done differently by using two separate query
// types or even using plain text/string queries. But, both would require us
// to refactor them back in the future, to the single, strongly-typed query
// approach as it was before. So, duplicating the entire function for now
// seems like the lesser evil, with a quicker and less risky clean up in the
// near future.
//
// Note 2: We couldn't use GraphQL directives like `@include(condition)` or
// `@skip(condition)` here because if the field doesn't exist on the schema
// then the whole query would still fail regardless of the condition being
// met or not.
if !releaseFeatures.ImmutableReleases {
return fetchReleasesWithoutImmutableReleases(httpClient, repo, limit, excludeDrafts, excludePreReleases, order)
}

type responseData struct {
Repository struct {
Releases struct {
Expand Down Expand Up @@ -93,3 +116,88 @@ loop:

return releases, nil
}

// TODO: immutableReleaseFullSupport
// This is a temporary workaround until all supported GHES versions fully
// support immutable releases, which would be when GHES 3.18 goes EOL. At that
// point we can remove this function.
func fetchReleasesWithoutImmutableReleases(httpClient *http.Client, repo ghrepo.Interface, limit int, excludeDrafts bool, excludePreReleases bool, order string) ([]Release, error) {
type releaseOld struct {
Name string
TagName string
IsDraft bool
IsLatest bool
IsPrerelease bool
CreatedAt time.Time
PublishedAt time.Time
}

fromReleaseOld := func(old releaseOld) Release {
return Release{
Name: old.Name,
TagName: old.TagName,
IsDraft: old.IsDraft,
IsLatest: old.IsLatest,
IsPrerelease: old.IsPrerelease,
CreatedAt: old.CreatedAt,
PublishedAt: old.PublishedAt,
}
}

type responseData struct {
Repository struct {
Releases struct {
Nodes []releaseOld
PageInfo struct {
HasNextPage bool
EndCursor string
}
} `graphql:"releases(first: $perPage, orderBy: {field: CREATED_AT, direction: $direction}, after: $endCursor)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}

perPage := limit
if limit > 100 {
perPage = 100
}

variables := map[string]interface{}{
"owner": githubv4.String(repo.RepoOwner()),
"name": githubv4.String(repo.RepoName()),
"perPage": githubv4.Int(perPage),
"endCursor": (*githubv4.String)(nil),
"direction": githubv4.OrderDirection(strings.ToUpper(order)),
}

gql := api.NewClientFromHTTP(httpClient)

var releases []Release
loop:
for {
var query responseData
err := gql.Query(repo.RepoHost(), "RepositoryReleaseList", &query, variables)
if err != nil {
return nil, err
}

for _, r := range query.Repository.Releases.Nodes {
if excludeDrafts && r.IsDraft {
continue
}
if excludePreReleases && r.IsPrerelease {
continue
}
releases = append(releases, fromReleaseOld(r))
if len(releases) == limit {
break loop
}
}

if !query.Repository.Releases.PageInfo.HasNextPage {
break
}
variables["endCursor"] = githubv4.String(query.Repository.Releases.PageInfo.EndCursor)
}

return releases, nil
}
18 changes: 17 additions & 1 deletion pkg/cmd/release/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"net/http"
"time"

"github.com/cli/cli/v2/api"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/tableprinter"
"github.com/cli/cli/v2/internal/text"
Expand All @@ -19,6 +21,7 @@ type ListOptions struct {
BaseRepo func() (ghrepo.Interface, error)

Exporter cmdutil.Exporter
Detector fd.Detector

LimitResults int
ExcludeDrafts bool
Expand Down Expand Up @@ -68,7 +71,20 @@ func listRun(opts *ListOptions) error {
return err
}

releases, err := fetchReleases(httpClient, baseRepo, opts.LimitResults, opts.ExcludeDrafts, opts.ExcludePreReleases, opts.Order)
// TODO: immutableReleaseFullSupport
// The detector is not needed when covered GHES versions fully support
// immutable releases (probably when 3.18 goes EOL).
if opts.Detector == nil {
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
}

releaseFeatures, err := opts.Detector.ReleaseFeatures()
if err != nil {
return err
}

releases, err := fetchReleases(httpClient, baseRepo, opts.LimitResults, opts.ExcludeDrafts, opts.ExcludePreReleases, opts.Order, releaseFeatures)
if err != nil {
return err
}
Expand Down
Loading