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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions pkg/cmd/repo/autolink/autolink.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -12,18 +13,17 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command {
Use: "autolink <command>",
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 <https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/configuring-autolinks-to-reference-external-resources>
`, "`"),
}
cmdutil.EnableRepoOverride(cmd, f)

cmd.AddCommand(cmdList.NewCmdList(f, nil))
cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))

return cmd
}
118 changes: 118 additions & 0 deletions pkg/cmd/repo/autolink/create/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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/shared"
"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) (*shared.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 <keyPrefix> <urlTemplate>",
Short: "Create a new autolink reference",
Long: heredoc.Docf(`
Create a new autolink reference for a repository.

The %[1]skeyPrefix%[1]s argument 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<num>%[1]s variable for the reference number.

By default, autolinks are alphanumeric with %[1]s--numeric%[1]s flag used to create a numeric autolink.

The %[1]s<num>%[1]s variable behavior differs depending on whether the autolink is alphanumeric or numeric:

- 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<num>%[1]s, only the first will be replaced.
`, "`"),
Example: heredoc.Doc(`
# 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=<num>"

# 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=<num>" --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 numeric")

return cmd
}

func createRun(opts *createOptions) error {
repo, err := opts.BaseRepo()
if err != nil {
return err
}

request := AutolinkCreateRequest{
KeyPrefix: opts.KeyPrefix,
URLTemplate: opts.URLTemplate,
IsAlphanumeric: !opts.Numeric,
}

autolink, err := opts.AutolinkClient.Create(repo, request)
if err != nil {
return fmt.Errorf("error creating autolink: %w", err)
}

cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out,
"%s Created repository autolink %d on %s\n",
cs.SuccessIconWithColor(cs.Green),
autolink.ID,
ghrepo.FullName(repo))

return nil
}
190 changes: 190 additions & 0 deletions pkg/cmd/repo/autolink/create/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package create

import (
"bytes"
"fmt"
"net/http"
"testing"

"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"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"
"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=<num>",
output: createOptions{
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
},
},
{
name: "numeric flag",
input: "TICKET- https://example.com/TICKET?query=<num> --numeric",
output: createOptions{
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
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) (*shared.Autolink, error) {
if g.err != nil {
return nil, g.err
}

return &shared.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=<num>",
},
stubCreator: stubAutoLinkCreator{},
wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n",
},
{
name: "success, numeric",
opts: &createOptions{
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
Numeric: true,
},
stubCreator: stubAutoLinkCreator{},
wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n",
},
{
name: "client error",
opts: &createOptions{
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
},
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, tt.errMsg, err.Error())
} 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())
}
})
}
}
Loading