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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ require (
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.0.3+incompatible // indirect
github.com/docker/cli v29.2.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
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E=
github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
Expand Down
52 changes: 5 additions & 47 deletions internal/featuredetection/feature_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,11 @@ type Detector interface {
}

type IssueFeatures struct {
StateReason bool
StateReasonDuplicate bool
ActorIsAssignable bool
ActorIsAssignable bool
}

var allIssueFeatures = IssueFeatures{
StateReason: true,
StateReasonDuplicate: true,
ActorIsAssignable: true,
ActorIsAssignable: true,
}

type PullRequestFeatures struct {
Expand Down Expand Up @@ -139,47 +135,9 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
return allIssueFeatures, nil
}

features := IssueFeatures{
StateReason: false,
StateReasonDuplicate: false,
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
}

var featureDetection struct {
Issue struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"Issue: __type(name: \"Issue\")"`
IssueClosedStateReason struct {
EnumValues []struct {
Name string
} `graphql:"enumValues(includeDeprecated: true)"`
} `graphql:"IssueClosedStateReason: __type(name: \"IssueClosedStateReason\")"`
}

gql := api.NewClientFromHTTP(d.httpClient)
err := gql.Query(d.host, "Issue_fields", &featureDetection, nil)
if err != nil {
return features, err
}

for _, field := range featureDetection.Issue.Fields {
if field.Name == "stateReason" {
features.StateReason = true
}
}

if features.StateReason {
for _, enumValue := range featureDetection.IssueClosedStateReason.EnumValues {
if enumValue.Name == "DUPLICATE" {
features.StateReasonDuplicate = true
break
}
}
}

return features, nil
return IssueFeatures{
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
}, nil
}

func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
Expand Down
58 changes: 4 additions & 54 deletions internal/featuredetection/feature_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,73 +23,23 @@ func TestIssueFeatures(t *testing.T) {
name: "github.com",
hostname: "github.com",
wantFeatures: IssueFeatures{
StateReason: true,
StateReasonDuplicate: true,
ActorIsAssignable: true,
ActorIsAssignable: true,
},
wantErr: false,
},
{
name: "ghec data residency (ghe.com)",
hostname: "stampname.ghe.com",
wantFeatures: IssueFeatures{
StateReason: true,
StateReasonDuplicate: true,
ActorIsAssignable: true,
ActorIsAssignable: true,
},
wantErr: false,
},
{
name: "GHE empty response",
hostname: "git.my.org",
queryResponse: map[string]string{
`query Issue_fields\b`: `{"data": {}}`,
},
wantFeatures: IssueFeatures{
StateReason: false,
StateReasonDuplicate: false,
ActorIsAssignable: false,
},
wantErr: false,
},
{
name: "GHE has state reason field without duplicate enum",
name: "GHE",
hostname: "git.my.org",
queryResponse: map[string]string{
`query Issue_fields\b`: heredoc.Doc(`
{ "data": { "Issue": { "fields": [
{"name": "stateReason"}
] }, "IssueClosedStateReason": { "enumValues": [
{"name": "COMPLETED"},
{"name": "NOT_PLANNED"}
] } } }
`),
},
wantFeatures: IssueFeatures{
StateReason: true,
StateReasonDuplicate: false,
ActorIsAssignable: false,
},
wantErr: false,
},
{
name: "GHE has duplicate state reason enum value",
hostname: "git.my.org",
queryResponse: map[string]string{
`query Issue_fields\b`: heredoc.Doc(`
{ "data": { "Issue": { "fields": [
{"name": "stateReason"}
] }, "IssueClosedStateReason": { "enumValues": [
{"name": "COMPLETED"},
{"name": "NOT_PLANNED"},
{"name": "DUPLICATE"}
] } } }
`),
},
wantFeatures: IssueFeatures{
StateReason: true,
StateReasonDuplicate: true,
ActorIsAssignable: false,
ActorIsAssignable: false,
},
wantErr: false,
},
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/extension/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ func (m *Manager) Install(repo ghrepo.Interface, target string) error {
return err
}
if !hs {
return fmt.Errorf("extension is not installable: no usable release artifact or script found in %s", repo)
return fmt.Errorf("extension is not installable: no usable release artifact or script found in %s", ghrepo.FullName(repo))
}

return m.installGit(repo, target)
Expand Down
33 changes: 33 additions & 0 deletions pkg/cmd/extension/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,39 @@ func TestManager_Install_git(t *testing.T) {
assert.NoDirExistsf(t, extensionUpdatePath, "update directory should be removed")
}

func TestManager_Install_not_installable(t *testing.T) {
dataDir := t.TempDir()
updateDir := t.TempDir()

reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}

ios, _, _, _ := iostreams.Test()

m := newTestManager(dataDir, updateDir, &client, nil, ios)

reg.Register(
httpmock.REST("GET", "repos/owner/gh-some-ext/releases/latest"),
httpmock.JSONResponse(
release{
Assets: []releaseAsset{
{
Name: "not-a-binary",
APIURL: "https://example.com/release/cool",
},
},
}))
reg.Register(
httpmock.REST("GET", "repos/owner/gh-some-ext/contents/gh-some-ext"),
httpmock.StatusStringResponse(404, "not found"))

repo := ghrepo.New("owner", "gh-some-ext")

err := m.Install(repo, "")
assert.EqualError(t, err, "extension is not installable: no usable release artifact or script found in owner/gh-some-ext")
}

func TestManager_Install_git_pinned(t *testing.T) {
dataDir := t.TempDir()
updateDir := t.TempDir()
Expand Down
33 changes: 2 additions & 31 deletions pkg/cmd/issue/close/close.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ package close
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/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/issue/shared"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
Expand All @@ -26,8 +24,6 @@ type CloseOptions struct {
Comment string
Reason string
DuplicateOf string

Detector fd.Detector
}

func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Command {
Expand Down Expand Up @@ -165,7 +161,7 @@ func closeRun(opts *CloseOptions) error {
}
}

err = apiClose(httpClient, baseRepo, issue, opts.Detector, closeReason, duplicateIssueID)
err = apiClose(httpClient, baseRepo, issue, closeReason, duplicateIssueID)
if err != nil {
return err
}
Expand All @@ -175,36 +171,11 @@ func closeRun(opts *CloseOptions) error {
return nil
}

func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue, detector fd.Detector, reason string, duplicateIssueID string) error {
func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue, reason string, duplicateIssueID string) error {
if issue.IsPullRequest() {
return api.PullRequestClose(httpClient, repo, issue.ID)
}

if reason != "" || duplicateIssueID != "" {
if detector == nil {
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
detector = fd.NewDetector(cachedClient, repo.RepoHost())
}
features, err := detector.IssueFeatures()
if err != nil {
return err
}
// TODO stateReasonCleanup
if !features.StateReason {
// If StateReason is not supported silently close issue without setting StateReason.
if duplicateIssueID != "" {
return fmt.Errorf("closing as duplicate is not supported on %s", repo.RepoHost())
}
reason = ""
} else if reason == "duplicate" && !features.StateReasonDuplicate {
if duplicateIssueID != "" {
return fmt.Errorf("closing as duplicate is not supported on %s", repo.RepoHost())
}
// If DUPLICATE is not supported silently close issue without setting StateReason.
reason = ""
}
}

switch reason {
case "":
// If no reason is specified do not set it.
Expand Down
Loading
Loading