From 6cf2e9ee3e1b64656bbcd4ef7e4fc0d9681b3378 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Fri, 3 Jan 2025 11:14:33 -0500 Subject: [PATCH 1/4] feat: Add support for creating autolink references --- pkg/cmd/repo/autolink/autolink.go | 2 + pkg/cmd/repo/autolink/create/create.go | 139 ++++++++++++++ pkg/cmd/repo/autolink/create/create_test.go | 197 ++++++++++++++++++++ pkg/cmd/repo/autolink/create/http.go | 83 +++++++++ pkg/cmd/repo/autolink/create/http_test.go | 155 +++++++++++++++ pkg/cmd/repo/autolink/domain/autolink.go | 14 ++ pkg/cmd/repo/autolink/list/http.go | 7 +- pkg/cmd/repo/autolink/list/http_test.go | 9 +- pkg/cmd/repo/autolink/list/list.go | 18 +- pkg/cmd/repo/autolink/list/list_test.go | 15 +- pkg/httpmock/stub.go | 6 +- 11 files changed, 616 insertions(+), 29 deletions(-) create mode 100644 pkg/cmd/repo/autolink/create/create.go create mode 100644 pkg/cmd/repo/autolink/create/create_test.go create mode 100644 pkg/cmd/repo/autolink/create/http.go create mode 100644 pkg/cmd/repo/autolink/create/http_test.go create mode 100644 pkg/cmd/repo/autolink/domain/autolink.go diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index d9430f562d4..b988de60d0d 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -2,6 +2,7 @@ package autolink import ( "github.com/MakeNowJust/heredoc" + cmdCreate "github.com/cli/cli/v2/pkg/cmd/repo/autolink/create" cmdList "github.com/cli/cli/v2/pkg/cmd/repo/autolink/list" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" @@ -24,6 +25,7 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command { cmdutil.EnableRepoOverride(cmd, f) cmd.AddCommand(cmdList.NewCmdList(f, nil)) + cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil)) return cmd } diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go new file mode 100644 index 00000000000..698feb555f8 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/create.go @@ -0,0 +1,139 @@ +package create + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type createOptions struct { + BaseRepo func() (ghrepo.Interface, error) + Browser browser.Browser + AutolinkClient AutolinkCreateClient + IO *iostreams.IOStreams + Exporter cmdutil.Exporter + + KeyPrefix string + URLTemplate string + Numeric bool +} + +type AutolinkCreateClient interface { + Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) +} + +func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Command { + opts := &createOptions{ + Browser: f.Browser, + IO: f.IOStreams, + } + + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a new autolink reference", + Long: heredoc.Docf(` + Create a new autolink reference for a repository. + + Autolinks automatically generate links to external resources when they appear in an issue, pull request, or commit. + + The %[1]skeyPrefix%[1]s specifies the prefix that will generate a link when it is appended by certain characters. + + The %[1]surlTemplate%[1]s specifies the target URL that will be generated when the keyPrefix is found. The urlTemplate must contain %[1]s%[1]s for the reference number. %[1]s%[1]s matches different characters depending on the whether the autolink is specified as numeric or alphanumeric. + + By default, the command will create an alphanumeric autolink. This means that the %[1]s%[1]s in the %[1]surlTemplate%[1]s will match alphanumeric characters %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s. To create a numeric autolink, use the %[1]s--numeric%[1]s flag. Numeric autolinks only match against numeric characters. If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. + + If you are using a shell that applies special meaning to angle brackets, you you will need to need to escape these characters in the %[1]surlTemplate%[1]s or place quotation marks around this argument. + + Only repository administrators can create autolinks. + `, "`"), + Example: heredoc.Doc(` + # Create an alphanumeric autolink to example.com for the key prefix "TICKET-". This will generate a link to https://example.com/TICKET?query=123abc when "TICKET-123abc" appears. + $ gh repo autolink create TICKET- https://example.com/TICKET?query= + + # Create a numeric autolink to example.com for the key prefix "STORY-". This will generate a link to https://example.com/STORY?id=123 when "STORY-123" appears. + $ gh repo autolink create STORY- https://example.com/STORY?id= --numeric + + `), + Args: cmdutil.ExactArgs(2, "Cannot create autolink: keyPrefix and urlTemplate arguments are both required"), + Aliases: []string{"new"}, + RunE: func(c *cobra.Command, args []string) error { + opts.BaseRepo = f.BaseRepo + httpClient, err := f.HttpClient() + + if err != nil { + return err + } + + opts.AutolinkClient = &AutolinkCreator{HTTPClient: httpClient} + opts.KeyPrefix = args[0] + opts.URLTemplate = args[1] + + if runF != nil { + return runF(opts) + } + + return createRun(opts) + }, + } + + cmd.Flags().BoolVarP(&opts.Numeric, "numeric", "n", false, "Mark autolink as non-alphanumeric") + + return cmd +} + +func createRun(opts *createOptions) error { + repo, err := opts.BaseRepo() + if err != nil { + return err + } + cs := opts.IO.ColorScheme() + + isAlphanumeric := !opts.Numeric + + request := AutolinkCreateRequest{ + KeyPrefix: opts.KeyPrefix, + URLTemplate: opts.URLTemplate, + IsAlphanumeric: isAlphanumeric, + } + + autolink, err := opts.AutolinkClient.Create(repo, request) + + if err != nil { + return fmt.Errorf("%s %w", cs.Red("error creating autolink:"), err) + } + + msg := successMsg(autolink, repo, cs) + fmt.Fprint(opts.IO.Out, msg) + + return nil +} + +func successMsg(autolink *domain.Autolink, repo ghrepo.Interface, cs *iostreams.ColorScheme) string { + autolinkType := "Numeric" + if autolink.IsAlphanumeric { + autolinkType = "Alphanumeric" + } + + createdMsg := fmt.Sprintf( + "%s %s autolink created in %s (id: %d)", + cs.SuccessIconWithColor(cs.Green), + autolinkType, + ghrepo.FullName(repo), + autolink.ID, + ) + + autolinkMapMsg := cs.Bluef( + " %s%s → %s", + autolink.KeyPrefix, + "", + autolink.URLTemplate, + ) + + return fmt.Sprintf("%s\n%s\n", createdMsg, autolinkMapMsg) +} diff --git a/pkg/cmd/repo/autolink/create/create_test.go b/pkg/cmd/repo/autolink/create/create_test.go new file mode 100644 index 00000000000..3b31a9c7cb3 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/create_test.go @@ -0,0 +1,197 @@ +package create + +import ( + "bytes" + "fmt" + "net/http" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCmdCreate(t *testing.T) { + tests := []struct { + name string + input string + output createOptions + wantErr bool + errMsg string + }{ + { + name: "no argument", + input: "", + wantErr: true, + errMsg: "Cannot create autolink: keyPrefix and urlTemplate arguments are both required", + }, + { + name: "one argument", + input: "TEST-", + wantErr: true, + errMsg: "Cannot create autolink: keyPrefix and urlTemplate arguments are both required", + }, + { + name: "two argument", + input: "TICKET- https://example.com/TICKET?query=", + output: createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + }, + { + name: "numeric flag", + input: "TICKET- https://example.com/TICKET?query= --numeric", + output: createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + Numeric: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: ios, + } + f.HttpClient = func() (*http.Client, error) { + return &http.Client{}, nil + } + + argv, err := shlex.Split(tt.input) + require.NoError(t, err) + + var gotOpts *createOptions + cmd := NewCmdCreate(f, func(opts *createOptions) error { + gotOpts = opts + return nil + }) + + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + if tt.wantErr { + require.EqualError(t, err, tt.errMsg) + } else { + require.NoError(t, err) + assert.Equal(t, tt.output.KeyPrefix, gotOpts.KeyPrefix) + assert.Equal(t, tt.output.URLTemplate, gotOpts.URLTemplate) + assert.Equal(t, tt.output.Numeric, gotOpts.Numeric) + } + }) + } +} + +type stubAutoLinkCreator struct { + err error +} + +func (g stubAutoLinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) { + if g.err != nil { + return nil, g.err + } + + return &domain.Autolink{ + ID: 1, + KeyPrefix: request.KeyPrefix, + URLTemplate: request.URLTemplate, + IsAlphanumeric: request.IsAlphanumeric, + }, nil +} + +type testAutolinkClientCreateError struct{} + +func (e testAutolinkClientCreateError) Error() string { + return "autolink client create error" +} + +func TestCreateRun(t *testing.T) { + tests := []struct { + name string + opts *createOptions + stubCreator stubAutoLinkCreator + expectedErr error + errMsg string + wantStdout string + wantStderr string + }{ + { + name: "success, alphanumeric", + opts: &createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubCreator: stubAutoLinkCreator{}, + wantStdout: heredoc.Doc(` + ✓ Alphanumeric autolink created in OWNER/REPO (id: 1) + TICKET- → https://example.com/TICKET?query= + `), + }, + { + name: "success, numeric", + opts: &createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + Numeric: true, + }, + stubCreator: stubAutoLinkCreator{}, + wantStdout: heredoc.Doc(` + ✓ Numeric autolink created in OWNER/REPO (id: 1) + TICKET- → https://example.com/TICKET?query= + `), + }, + { + name: "client error", + opts: &createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubCreator: stubAutoLinkCreator{err: testAutolinkClientCreateError{}}, + expectedErr: testAutolinkClientCreateError{}, + errMsg: fmt.Sprint("error creating autolink: ", testAutolinkClientCreateError{}.Error()), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdout, stderr := iostreams.Test() + + opts := tt.opts + opts.IO = ios + opts.Browser = &browser.Stub{} + + opts.IO = ios + opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil } + + opts.AutolinkClient = &tt.stubCreator + err := createRun(opts) + + if tt.expectedErr != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tt.expectedErr) + assert.Equal(t, err.Error(), tt.errMsg) + } else { + require.NoError(t, err) + } + + if tt.wantStdout != "" { + assert.Equal(t, tt.wantStdout, stdout.String()) + } + + if tt.wantStderr != "" { + assert.Equal(t, tt.wantStderr, stderr.String()) + } + }) + } +} diff --git a/pkg/cmd/repo/autolink/create/http.go b/pkg/cmd/repo/autolink/create/http.go new file mode 100644 index 00000000000..e4923c3f02c --- /dev/null +++ b/pkg/cmd/repo/autolink/create/http.go @@ -0,0 +1,83 @@ +package create + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" +) + +type AutolinkCreator struct { + HTTPClient *http.Client +} + +type AutolinkCreateRequest struct { + IsAlphanumeric bool `json:"is_alphanumeric"` + KeyPrefix string `json:"key_prefix"` + URLTemplate string `json:"url_template"` +} + +func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) { + path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) + url := ghinstance.RESTPrefix(repo.RepoHost()) + path + + requestByte, err := json.Marshal(request) + if err != nil { + return nil, err + } + requestBody := bytes.NewReader(requestByte) + + req, err := http.NewRequest(http.MethodPost, url, requestBody) + if err != nil { + return nil, err + } + + resp, err := a.HTTPClient.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + // if resp.StatusCode != http.StatusCreated { + // return nil, api.HandleHTTPError(resp) + // } + + err = handleAutolinkCreateError(resp) + + if err != nil { + return nil, err + } + + var autolink domain.Autolink + + err = json.NewDecoder(resp.Body).Decode(&autolink) + if err != nil { + return nil, err + } + + return &autolink, nil +} + +func handleAutolinkCreateError(resp *http.Response) error { + switch resp.StatusCode { + case http.StatusCreated: + return nil + case http.StatusNotFound: + err := api.HandleHTTPError(resp) + var httpErr api.HTTPError + if errors.As(err, &httpErr) { + httpErr.Message = "Must have admin rights to Repository." + return httpErr + } + return err + default: + return api.HandleHTTPError(resp) + } +} diff --git a/pkg/cmd/repo/autolink/create/http_test.go b/pkg/cmd/repo/autolink/create/http_test.go new file mode 100644 index 00000000000..7f29f4611d1 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/http_test.go @@ -0,0 +1,155 @@ +package create + +import ( + "fmt" + "net/http" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAutoLinkCreator_Create(t *testing.T) { + repo := ghrepo.New("OWNER", "REPO") + + tests := []struct { + name string + req AutolinkCreateRequest + stubStatus int + stubRespJSON string + + expectedAutolink *domain.Autolink + expectErr bool + expectedErrMsg string + }{ + { + name: "201 successful creation", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubStatus: http.StatusCreated, + stubRespJSON: `{ + "id": 1, + "is_alphanumeric": true, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=" + }`, + expectedAutolink: &domain.Autolink{ + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + { + name: "422 URL template not valid URL", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "foo/", + }, + stubStatus: http.StatusUnprocessableEntity, + stubRespJSON: `{ + "message": "Validation Failed", + "errors": [ + { + "resource": "KeyLink", + "code": "custom", + "field": "url_template", + "message": "url_template must be an absolute URL" + } + ], + "documentation_url": "https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository", + "status": "422" + }`, + expectErr: true, + expectedErrMsg: heredoc.Doc(` + HTTP 422: Validation Failed (https://api.github.com/repos/OWNER/REPO/autolinks) + url_template must be an absolute URL`), + }, + { + name: "404 repo not found", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubStatus: http.StatusNotFound, + stubRespJSON: `{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository", + "status": "404" + }`, + expectErr: true, + expectedErrMsg: "HTTP 404: Must have admin rights to Repository. (https://api.github.com/repos/OWNER/REPO/autolinks)", + }, + { + name: "422 URL template missing ", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET", + }, + stubStatus: http.StatusUnprocessableEntity, + stubRespJSON: `{"message":"Validation Failed","errors":[{"resource":"KeyLink","code":"custom","field":"url_template","message":"url_template is missing a token"}],"documentation_url":"https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository","status":"422"}`, + expectErr: true, + expectedErrMsg: heredoc.Doc(` + HTTP 422: Validation Failed (https://api.github.com/repos/OWNER/REPO/autolinks) + url_template is missing a token`), + }, + { + name: "422 already exists", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + stubStatus: http.StatusUnprocessableEntity, + stubRespJSON: `{"message":"Validation Failed","errors":[{"resource":"KeyLink","code":"already_exists","field":"key_prefix"}],"documentation_url":"https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository","status":"422"}`, + expectErr: true, + expectedErrMsg: heredoc.Doc(` + HTTP 422: Validation Failed (https://api.github.com/repos/OWNER/REPO/autolinks) + KeyLink.key_prefix already exists`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + reg.Register( + httpmock.REST( + http.MethodPost, + fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName())), + httpmock.RESTPayload(tt.stubStatus, tt.stubRespJSON, + func(payload map[string]interface{}) { + require.Equal(t, map[string]interface{}{ + "is_alphanumeric": tt.req.IsAlphanumeric, + "key_prefix": tt.req.KeyPrefix, + "url_template": tt.req.URLTemplate, + }, payload) + }, + ), + ) + defer reg.Verify(t) + + autolinkCreator := &AutolinkCreator{ + HTTPClient: &http.Client{Transport: reg}, + } + + autolink, err := autolinkCreator.Create(repo, tt.req) + + if tt.expectErr { + require.EqualError(t, err, tt.expectedErrMsg) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedAutolink, autolink) + } + }) + } +} diff --git a/pkg/cmd/repo/autolink/domain/autolink.go b/pkg/cmd/repo/autolink/domain/autolink.go new file mode 100644 index 00000000000..9c3f10c05e1 --- /dev/null +++ b/pkg/cmd/repo/autolink/domain/autolink.go @@ -0,0 +1,14 @@ +package domain + +import "github.com/cli/cli/v2/pkg/cmdutil" + +type Autolink struct { + ID int `json:"id"` + IsAlphanumeric bool `json:"is_alphanumeric"` + KeyPrefix string `json:"key_prefix"` + URLTemplate string `json:"url_template"` +} + +func (a *Autolink) ExportData(fields []string) map[string]interface{} { + return cmdutil.StructExportData(a, fields) +} diff --git a/pkg/cmd/repo/autolink/list/http.go b/pkg/cmd/repo/autolink/list/http.go index 70d913d70e5..a5e707f4eeb 100644 --- a/pkg/cmd/repo/autolink/list/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -8,16 +8,17 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" ) type AutolinkLister struct { HTTPClient *http.Client } -func (a *AutolinkLister) List(repo ghrepo.Interface) ([]autolink, error) { +func (a *AutolinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) { path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) url := ghinstance.RESTPrefix(repo.RepoHost()) + path - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } @@ -33,7 +34,7 @@ func (a *AutolinkLister) List(repo ghrepo.Interface) ([]autolink, error) { } else if resp.StatusCode > 299 { return nil, api.HandleHTTPError(resp) } - var autolinks []autolink + var autolinks []domain.Autolink err = json.NewDecoder(resp.Body).Decode(&autolinks) if err != nil { return nil, err diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index fc1e44b238a..46f83484fc6 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,19 +16,19 @@ func TestAutoLinkLister_List(t *testing.T) { tests := []struct { name string repo ghrepo.Interface - resp []autolink + resp []domain.Autolink status int }{ { name: "no autolinks", repo: ghrepo.New("OWNER", "REPO"), - resp: []autolink{}, + resp: []domain.Autolink{}, status: 200, }, { name: "two autolinks", repo: ghrepo.New("OWNER", "REPO"), - resp: []autolink{ + resp: []domain.Autolink{ { ID: 1, IsAlphanumeric: true, @@ -54,7 +55,7 @@ func TestAutoLinkLister_List(t *testing.T) { t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} reg.Register( - httpmock.REST("GET", fmt.Sprintf("repos/%s/%s/autolinks", tt.repo.RepoOwner(), tt.repo.RepoName())), + httpmock.REST(http.MethodGet, fmt.Sprintf("repos/%s/%s/autolinks", tt.repo.RepoOwner(), tt.repo.RepoName())), httpmock.StatusJSONResponse(tt.status, tt.resp), ) defer reg.Verify(t) diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index d8a9c9f1292..8d4a484e9c0 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -9,6 +9,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/internal/text" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" @@ -21,29 +22,18 @@ var autolinkFields = []string{ "urlTemplate", } -type autolink struct { - ID int `json:"id"` - IsAlphanumeric bool `json:"is_alphanumeric"` - KeyPrefix string `json:"key_prefix"` - URLTemplate string `json:"url_template"` -} - -func (s *autolink) ExportData(fields []string) map[string]interface{} { - return cmdutil.StructExportData(s, fields) -} - type listOptions struct { BaseRepo func() (ghrepo.Interface, error) Browser browser.Browser - AutolinkClient AutolinkClient + AutolinkClient AutolinkListClient IO *iostreams.IOStreams Exporter cmdutil.Exporter WebMode bool } -type AutolinkClient interface { - List(repo ghrepo.Interface) ([]autolink, error) +type AutolinkListClient interface { + List(repo ghrepo.Interface) ([]domain.Autolink, error) } func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command { diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index 3fc8e0261dd..0ba350ab05c 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -8,6 +8,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/jsonfieldstest" @@ -96,11 +97,11 @@ func TestNewCmdList(t *testing.T) { } type stubAutoLinkLister struct { - autolinks []autolink + autolinks []domain.Autolink err error } -func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]autolink, error) { +func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) { return g.autolinks, g.err } @@ -125,7 +126,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{ + autolinks: []domain.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -161,7 +162,7 @@ func TestListRun(t *testing.T) { }, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{ + autolinks: []domain.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -184,7 +185,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: false, stubLister: stubAutoLinkLister{ - autolinks: []autolink{ + autolinks: []domain.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -210,7 +211,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{}, + autolinks: []domain.Autolink{}, }, expectedErr: cmdutil.NewNoResultsError("no autolinks found in OWNER/REPO"), wantStderr: "", @@ -220,7 +221,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{}, + autolinks: []domain.Autolink{}, err: testAutolinkClientListError{}, }, expectedErr: testAutolinkClientListError{}, diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index 196a047d889..4e61d12f44f 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -188,7 +188,11 @@ func RESTPayload(responseStatus int, responseBody string, cb func(payload map[st return nil, err } cb(bodyData) - return httpResponse(responseStatus, req, bytes.NewBufferString(responseBody)), nil + + header := http.Header{ + "Content-Type": []string{"application/json"}, + } + return httpResponseWithHeader(responseStatus, req, bytes.NewBufferString(responseBody), header), nil } } From fdf9a6e2f6ac605904b4bfad5dad1cdfc1729df0 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Sun, 5 Jan 2025 19:41:15 -0500 Subject: [PATCH 2/4] Fix typos --- pkg/cmd/repo/autolink/create/create.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go index 698feb555f8..7c75f135239 100644 --- a/pkg/cmd/repo/autolink/create/create.go +++ b/pkg/cmd/repo/autolink/create/create.go @@ -44,11 +44,11 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Co The %[1]skeyPrefix%[1]s specifies the prefix that will generate a link when it is appended by certain characters. - The %[1]surlTemplate%[1]s specifies the target URL that will be generated when the keyPrefix is found. The urlTemplate must contain %[1]s%[1]s for the reference number. %[1]s%[1]s matches different characters depending on the whether the autolink is specified as numeric or alphanumeric. + The %[1]surlTemplate%[1]s specifies the target URL that will be generated when the keyPrefix is found. The %[1]surlTemplate%[1]s must contain %[1]s%[1]s for the reference number. %[1]s%[1]s matches different characters depending on the whether the autolink is specified as numeric or alphanumeric. - By default, the command will create an alphanumeric autolink. This means that the %[1]s%[1]s in the %[1]surlTemplate%[1]s will match alphanumeric characters %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s. To create a numeric autolink, use the %[1]s--numeric%[1]s flag. Numeric autolinks only match against numeric characters. If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. + By default, the command will create an alphanumeric autolink. This means that the %[1]s%[1]s in the %[1]surlTemplate%[1]s will match alphanumeric characters %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s. To create a numeric autolink, use the %[1]s--numeric%[1]s flag. Numeric autolinks only match against numeric characters. If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. - If you are using a shell that applies special meaning to angle brackets, you you will need to need to escape these characters in the %[1]surlTemplate%[1]s or place quotation marks around this argument. + If you are using a shell that applies special meaning to angle brackets, you will need to escape these characters in the %[1]surlTemplate%[1]s or place quotation marks around the whole argument. Only repository administrators can create autolinks. `, "`"), From e916ae5b437486a12805c5e71e9f029e6d846634 Mon Sep 17 00:00:00 2001 From: Michael Hoffman Date: Mon, 13 Jan 2025 09:08:19 -0500 Subject: [PATCH 3/4] Rename domain pkg to shared --- pkg/cmd/repo/autolink/create/create.go | 6 +++--- pkg/cmd/repo/autolink/create/create_test.go | 6 +++--- pkg/cmd/repo/autolink/create/http.go | 6 +++--- pkg/cmd/repo/autolink/create/http_test.go | 6 +++--- pkg/cmd/repo/autolink/list/http.go | 6 +++--- pkg/cmd/repo/autolink/list/http_test.go | 8 ++++---- pkg/cmd/repo/autolink/list/list.go | 4 ++-- pkg/cmd/repo/autolink/list/list_test.go | 16 ++++++++-------- .../repo/autolink/{domain => shared}/autolink.go | 2 +- 9 files changed, 30 insertions(+), 30 deletions(-) rename pkg/cmd/repo/autolink/{domain => shared}/autolink.go (96%) diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go index 7c75f135239..6c321db5b16 100644 --- a/pkg/cmd/repo/autolink/create/create.go +++ b/pkg/cmd/repo/autolink/create/create.go @@ -6,7 +6,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" @@ -25,7 +25,7 @@ type createOptions struct { } type AutolinkCreateClient interface { - Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) + Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) } func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Command { @@ -114,7 +114,7 @@ func createRun(opts *createOptions) error { return nil } -func successMsg(autolink *domain.Autolink, repo ghrepo.Interface, cs *iostreams.ColorScheme) string { +func successMsg(autolink *shared.Autolink, repo ghrepo.Interface, cs *iostreams.ColorScheme) string { autolinkType := "Numeric" if autolink.IsAlphanumeric { autolinkType = "Alphanumeric" diff --git a/pkg/cmd/repo/autolink/create/create_test.go b/pkg/cmd/repo/autolink/create/create_test.go index 3b31a9c7cb3..43fe3e4708b 100644 --- a/pkg/cmd/repo/autolink/create/create_test.go +++ b/pkg/cmd/repo/autolink/create/create_test.go @@ -9,7 +9,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" @@ -97,12 +97,12 @@ type stubAutoLinkCreator struct { err error } -func (g stubAutoLinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) { +func (g stubAutoLinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) { if g.err != nil { return nil, g.err } - return &domain.Autolink{ + return &shared.Autolink{ ID: 1, KeyPrefix: request.KeyPrefix, URLTemplate: request.URLTemplate, diff --git a/pkg/cmd/repo/autolink/create/http.go b/pkg/cmd/repo/autolink/create/http.go index e4923c3f02c..d7ee940b907 100644 --- a/pkg/cmd/repo/autolink/create/http.go +++ b/pkg/cmd/repo/autolink/create/http.go @@ -10,7 +10,7 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" ) type AutolinkCreator struct { @@ -23,7 +23,7 @@ type AutolinkCreateRequest struct { URLTemplate string `json:"url_template"` } -func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*domain.Autolink, error) { +func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) { path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) url := ghinstance.RESTPrefix(repo.RepoHost()) + path @@ -55,7 +55,7 @@ func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRe return nil, err } - var autolink domain.Autolink + var autolink shared.Autolink err = json.NewDecoder(resp.Body).Decode(&autolink) if err != nil { diff --git a/pkg/cmd/repo/autolink/create/http_test.go b/pkg/cmd/repo/autolink/create/http_test.go index 7f29f4611d1..9ef5e0da5ae 100644 --- a/pkg/cmd/repo/autolink/create/http_test.go +++ b/pkg/cmd/repo/autolink/create/http_test.go @@ -7,7 +7,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,7 +22,7 @@ func TestAutoLinkCreator_Create(t *testing.T) { stubStatus int stubRespJSON string - expectedAutolink *domain.Autolink + expectedAutolink *shared.Autolink expectErr bool expectedErrMsg string }{ @@ -40,7 +40,7 @@ func TestAutoLinkCreator_Create(t *testing.T) { "key_prefix": "TICKET-", "url_template": "https://example.com/TICKET?query=" }`, - expectedAutolink: &domain.Autolink{ + expectedAutolink: &shared.Autolink{ ID: 1, KeyPrefix: "TICKET-", URLTemplate: "https://example.com/TICKET?query=", diff --git a/pkg/cmd/repo/autolink/list/http.go b/pkg/cmd/repo/autolink/list/http.go index a5e707f4eeb..cdb8e621c61 100644 --- a/pkg/cmd/repo/autolink/list/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -8,14 +8,14 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" ) type AutolinkLister struct { HTTPClient *http.Client } -func (a *AutolinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) { +func (a *AutolinkLister) List(repo ghrepo.Interface) ([]shared.Autolink, error) { path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName()) url := ghinstance.RESTPrefix(repo.RepoHost()) + path req, err := http.NewRequest(http.MethodGet, url, nil) @@ -34,7 +34,7 @@ func (a *AutolinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) } else if resp.StatusCode > 299 { return nil, api.HandleHTTPError(resp) } - var autolinks []domain.Autolink + var autolinks []shared.Autolink err = json.NewDecoder(resp.Body).Decode(&autolinks) if err != nil { return nil, err diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index 46f83484fc6..0fa918f6116 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,19 +16,19 @@ func TestAutoLinkLister_List(t *testing.T) { tests := []struct { name string repo ghrepo.Interface - resp []domain.Autolink + resp []shared.Autolink status int }{ { name: "no autolinks", repo: ghrepo.New("OWNER", "REPO"), - resp: []domain.Autolink{}, + resp: []shared.Autolink{}, status: 200, }, { name: "two autolinks", repo: ghrepo.New("OWNER", "REPO"), - resp: []domain.Autolink{ + resp: []shared.Autolink{ { ID: 1, IsAlphanumeric: true, diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index 8d4a484e9c0..a68c03f3c94 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -9,7 +9,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/internal/text" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" @@ -33,7 +33,7 @@ type listOptions struct { } type AutolinkListClient interface { - List(repo ghrepo.Interface) ([]domain.Autolink, error) + List(repo ghrepo.Interface) ([]shared.Autolink, error) } func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command { diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index 0ba350ab05c..1e4d73ab801 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -8,7 +8,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/autolink/domain" + "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/jsonfieldstest" @@ -97,11 +97,11 @@ func TestNewCmdList(t *testing.T) { } type stubAutoLinkLister struct { - autolinks []domain.Autolink + autolinks []shared.Autolink err error } -func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]domain.Autolink, error) { +func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]shared.Autolink, error) { return g.autolinks, g.err } @@ -126,7 +126,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{ + autolinks: []shared.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -162,7 +162,7 @@ func TestListRun(t *testing.T) { }, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{ + autolinks: []shared.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -185,7 +185,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: false, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{ + autolinks: []shared.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -211,7 +211,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{}, + autolinks: []shared.Autolink{}, }, expectedErr: cmdutil.NewNoResultsError("no autolinks found in OWNER/REPO"), wantStderr: "", @@ -221,7 +221,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []domain.Autolink{}, + autolinks: []shared.Autolink{}, err: testAutolinkClientListError{}, }, expectedErr: testAutolinkClientListError{}, diff --git a/pkg/cmd/repo/autolink/domain/autolink.go b/pkg/cmd/repo/autolink/shared/autolink.go similarity index 96% rename from pkg/cmd/repo/autolink/domain/autolink.go rename to pkg/cmd/repo/autolink/shared/autolink.go index 9c3f10c05e1..d79537c3184 100644 --- a/pkg/cmd/repo/autolink/domain/autolink.go +++ b/pkg/cmd/repo/autolink/shared/autolink.go @@ -1,4 +1,4 @@ -package domain +package shared import "github.com/cli/cli/v2/pkg/cmdutil" From 7c31d1a76b6c7a0e3eeb71ccb34b55b679e5fef5 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 27 Jan 2025 17:47:55 -0500 Subject: [PATCH 4/4] Minor refactoring autolink create help and logic - simplified and wrapped `gh repo autolink create` and `gh repo autolink` long help usage docs - simplified success message, brought into alignment with other commands --- pkg/cmd/repo/autolink/autolink.go | 14 ++--- pkg/cmd/repo/autolink/create/create.go | 69 +++++++-------------- pkg/cmd/repo/autolink/create/create_test.go | 13 +--- 3 files changed, 33 insertions(+), 63 deletions(-) diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index b988de60d0d..1fd2183543a 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -13,14 +13,12 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command { Use: "autolink ", Short: "Manage autolink references", Long: heredoc.Docf(` - Work with GitHub autolink references. - - GitHub autolinks require admin access to configure and can be found at - https://github.com/{owner}/{repo}/settings/key_links. - Use %[1]sgh repo autolink list --web%[1]s to open this page for the current repository. - - For more information about GitHub autolinks, see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/configuring-autolinks-to-reference-external-resources - `, "`"), + Autolinks link issues, pull requests, commit messages, and release descriptions to external third-party services. + + Autolinks require %[1]sadmin%[1]s role to view or manage. + + For more information, see + `, "`"), } cmdutil.EnableRepoOverride(cmd, f) diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go index 6c321db5b16..ac3035c79e9 100644 --- a/pkg/cmd/repo/autolink/create/create.go +++ b/pkg/cmd/repo/autolink/create/create.go @@ -40,32 +40,35 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Co Long: heredoc.Docf(` Create a new autolink reference for a repository. - Autolinks automatically generate links to external resources when they appear in an issue, pull request, or commit. + The %[1]skeyPrefix%[1]s argument specifies the prefix that will generate a link when it is appended by certain characters. - The %[1]skeyPrefix%[1]s specifies the prefix that will generate a link when it is appended by certain characters. + The %[1]surlTemplate%[1]s argument specifies the target URL that will be generated when the keyPrefix is found, which + must contain %[1]s%[1]s variable for the reference number. - The %[1]surlTemplate%[1]s specifies the target URL that will be generated when the keyPrefix is found. The %[1]surlTemplate%[1]s must contain %[1]s%[1]s for the reference number. %[1]s%[1]s matches different characters depending on the whether the autolink is specified as numeric or alphanumeric. + By default, autolinks are alphanumeric with %[1]s--numeric%[1]s flag used to create a numeric autolink. - By default, the command will create an alphanumeric autolink. This means that the %[1]s%[1]s in the %[1]surlTemplate%[1]s will match alphanumeric characters %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s. To create a numeric autolink, use the %[1]s--numeric%[1]s flag. Numeric autolinks only match against numeric characters. If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. + The %[1]s%[1]s variable behavior differs depending on whether the autolink is alphanumeric or numeric: - If you are using a shell that applies special meaning to angle brackets, you will need to escape these characters in the %[1]surlTemplate%[1]s or place quotation marks around the whole argument. - - Only repository administrators can create autolinks. + - alphanumeric: matches %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s + - numeric: matches %[1]s0-9%[1]s + + If the template contains multiple instances of %[1]s%[1]s, only the first will be replaced. `, "`"), Example: heredoc.Doc(` - # Create an alphanumeric autolink to example.com for the key prefix "TICKET-". This will generate a link to https://example.com/TICKET?query=123abc when "TICKET-123abc" appears. - $ gh repo autolink create TICKET- https://example.com/TICKET?query= - - # Create a numeric autolink to example.com for the key prefix "STORY-". This will generate a link to https://example.com/STORY?id=123 when "STORY-123" appears. - $ gh repo autolink create STORY- https://example.com/STORY?id= --numeric + # Create an alphanumeric autolink to example.com for the key prefix "TICKET-". + # Generates https://example.com/TICKET?query=123abc from "TICKET-123abc". + $ gh repo autolink create TICKET- "https://example.com/TICKET?query=" + # Create a numeric autolink to example.com for the key prefix "STORY-". + # Generates https://example.com/STORY?id=123 from "STORY-123". + $ gh repo autolink create STORY- "https://example.com/STORY?id=" --numeric `), Args: cmdutil.ExactArgs(2, "Cannot create autolink: keyPrefix and urlTemplate arguments are both required"), Aliases: []string{"new"}, RunE: func(c *cobra.Command, args []string) error { opts.BaseRepo = f.BaseRepo - httpClient, err := f.HttpClient() + httpClient, err := f.HttpClient() if err != nil { return err } @@ -82,7 +85,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&opts.Numeric, "numeric", "n", false, "Mark autolink as non-alphanumeric") + cmd.Flags().BoolVarP(&opts.Numeric, "numeric", "n", false, "Mark autolink as numeric") return cmd } @@ -92,48 +95,24 @@ func createRun(opts *createOptions) error { if err != nil { return err } - cs := opts.IO.ColorScheme() - - isAlphanumeric := !opts.Numeric request := AutolinkCreateRequest{ KeyPrefix: opts.KeyPrefix, URLTemplate: opts.URLTemplate, - IsAlphanumeric: isAlphanumeric, + IsAlphanumeric: !opts.Numeric, } autolink, err := opts.AutolinkClient.Create(repo, request) - if err != nil { - return fmt.Errorf("%s %w", cs.Red("error creating autolink:"), err) - } - - msg := successMsg(autolink, repo, cs) - fmt.Fprint(opts.IO.Out, msg) - - return nil -} - -func successMsg(autolink *shared.Autolink, repo ghrepo.Interface, cs *iostreams.ColorScheme) string { - autolinkType := "Numeric" - if autolink.IsAlphanumeric { - autolinkType = "Alphanumeric" + return fmt.Errorf("error creating autolink: %w", err) } - createdMsg := fmt.Sprintf( - "%s %s autolink created in %s (id: %d)", + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.Out, + "%s Created repository autolink %d on %s\n", cs.SuccessIconWithColor(cs.Green), - autolinkType, - ghrepo.FullName(repo), autolink.ID, - ) + ghrepo.FullName(repo)) - autolinkMapMsg := cs.Bluef( - " %s%s → %s", - autolink.KeyPrefix, - "", - autolink.URLTemplate, - ) - - return fmt.Sprintf("%s\n%s\n", createdMsg, autolinkMapMsg) + return nil } diff --git a/pkg/cmd/repo/autolink/create/create_test.go b/pkg/cmd/repo/autolink/create/create_test.go index 43fe3e4708b..477d28da9e4 100644 --- a/pkg/cmd/repo/autolink/create/create_test.go +++ b/pkg/cmd/repo/autolink/create/create_test.go @@ -6,7 +6,6 @@ import ( "net/http" "testing" - "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared" @@ -133,10 +132,7 @@ func TestCreateRun(t *testing.T) { URLTemplate: "https://example.com/TICKET?query=", }, stubCreator: stubAutoLinkCreator{}, - wantStdout: heredoc.Doc(` - ✓ Alphanumeric autolink created in OWNER/REPO (id: 1) - TICKET- → https://example.com/TICKET?query= - `), + wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n", }, { name: "success, numeric", @@ -146,10 +142,7 @@ func TestCreateRun(t *testing.T) { Numeric: true, }, stubCreator: stubAutoLinkCreator{}, - wantStdout: heredoc.Doc(` - ✓ Numeric autolink created in OWNER/REPO (id: 1) - TICKET- → https://example.com/TICKET?query= - `), + wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n", }, { name: "client error", @@ -180,7 +173,7 @@ func TestCreateRun(t *testing.T) { if tt.expectedErr != nil { require.Error(t, err) assert.ErrorIs(t, err, tt.expectedErr) - assert.Equal(t, err.Error(), tt.errMsg) + assert.Equal(t, tt.errMsg, err.Error()) } else { require.NoError(t, err) }