diff --git a/go.mod b/go.mod index 0513d67d095..8bb5858aa1e 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/cli/cli/v2 go 1.26.0 -toolchain go1.26.7 +toolchain go1.26.8 require ( charm.land/bubbles/v2 v2.2.1 @@ -18,7 +18,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 github.com/charmbracelet/glamour v0.10.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 - github.com/cli/go-gh/v2 v2.15.0 + github.com/cli/go-gh/v2 v2.16.0 github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 github.com/cli/oauth v1.2.2 github.com/cli/safeexec v1.0.1 @@ -57,7 +57,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/yuin/goldmark v1.8.5 github.com/zalando/go-keyring v0.2.8 - golang.org/x/crypto v0.55.0 + golang.org/x/crypto v0.56.0 golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 golang.org/x/term v0.45.0 diff --git a/go.sum b/go.sum index 98f3fb40c09..0d36a8f0947 100644 --- a/go.sum +++ b/go.sum @@ -145,8 +145,8 @@ github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8 github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/cli/go-gh/v2 v2.15.0 h1:LF5lDLs6yLaUgUlvki/D9syUGYnYaJXjzYXpazTqYdw= -github.com/cli/go-gh/v2 v2.15.0/go.mod h1:OaJTFtHJapQq670h/3L0vqm4NwZGoJmSAVctWiY+3pQ= +github.com/cli/go-gh/v2 v2.16.0 h1:xaePUubgeuj4wKz87NIo+zFQtuB6566K8cAGTh0Ctjc= +github.com/cli/go-gh/v2 v2.16.0/go.mod h1:OaJTFtHJapQq670h/3L0vqm4NwZGoJmSAVctWiY+3pQ= github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 h1:QDrhR4JA2n3ij9YQN0u5ZeuvRIIvsUGmf5yPlTS0w8E= github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24/go.mod h1:rr9GNING0onuVw8MnracQHn7PcchnFlP882Y0II2KZk= github.com/cli/oauth v1.2.2 h1:/qG/wok8jzu66tx7q+duGOIp4DT5P/ACXrdc33UoNUQ= @@ -561,8 +561,8 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= 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.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= +golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/internal/attachments/flags.go b/internal/attachments/flags.go index c47117c79fc..10e42fd6fca 100644 --- a/internal/attachments/flags.go +++ b/internal/attachments/flags.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -39,6 +40,24 @@ func (f *Flag) Changed() bool { return f.flag.Changed } +// RecordTelemetry records how many attachment flags were provided. +func (f *Flag) RecordTelemetry(command string, recorder ghtelemetry.CommandRecorder) { + if recorder == nil || !f.Changed() { + return + } + + recorder.SetSampleRate(ghtelemetry.SAMPLE_ALL) + recorder.Record(ghtelemetry.Event{ + Type: "attachment_invocation", + Dimensions: ghtelemetry.Dimensions{ + "command": command, + }, + Measures: ghtelemetry.Measures{ + "attach_count": int64(len(f.values)), + }, + }) +} + // UserAssets validates the files named by the attachment flag, keeping them in // the order they were written. It returns nothing when the flag was not passed. func (f *Flag) UserAssets() ([]UserAsset, error) { diff --git a/internal/attachments/flags_test.go b/internal/attachments/flags_test.go index 1238acc1519..af22157715e 100644 --- a/internal/attachments/flags_test.go +++ b/internal/attachments/flags_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" + "github.com/cli/cli/v2/internal/telemetry" "github.com/google/shlex" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -88,6 +90,58 @@ func TestAddFlag(t *testing.T) { } } +func TestFlagRecordTelemetry(t *testing.T) { + tests := []struct { + name string + input string + wantEvent bool + wantCount int64 + }{ + { + name: "flag not passed", + input: "", + }, + { + name: "one attachment", + input: "--attach ./first.png", + wantEvent: true, + wantCount: 1, + }, + { + name: "several attachments before validation", + input: "--attach ./first.png --attach ./second.png --attach ./third.png", + wantEvent: true, + wantCount: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, attachFlag := attachCmd(t, tt.input) + recorder := &telemetry.CommandRecorderSpy{} + + attachFlag.RecordTelemetry("gh issue comment", recorder) + + if !tt.wantEvent { + assert.Empty(t, recorder.Events) + assert.Zero(t, recorder.LastSampleRate) + return + } + + require.Equal(t, ghtelemetry.SAMPLE_ALL, recorder.LastSampleRate) + require.Equal(t, []ghtelemetry.Event{{ + Type: "attachment_invocation", + Dimensions: ghtelemetry.Dimensions{ + "command": "gh issue comment", + }, + Measures: ghtelemetry.Measures{ + "attach_count": tt.wantCount, + }, + }}, recorder.Events) + }) + } +} + func TestFlagUserAssets(t *testing.T) { tests := []struct { name string diff --git a/internal/config/auth_config_test.go b/internal/config/auth_config_test.go index 58c938999c7..32884758b69 100644 --- a/internal/config/auth_config_test.go +++ b/internal/config/auth_config_test.go @@ -987,38 +987,38 @@ func TestHostForAPIHost(t *testing.T) { }{ { name: "no hosts configure an api_host", - lookup: "api.example.com", + lookup: "api-gateway.example.com", wantFound: false, }, { name: "a host configures the api_host", - apiHosts: map[string]string{"github.com": "api.example.com"}, - lookup: "api.example.com", + apiHosts: map[string]string{"github.com": "api-gateway.example.com"}, + lookup: "api-gateway.example.com", wantHost: "github.com", wantFound: true, }, { name: "matching is case insensitive", - apiHosts: map[string]string{"github.com": "API.example.com"}, - lookup: "api.example.com", + apiHosts: map[string]string{"github.com": "API-gateway.example.com"}, + lookup: "api-gateway.example.com", wantHost: "github.com", wantFound: true, }, { name: "an unrelated api_host does not match", - apiHosts: map[string]string{"github.com": "api.example.com"}, + apiHosts: map[string]string{"github.com": "api-gateway.example.com"}, lookup: "api.other.com", wantFound: false, }, { name: "an empty lookup matches nothing", - apiHosts: map[string]string{"github.com": "api.example.com"}, + apiHosts: map[string]string{"github.com": "api-gateway.example.com"}, lookup: "", wantFound: false, }, { name: "the right host is chosen when several configure an api_host", - apiHosts: map[string]string{"github.com": "api.example.com", "ghe.io": "api.ghe.io"}, + apiHosts: map[string]string{"github.com": "api-gateway.example.com", "ghe.io": "api.ghe.io"}, lookup: "api.ghe.io", wantHost: "ghe.io", wantFound: true, @@ -1028,8 +1028,8 @@ func TestHostForAPIHost(t *testing.T) { // only hopes to catch an ordering regression rather than // guaranteeing it on every run. name: "the first lexical match is returned when several matches found", - apiHosts: map[string]string{"A.github.com": "api.example.com", "a.github.com": "api.example.com"}, - lookup: "api.example.com", + apiHosts: map[string]string{"A.github.com": "api-gateway.example.com", "a.github.com": "api-gateway.example.com"}, + lookup: "api-gateway.example.com", wantHost: "A.github.com", wantFound: true, }, @@ -1084,20 +1084,20 @@ func TestAPIHostForHost(t *testing.T) { }, { name: "the host configures an api_host", - apiHost: "api.example.com", + apiHost: "api-gateway.example.com", lookup: "github.com", - wantAPIHost: "api.example.com", + wantAPIHost: "api-gateway.example.com", wantFound: true, }, { name: "an empty host matches nothing", - apiHost: "api.example.com", + apiHost: "api-gateway.example.com", lookup: "", wantFound: false, }, { name: "an unknown host matches nothing", - apiHost: "api.example.com", + apiHost: "api-gateway.example.com", lookup: "ghe.io", wantFound: false, }, diff --git a/internal/config/config.go b/internal/config/config.go index 8d82017e712..6467fe77d73 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -646,10 +646,21 @@ type ConfigOption struct { Description string DefaultValue string AllowedValues []string + PerHostOnly bool CurrentValue func(c gh.Config, hostname string) string } var Options = []ConfigOption{ + { + Key: apiHostKey, + Description: "experimental: the hostname to use when making API requests for a GitHub host. Note: this is not a security boundary and requests to the canonical host will remain authenticated", + DefaultValue: "", + PerHostOnly: true, + CurrentValue: func(c gh.Config, hostname string) string { + apiHost, _ := c.Authentication().APIHostForHost(hostname) + return apiHost + }, + }, { Key: gitProtocolKey, Description: "the protocol to use for git clone and push operations", diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index c1bdc911fd4..cb2b86dcc0b 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -18,6 +18,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/factory" @@ -34,13 +35,14 @@ const ( ) type ApiOptions struct { - AppVersion string - InvokingAgent string - BaseRepo func() (ghrepo.Interface, error) - Branch func() (string, error) - Config func() (gh.Config, error) - HttpClient func() (*http.Client, error) - IO *iostreams.IOStreams + AppVersion string + InvokingAgent string + BaseRepo func() (ghrepo.Interface, error) + Branch func() (string, error) + Config func() (gh.Config, error) + HttpClient func(api.HTTPClientOptions) (*http.Client, error) + IO *iostreams.IOStreams + TelemetryDisabler ghtelemetry.Disabler Hostname string RequestMethod string @@ -65,12 +67,14 @@ type ApiOptions struct { func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command { opts := ApiOptions{ - AppVersion: f.AppVersion, - InvokingAgent: f.InvokingAgent, - BaseRepo: f.BaseRepo, - Branch: f.Branch, - Config: f.Config, - IO: f.IOStreams, + AppVersion: f.AppVersion, + InvokingAgent: f.InvokingAgent, + BaseRepo: f.BaseRepo, + Branch: f.Branch, + Config: f.Config, + HttpClient: api.NewHTTPClient, + IO: f.IOStreams, + TelemetryDisabler: f.TelemetryDisabler, } cmd := &cobra.Command{ @@ -389,25 +393,23 @@ func apiRun(opts *ApiOptions) error { } if opts.HttpClient == nil { - opts.HttpClient = func() (*http.Client, error) { - log := opts.IO.ErrOut - if opts.Verbose { - log = opts.IO.Out - } - opts := api.HTTPClientOptions{ - AppVersion: opts.AppVersion, - InvokingAgent: opts.InvokingAgent, - CacheTTL: opts.CacheTTL, - Config: cfg.Authentication(), - EnableCache: opts.CacheTTL > 0, - Log: log, - LogColorize: opts.IO.ColorEnabled(), - LogVerboseHTTP: opts.Verbose, - } - return api.NewHTTPClient(opts) - } + opts.HttpClient = api.NewHTTPClient } - httpClient, err := opts.HttpClient() + log := opts.IO.ErrOut + if opts.Verbose { + log = opts.IO.Out + } + httpClient, err := opts.HttpClient(api.HTTPClientOptions{ + AppVersion: opts.AppVersion, + InvokingAgent: opts.InvokingAgent, + CacheTTL: opts.CacheTTL, + Config: cfg.Authentication(), + EnableCache: opts.CacheTTL > 0, + Log: log, + LogColorize: opts.IO.ColorEnabled(), + LogVerboseHTTP: opts.Verbose, + TelemetryDisabler: opts.TelemetryDisabler, + }) if err != nil { return err } diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index f5ba71cd324..e67ca517a76 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -14,11 +14,14 @@ import ( "time" "github.com/MakeNowJust/heredoc" + rootapi "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" ghmock "github.com/cli/cli/v2/internal/gh/mock" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/telemetry" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/go-gh/v2/pkg/template" @@ -422,6 +425,35 @@ func Test_NewCmdApi_WindowsAbsPath(t *testing.T) { assert.EqualError(t, err, `invalid API endpoint: "C:\users\repos". Your shell might be rewriting URL paths as filesystem paths. To avoid this, omit the leading slash from the endpoint argument`) } +func TestNewCmdApiTelemetry(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, "{}") + })) + t.Cleanup(server.Close) + + var payload telemetry.SendTelemetryPayload + recorder := telemetry.NewService(func(p telemetry.SendTelemetryPayload) { + payload = p + }) + recorder.Record(ghtelemetry.Event{Type: "command"}) + + ios, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + Config: func() (gh.Config, error) { return config.NewMockConfig(), nil }, + IOStreams: ios, + TelemetryDisabler: recorder, + } + cmd := NewCmdApi(f, nil) + cmd.SetArgs([]string{server.URL}) + + _, err := cmd.ExecuteC() + require.NoError(t, err) + recorder.Flush() + + assert.Empty(t, payload.Events) +} + func Test_apiRun(t *testing.T) { tests := []struct { name string @@ -741,7 +773,7 @@ func Test_apiRun(t *testing.T) { tt.options.IO = ios tt.options.Config = func() (gh.Config, error) { return config.NewMockConfig(), nil } - tt.options.HttpClient = func() (*http.Client, error) { + tt.options.HttpClient = func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { resp := tt.httpResponse resp.Request = req @@ -810,7 +842,7 @@ func Test_apiRun_paginationREST(t *testing.T) { options := ApiOptions{ IO: ios, - HttpClient: func() (*http.Client, error) { + HttpClient: func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { resp := responses[requestCount] resp.Request = req @@ -882,7 +914,7 @@ func Test_apiRun_arrayPaginationREST(t *testing.T) { options := ApiOptions{ IO: ios, - HttpClient: func() (*http.Client, error) { + HttpClient: func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { resp := responses[requestCount] resp.Request = req @@ -954,7 +986,7 @@ func Test_apiRun_arrayPaginationREST_with_headers(t *testing.T) { options := ApiOptions{ IO: ios, - HttpClient: func() (*http.Client, error) { + HttpClient: func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { resp := responses[requestCount] resp.Request = req @@ -1023,7 +1055,7 @@ func Test_apiRun_paginationGraphQL(t *testing.T) { options := ApiOptions{ IO: ios, - HttpClient: func() (*http.Client, error) { + HttpClient: func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { resp := responses[requestCount] resp.Request = req @@ -1122,7 +1154,7 @@ func Test_apiRun_paginationGraphQL_slurp(t *testing.T) { options := ApiOptions{ IO: ios, - HttpClient: func() (*http.Client, error) { + HttpClient: func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { resp := responses[requestCount] resp.Request = req @@ -1234,7 +1266,7 @@ func Test_apiRun_paginated_template(t *testing.T) { options := ApiOptions{ IO: ios, - HttpClient: func() (*http.Client, error) { + HttpClient: func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { resp := responses[requestCount] resp.Request = req @@ -1293,7 +1325,7 @@ func Test_apiRun_DELETE(t *testing.T) { Config: func() (gh.Config, error) { return config.NewMockConfig(), nil }, - HttpClient: func() (*http.Client, error) { + HttpClient: func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { gotRequest = req return &http.Response{StatusCode: 204, Request: req}, nil @@ -1322,7 +1354,7 @@ func Test_apiRun_HEAD(t *testing.T) { Config: func() (gh.Config, error) { return config.NewMockConfig(), nil }, - HttpClient: func() (*http.Client, error) { + HttpClient: func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 422, @@ -1393,7 +1425,7 @@ func Test_apiRun_inputFile(t *testing.T) { RawFields: []string{"a=b", "c=d"}, IO: ios, - HttpClient: func() (*http.Client, error) { + HttpClient: func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { var err error if bodyBytes, err = io.ReadAll(req.Body); err != nil { @@ -1889,7 +1921,7 @@ func Test_apiRun_acceptHeader(t *testing.T) { } var gotReq *http.Request - tt.options.HttpClient = func() (*http.Client, error) { + tt.options.HttpClient = func(rootapi.HTTPClientOptions) (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { gotReq = req resp := &http.Response{ diff --git a/pkg/cmd/config/list/list_test.go b/pkg/cmd/config/list/list_test.go index 019d397eec6..3a04647fdac 100644 --- a/pkg/cmd/config/list/list_test.go +++ b/pkg/cmd/config/list/list_test.go @@ -82,6 +82,7 @@ func Test_listRun(t *testing.T) { name: "list", config: func() gh.Config { cfg := config.NewMockConfig() + cfg.Set("HOST", "api_host", "api-gateway.example.com") cfg.Set("HOST", "git_protocol", "ssh") cfg.Set("HOST", "editor", "/usr/bin/vim") cfg.Set("HOST", "prompt", "disabled") @@ -93,6 +94,7 @@ func Test_listRun(t *testing.T) { }(), input: &ListOptions{Hostname: "HOST"}, stdout: heredoc.Doc(` + api_host=api-gateway.example.com git_protocol=ssh editor=/usr/bin/vim prompt=disabled diff --git a/pkg/cmd/config/set/set.go b/pkg/cmd/config/set/set.go index 2e4496b86ab..e592d1efc1e 100644 --- a/pkg/cmd/config/set/set.go +++ b/pkg/cmd/config/set/set.go @@ -35,6 +35,7 @@ func NewCmdConfigSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Co $ gh config set editor vim $ gh config set editor "code --wait" $ gh config set git_protocol ssh --host github.com + $ gh config set api_host api-gateway.example.com --host example.com $ gh config set prompt disabled `), Args: cobra.ExactArgs(2), @@ -61,6 +62,10 @@ func NewCmdConfigSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Co } func setRun(opts *SetOptions) error { + if isPerHostOnly(opts.Key) && opts.Hostname == "" { + return cmdutil.FlagErrorf("--host required when setting %s", opts.Key) + } + err := ValidateKey(opts.Key) if err != nil { warningIcon := opts.IO.ColorScheme().WarningIcon() @@ -98,6 +103,16 @@ func ValidateKey(key string) error { return fmt.Errorf("invalid key") } +func isPerHostOnly(key string) bool { + for _, configKey := range config.Options { + if key == configKey.Key { + return configKey.PerHostOnly + } + } + + return false +} + type InvalidValueError struct { ValidValues []string } diff --git a/pkg/cmd/config/set/set_test.go b/pkg/cmd/config/set/set_test.go index 80aed28dbe7..a03207cf793 100644 --- a/pkg/cmd/config/set/set_test.go +++ b/pkg/cmd/config/set/set_test.go @@ -113,6 +113,26 @@ func Test_setRun(t *testing.T) { }, expectedValue: "vim", }, + { + name: "set api_host scoped by host", + input: &SetOptions{ + Config: config.NewMockConfig(), + Hostname: "github.example.com", + Key: "api_host", + Value: "api-gateway.example.com", + }, + expectedValue: "api-gateway.example.com", + }, + { + name: "set api_host without hostname", + input: &SetOptions{ + Config: config.NewMockConfig(), + Key: "api_host", + Value: "api-gateway.example.com", + }, + wantsErr: true, + errMsg: "--host required when setting api_host", + }, { name: "set unknown key", input: &SetOptions{ @@ -196,4 +216,13 @@ func Test_ValidateKey(t *testing.T) { err = ValidateKey("browser") assert.NoError(t, err) + + err = ValidateKey("api_host") + assert.NoError(t, err) +} + +func Test_isPerHostOnly(t *testing.T) { + assert.True(t, isPerHostOnly("api_host")) + assert.False(t, isPerHostOnly("editor")) + assert.False(t, isPerHostOnly("unknown")) } diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go index cc10075f203..202f7ae556c 100644 --- a/pkg/cmd/factory/default.go +++ b/pkg/cmd/factory/default.go @@ -32,6 +32,7 @@ func New(appVersion string, invokingAgent string, cfgFunc func() (gh.Config, err } f.IOStreams = ios + f.TelemetryDisabler = telemetryDisabler f.HttpClient = HttpClientFunc(cfgFunc, ios, appVersion, invokingAgent, telemetryDisabler) f.PlainHttpClient = plainHttpClientFunc(ios, appVersion, invokingAgent, telemetryDisabler) f.ExternalHttpClient = externalHttpClientFunc(ios, appVersion) diff --git a/pkg/cmd/issue/comment/comment.go b/pkg/cmd/issue/comment/comment.go index c85f17d20ae..3c972d14f20 100644 --- a/pkg/cmd/issue/comment/comment.go +++ b/pkg/cmd/issue/comment/comment.go @@ -3,6 +3,7 @@ package comment import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/attachments" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/issue/shared" issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -11,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) error) *cobra.Command { +func NewCmdComment(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, runF func(*prShared.CommentableOptions) error) *cobra.Command { opts := &prShared.CommentableOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, @@ -58,6 +59,8 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e `), Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { + opts.AttachFlag.RecordTelemetry(cmd.CommandPath(), telemetry) + opts.RetrieveCommentable = func() (prShared.Commentable, ghrepo.Interface, error) { // TODO wm: more testing issueNumber, parsedBaseRepo, err := shared.ParseIssueFromArg(args[0]) diff --git a/pkg/cmd/issue/comment/comment_test.go b/pkg/cmd/issue/comment/comment_test.go index af7ff54f271..08ad22e7757 100644 --- a/pkg/cmd/issue/comment/comment_test.go +++ b/pkg/cmd/issue/comment/comment_test.go @@ -14,7 +14,9 @@ import ( "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/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/telemetry" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" @@ -408,7 +410,8 @@ func TestNewCmdComment(t *testing.T) { assert.NoError(t, err) var gotOpts *shared.CommentableOptions - cmd := NewCmdComment(f, func(opts *shared.CommentableOptions) error { + recorder := &telemetry.CommandRecorderSpy{} + cmd := NewCmdComment(f, recorder, func(opts *shared.CommentableOptions) error { gotOpts = opts return nil }) @@ -420,6 +423,18 @@ func TestNewCmdComment(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) _, err = cmd.ExecuteC() + if cmd.Flags().Changed("attach") { + values, flagErr := cmd.Flags().GetStringArray("attach") + require.NoError(t, flagErr) + require.Equal(t, ghtelemetry.SAMPLE_ALL, recorder.LastSampleRate) + require.Len(t, recorder.Events, 1) + assert.Equal(t, "attachment_invocation", recorder.Events[0].Type) + assert.Equal(t, cmd.CommandPath(), recorder.Events[0].Dimensions["command"]) + assert.Equal(t, int64(len(values)), recorder.Events[0].Measures["attach_count"]) + } else { + assert.Empty(t, recorder.Events) + assert.Zero(t, recorder.LastSampleRate) + } if tt.wantsErr { assert.Error(t, err) if tt.wantsErrContains != "" { diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go index 0fd18db0dd6..c0e6adfa481 100644 --- a/pkg/cmd/issue/create/create.go +++ b/pkg/cmd/issue/create/create.go @@ -13,6 +13,7 @@ import ( "github.com/cli/cli/v2/internal/browser" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/text" @@ -61,7 +62,7 @@ type CreateOptions struct { Assets []attachments.UserAsset } -func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { +func NewCmdCreate(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, runF func(*CreateOptions) error) *cobra.Command { opts := &CreateOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, @@ -120,6 +121,8 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co Args: cmdutil.NoArgsQuoteReminder, Aliases: []string{"new"}, RunE: func(cmd *cobra.Command, args []string) error { + opts.AttachFlag.RecordTelemetry(cmd.CommandPath(), telemetry) + // support `-R, --repo` override opts.BaseRepo = f.BaseRepo opts.HasRepoOverride = cmd.Flags().Changed("repo") diff --git a/pkg/cmd/issue/create/create_test.go b/pkg/cmd/issue/create/create_test.go index 3f6971ec998..e0463fb9504 100644 --- a/pkg/cmd/issue/create/create_test.go +++ b/pkg/cmd/issue/create/create_test.go @@ -18,9 +18,11 @@ import ( "github.com/cli/cli/v2/internal/config" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/internal/telemetry" prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" @@ -311,7 +313,8 @@ func TestNewCmdCreate(t *testing.T) { } var opts *CreateOptions - cmd := NewCmdCreate(f, func(o *CreateOptions) error { + recorder := &telemetry.CommandRecorderSpy{} + cmd := NewCmdCreate(f, recorder, func(o *CreateOptions) error { opts = o return nil }) @@ -322,6 +325,18 @@ func TestNewCmdCreate(t *testing.T) { cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) _, err = cmd.ExecuteC() + if cmd.Flags().Changed("attach") { + values, flagErr := cmd.Flags().GetStringArray("attach") + require.NoError(t, flagErr) + require.Equal(t, ghtelemetry.SAMPLE_ALL, recorder.LastSampleRate) + require.Len(t, recorder.Events, 1) + assert.Equal(t, "attachment_invocation", recorder.Events[0].Type) + assert.Equal(t, cmd.CommandPath(), recorder.Events[0].Dimensions["command"]) + assert.Equal(t, int64(len(values)), recorder.Events[0].Measures["attach_count"]) + } else { + assert.Empty(t, recorder.Events) + assert.Zero(t, recorder.LastSampleRate) + } if tt.wantsErr { require.Error(t, err) if tt.wantsErrMsg != "" { @@ -1450,7 +1465,7 @@ func runCommandWithRootDirOverridden(rt http.RoundTripper, isTTY bool, cli strin Prompter: pm, } - cmd := NewCmdCreate(factory, func(opts *CreateOptions) error { + cmd := NewCmdCreate(factory, &telemetry.NoOpService{}, func(opts *CreateOptions) error { opts.RootDirOverride = rootDir opts.Detector = &fd.EnabledDetectorMock{} return createRun(opts) diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 1329e1a5c58..3b8557abee6 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -15,6 +15,7 @@ import ( "github.com/cli/cli/v2/internal/attachments" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/text" issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -57,7 +58,7 @@ type EditOptions struct { prShared.Editable } -func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command { +func NewCmdEdit(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, runF func(*EditOptions) error) *cobra.Command { opts := &EditOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, @@ -122,6 +123,8 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman `), Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + opts.AttachFlag.RecordTelemetry(cmd.CommandPath(), telemetry) + issueNumbers, baseRepo, err := issueShared.ParseIssuesFromArgs(args) if err != nil { return err diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index 54040de3d40..5e80ddc57a3 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -20,8 +20,10 @@ import ( "github.com/cli/cli/v2/internal/config" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/internal/telemetry" prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" @@ -455,7 +457,8 @@ func TestNewCmdEdit(t *testing.T) { assert.NoError(t, err) var gotOpts *EditOptions - cmd := NewCmdEdit(f, func(opts *EditOptions) error { + recorder := &telemetry.CommandRecorderSpy{} + cmd := NewCmdEdit(f, recorder, func(opts *EditOptions) error { gotOpts = opts return nil }) @@ -467,6 +470,18 @@ func TestNewCmdEdit(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) _, err = cmd.ExecuteC() + if cmd.Flags().Changed("attach") { + values, flagErr := cmd.Flags().GetStringArray("attach") + require.NoError(t, flagErr) + require.Equal(t, ghtelemetry.SAMPLE_ALL, recorder.LastSampleRate) + require.Len(t, recorder.Events, 1) + assert.Equal(t, "attachment_invocation", recorder.Events[0].Type) + assert.Equal(t, cmd.CommandPath(), recorder.Events[0].Dimensions["command"]) + assert.Equal(t, int64(len(values)), recorder.Events[0].Measures["attach_count"]) + } else { + assert.Empty(t, recorder.Events) + assert.Zero(t, recorder.LastSampleRate) + } if tt.wantsErr { require.Error(t, err) if tt.wantsErrMsg != "" { diff --git a/pkg/cmd/issue/issue.go b/pkg/cmd/issue/issue.go index eceea35c6df..65f43929cee 100644 --- a/pkg/cmd/issue/issue.go +++ b/pkg/cmd/issue/issue.go @@ -2,6 +2,7 @@ package issue import ( "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" cmdClose "github.com/cli/cli/v2/pkg/cmd/issue/close" cmdComment "github.com/cli/cli/v2/pkg/cmd/issue/comment" cmdCreate "github.com/cli/cli/v2/pkg/cmd/issue/create" @@ -20,7 +21,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmdIssue(f *cmdutil.Factory) *cobra.Command { +func NewCmdIssue(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder) *cobra.Command { cmd := &cobra.Command{ Use: "issue ", Short: "Manage issues", @@ -44,16 +45,16 @@ func NewCmdIssue(f *cmdutil.Factory) *cobra.Command { cmdutil.AddGroup(cmd, "General commands", cmdList.NewCmdList(f, nil), - cmdCreate.NewCmdCreate(f, nil), + cmdCreate.NewCmdCreate(f, telemetry, nil), cmdStatus.NewCmdStatus(f, nil), ) cmdutil.AddGroup(cmd, "Targeted commands", cmdView.NewCmdView(f, nil), - cmdComment.NewCmdComment(f, nil), + cmdComment.NewCmdComment(f, telemetry, nil), cmdClose.NewCmdClose(f, nil), cmdReopen.NewCmdReopen(f, nil), - cmdEdit.NewCmdEdit(f, nil), + cmdEdit.NewCmdEdit(f, telemetry, nil), cmdDevelop.NewCmdDevelop(f, nil), cmdLock.NewCmdLock(f, cmd.Name(), nil), cmdLock.NewCmdUnlock(f, cmd.Name(), nil), diff --git a/pkg/cmd/pr/comment/comment.go b/pkg/cmd/pr/comment/comment.go index cd9c7927edb..e7f7d504674 100644 --- a/pkg/cmd/pr/comment/comment.go +++ b/pkg/cmd/pr/comment/comment.go @@ -3,13 +3,14 @@ package comment import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/attachments" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" ) -func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) error) *cobra.Command { +func NewCmdComment(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, runF func(*shared.CommentableOptions) error) *cobra.Command { opts := &shared.CommentableOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, @@ -56,6 +57,8 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err `), Args: cobra.MaximumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { + opts.AttachFlag.RecordTelemetry(cmd.CommandPath(), telemetry) + if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 { return cmdutil.FlagErrorf("argument required when using the --repo flag") } diff --git a/pkg/cmd/pr/comment/comment_test.go b/pkg/cmd/pr/comment/comment_test.go index 52b17bee118..c09c83cb32d 100644 --- a/pkg/cmd/pr/comment/comment_test.go +++ b/pkg/cmd/pr/comment/comment_test.go @@ -14,7 +14,9 @@ import ( "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/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/telemetry" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" @@ -430,7 +432,8 @@ func TestNewCmdComment(t *testing.T) { assert.NoError(t, err) var gotOpts *shared.CommentableOptions - cmd := NewCmdComment(f, func(opts *shared.CommentableOptions) error { + recorder := &telemetry.CommandRecorderSpy{} + cmd := NewCmdComment(f, recorder, func(opts *shared.CommentableOptions) error { gotOpts = opts return nil }) @@ -442,6 +445,18 @@ func TestNewCmdComment(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) _, err = cmd.ExecuteC() + if cmd.Flags().Changed("attach") { + values, flagErr := cmd.Flags().GetStringArray("attach") + require.NoError(t, flagErr) + require.Equal(t, ghtelemetry.SAMPLE_ALL, recorder.LastSampleRate) + require.Len(t, recorder.Events, 1) + assert.Equal(t, "attachment_invocation", recorder.Events[0].Type) + assert.Equal(t, cmd.CommandPath(), recorder.Events[0].Dimensions["command"]) + assert.Equal(t, int64(len(values)), recorder.Events[0].Measures["attach_count"]) + } else { + assert.Empty(t, recorder.Events) + assert.Zero(t, recorder.LastSampleRate) + } if tt.wantsErr { assert.Error(t, err) if tt.wantsErrContains != "" { diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 11706462d28..c365fadebab 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -21,6 +21,7 @@ import ( "github.com/cli/cli/v2/internal/browser" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/text" @@ -195,7 +196,7 @@ type CreateContext struct { GitClient *git.Client } -func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { +func NewCmdCreate(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, runF func(*CreateOptions) error) *cobra.Command { opts := &CreateOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, @@ -274,6 +275,8 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co Args: cmdutil.NoArgsQuoteReminder, Aliases: []string{"new"}, RunE: func(cmd *cobra.Command, args []string) error { + opts.AttachFlag.RecordTelemetry(cmd.CommandPath(), telemetry) + opts.Finder = shared.NewFinder(f) opts.TitleProvided = cmd.Flags().Changed("title") diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index cf5b4ea3c38..e6ecc1047a4 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -21,9 +21,11 @@ import ( "github.com/cli/cli/v2/internal/config" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/internal/telemetry" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" @@ -335,7 +337,8 @@ func TestNewCmdCreate(t *testing.T) { } var opts *CreateOptions - cmd := NewCmdCreate(f, func(o *CreateOptions) error { + recorder := &telemetry.CommandRecorderSpy{} + cmd := NewCmdCreate(f, recorder, func(o *CreateOptions) error { opts = o return nil }) @@ -346,6 +349,18 @@ func TestNewCmdCreate(t *testing.T) { cmd.SetOut(stderr) cmd.SetErr(stderr) _, err = cmd.ExecuteC() + if cmd.Flags().Changed("attach") { + values, flagErr := cmd.Flags().GetStringArray("attach") + require.NoError(t, flagErr) + require.Equal(t, ghtelemetry.SAMPLE_ALL, recorder.LastSampleRate) + require.Len(t, recorder.Events, 1) + assert.Equal(t, "attachment_invocation", recorder.Events[0].Type) + assert.Equal(t, cmd.CommandPath(), recorder.Events[0].Dimensions["command"]) + assert.Equal(t, int64(len(values)), recorder.Events[0].Measures["attach_count"]) + } else { + assert.Empty(t, recorder.Events) + assert.Zero(t, recorder.LastSampleRate) + } if tt.wantsErr { if tt.wantsErrMsg != "" { if tt.wantErrIsNotExist { diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index fb4dc268e8a..fb8161f823c 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -14,6 +14,7 @@ import ( "github.com/cli/cli/v2/internal/attachments" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" @@ -46,7 +47,7 @@ type EditOptions struct { shared.Editable } -func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command { +func NewCmdEdit(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, runF func(*EditOptions) error) *cobra.Command { opts := &EditOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, @@ -134,6 +135,8 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + opts.AttachFlag.RecordTelemetry(cmd.CommandPath(), telemetry) + opts.Finder = shared.NewFinder(f) // support `-R, --repo` override diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 64e49e63afb..98cbd60b2d9 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -15,7 +15,9 @@ import ( "github.com/cli/cli/v2/internal/config" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/telemetry" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" @@ -354,7 +356,8 @@ func TestNewCmdEdit(t *testing.T) { assert.NoError(t, err) var gotOpts *EditOptions - cmd := NewCmdEdit(f, func(opts *EditOptions) error { + recorder := &telemetry.CommandRecorderSpy{} + cmd := NewCmdEdit(f, recorder, func(opts *EditOptions) error { gotOpts = opts return nil }) @@ -366,6 +369,18 @@ func TestNewCmdEdit(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) _, err = cmd.ExecuteC() + if cmd.Flags().Changed("attach") { + values, flagErr := cmd.Flags().GetStringArray("attach") + require.NoError(t, flagErr) + require.Equal(t, ghtelemetry.SAMPLE_ALL, recorder.LastSampleRate) + require.Len(t, recorder.Events, 1) + assert.Equal(t, "attachment_invocation", recorder.Events[0].Type) + assert.Equal(t, cmd.CommandPath(), recorder.Events[0].Dimensions["command"]) + assert.Equal(t, int64(len(values)), recorder.Events[0].Measures["attach_count"]) + } else { + assert.Empty(t, recorder.Events) + assert.Zero(t, recorder.LastSampleRate) + } if tt.wantsErr { assert.Error(t, err) return diff --git a/pkg/cmd/pr/pr.go b/pkg/cmd/pr/pr.go index e73193084c0..225b62ca7b7 100644 --- a/pkg/cmd/pr/pr.go +++ b/pkg/cmd/pr/pr.go @@ -2,6 +2,7 @@ package pr import ( "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" cmdLock "github.com/cli/cli/v2/pkg/cmd/issue/lock" cmdCheckout "github.com/cli/cli/v2/pkg/cmd/pr/checkout" cmdChecks "github.com/cli/cli/v2/pkg/cmd/pr/checks" @@ -23,7 +24,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmdPR(f *cmdutil.Factory) *cobra.Command { +func NewCmdPR(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder) *cobra.Command { cmd := &cobra.Command{ Use: "pr ", Short: "Manage pull requests", @@ -48,7 +49,7 @@ func NewCmdPR(f *cmdutil.Factory) *cobra.Command { cmdutil.AddGroup(cmd, "General commands", cmdList.NewCmdList(f, nil), - cmdCreate.NewCmdCreate(f, nil), + cmdCreate.NewCmdCreate(f, telemetry, nil), cmdStatus.NewCmdStatus(f, nil), ) @@ -61,11 +62,11 @@ func NewCmdPR(f *cmdutil.Factory) *cobra.Command { cmdMerge.NewCmdMerge(f, nil), cmdUpdateBranch.NewCmdUpdateBranch(f, nil), cmdReady.NewCmdReady(f, nil), - cmdComment.NewCmdComment(f, nil), + cmdComment.NewCmdComment(f, telemetry, nil), cmdClose.NewCmdClose(f, nil), cmdReopen.NewCmdReopen(f, nil), cmdRevert.NewCmdRevert(f, nil), - cmdEdit.NewCmdEdit(f, nil), + cmdEdit.NewCmdEdit(f, telemetry, nil), cmdLock.NewCmdLock(f, cmd.Name(), nil), cmdLock.NewCmdUnlock(f, cmd.Name(), nil), ) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index df90bb249f9..985a0d82a41 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -166,9 +166,9 @@ func NewCmdRoot(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, versi cmd.AddCommand(agentTaskCmd.NewCmdAgentTask(&repoResolvingCmdFactory)) cmd.AddCommand(browseCmd.NewCmdBrowse(&repoResolvingCmdFactory, nil)) cmd.AddCommand(discussionCmd.NewCmdDiscussion(&repoResolvingCmdFactory)) - cmd.AddCommand(prCmd.NewCmdPR(&repoResolvingCmdFactory)) + cmd.AddCommand(prCmd.NewCmdPR(&repoResolvingCmdFactory, telemetry)) cmd.AddCommand(orgCmd.NewCmdOrg(&repoResolvingCmdFactory)) - cmd.AddCommand(issueCmd.NewCmdIssue(&repoResolvingCmdFactory)) + cmd.AddCommand(issueCmd.NewCmdIssue(&repoResolvingCmdFactory, telemetry)) cmd.AddCommand(releaseCmd.NewCmdRelease(&repoResolvingCmdFactory)) cmd.AddCommand(repoCmd.NewCmdRepo(&repoResolvingCmdFactory)) cmd.AddCommand(rulesetCmd.NewCmdRuleset(&repoResolvingCmdFactory)) diff --git a/pkg/cmdutil/factory.go b/pkg/cmdutil/factory.go index 200314038b2..abbcdf0408b 100644 --- a/pkg/cmdutil/factory.go +++ b/pkg/cmdutil/factory.go @@ -7,6 +7,7 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/gh/ghtelemetry" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/pkg/extensions" @@ -18,11 +19,12 @@ type Factory struct { ExecutablePath string InvokingAgent string - Browser browser.Browser - ExtensionManager extensions.ExtensionManager - GitClient *git.Client - IOStreams *iostreams.IOStreams - Prompter prompter.Prompter + Browser browser.Browser + ExtensionManager extensions.ExtensionManager + GitClient *git.Client + IOStreams *iostreams.IOStreams + Prompter prompter.Prompter + TelemetryDisabler ghtelemetry.Disabler BaseRepo func() (ghrepo.Interface, error) Branch func() (string, error) diff --git a/pkg/extensions/official.go b/pkg/extensions/official.go index dc6bdc919b0..c45ddc031ce 100644 --- a/pkg/extensions/official.go +++ b/pkg/extensions/official.go @@ -25,6 +25,7 @@ func (e *OfficialExtension) Repository() ghrepo.Interface { var OfficialExtensions = []OfficialExtension{ {Name: "aw", Owner: "github", Repo: "gh-aw"}, {Name: "stack", Owner: "github", Repo: "gh-stack"}, + {Name: "webhook", Owner: "cli", Repo: "gh-webhook"}, } // IsOfficial reports whether the given extension command name and owner diff --git a/pkg/extensions/official_test.go b/pkg/extensions/official_test.go index 6d16ece2cf9..cb9b699876d 100644 --- a/pkg/extensions/official_test.go +++ b/pkg/extensions/official_test.go @@ -27,6 +27,12 @@ func TestIsOfficial(t *testing.T) { extOwner: "github", want: true, }, + { + name: "known official extension owned by cli matches", + extName: "webhook", + extOwner: "cli", + want: true, + }, { name: "official name with different owner is not official", extName: "stack",