Compare commits

...

8 Commits

Author SHA1 Message Date
Erran Carey 148864a125 Merge branch 'add-award-emojis' into 'main'
Draft: Add award-emoji subcommands

Closes #1202

See merge request https://gitlab.com/gitlab-org/cli/-/merge_requests/1148

Merged-by: Erran Carey <ecarey@gitlab.com>
2024-04-27 06:04:17 +00:00
Oscar Tovar 9ed43f6507 Merge branch 'feature/allow-to-set-client-id' into 'main'
feat: allow to provide custom client-id

See merge request https://gitlab.com/gitlab-org/cli/-/merge_requests/1440

Merged-by: Oscar Tovar <otovar@gitlab.com>
Approved-by: Amy Qualls <aqualls@gitlab.com>
Approved-by: Greg Alfaro <galfaro@gitlab.com>
Approved-by: Oscar Tovar <otovar@gitlab.com>
Reviewed-by: Oscar Tovar <otovar@gitlab.com>
Co-authored-by: avoidik <avoidik@gmail.com>
2024-04-24 19:27:12 +00:00
Viacheslav Vasilyev d4663189c2 feat: allow to provide custom client-id 2024-04-24 19:27:12 +00:00
Shekhar Patnaik 7953c99e8e Merge branch 'gmh-fix-environment-variable-tests' into 'main'
test: fix issue with environment variables being set

See merge request https://gitlab.com/gitlab-org/cli/-/merge_requests/1449

Merged-by: Shekhar Patnaik <spatnaik@gitlab.com>
Approved-by: Vitali Tatarintev <vtatarintev@gitlab.com>
Approved-by: Shekhar Patnaik <spatnaik@gitlab.com>
Co-authored-by: Gary Holtz <gholtz@gitlab.com>
2024-04-24 14:51:44 +00:00
Shekhar Patnaik 4b3defdd0f Merge branch 'update-avast' into 'main'
chore(deps): Update retry-go dependency

See merge request https://gitlab.com/gitlab-org/cli/-/merge_requests/1442

Merged-by: Shekhar Patnaik <spatnaik@gitlab.com>
Approved-by: Erran Carey <ecarey@gitlab.com>
Approved-by: Shekhar Patnaik <spatnaik@gitlab.com>
Co-authored-by: Mikel Olasagasti Uranga <mikel@olasagasti.info>
2024-04-24 14:50:58 +00:00
Mikel d2eabc73b9 chore(deps): Update retry-go dependency 2024-04-24 14:50:58 +00:00
Gary Holtz a09c60159e
test: fix issue with environment variables being set 2024-04-20 16:19:20 -05:00
Erran Carey 4a0e82370c
Add award-emoji create subcommands 2023-01-19 19:58:40 +00:00
21 changed files with 343 additions and 63 deletions

View File

@ -140,9 +140,9 @@ To build from source:
## Authentication
### OAuth (GitLab.com only)
### OAuth (GitLab.com)
To authenticate your installation of `glab` with OAuth:
To authenticate your installation of `glab` with an OAuth application connected to GitLab.com:
1. Start interactive setup with `glab auth login`.
1. For the GitLab instance you want to sign in to, select **GitLab.com**.
@ -151,6 +151,30 @@ To authenticate your installation of `glab` with OAuth:
1. Select **Authorize**.
1. Complete the authentication process in your terminal, selecting the appropriate options for your needs.
### OAuth (self-managed)
Prerequisites:
- You've created an OAuth application at the user, group, or instance level, and you
have its application ID. For instructions, see how to configure GitLab
[as an OAuth 2.0 authentication identity provider](https://docs.gitlab.com/ee/integration/oauth_provider.html)
in the GitLab documentation.
- Your OAuth application is configured with these parameters:
- **Redirect URI** is `http://localhost:7171/auth/redirect`.
- **Confidential** is not selected.
- **Scopes** are `openid`, `profile`, `read_user`, `write_repository`, and `api`.
To authenticate your installation of `glab` with an OAuth application connected
to your self-managed instance:
1. Store the application ID with `glab config set client_id <CLIENT_ID> --host <HOSTNAME>`.
For `<CLIENT_ID>`, provide your application ID.
1. Start interactive setup with `glab auth login --hostname <HOSTNAME>`.
1. For the login method, select **Web**. This selection launches your web browser
to request authorization for the GitLab CLI to use your self-managed account.
1. Select **Authorize**.
1. Complete the authentication process in your terminal, selecting the appropriate options for your needs.
### Personal Access Token
To authenticate your installation of `glab` with a personal access token:
@ -235,6 +259,7 @@ glab config set ca_cert /path/to/server.pem --host gitlab.example.com
Can be set in the config with `glab config set token xxxxxx`
- `GITLAB_URI` or `GITLAB_HOST`: specify the URL of the GitLab server if self-managed (eg: `https://gitlab.example.com`). Default is `https://gitlab.com`.
- `GITLAB_API_HOST`: specify the host where the API endpoint is found. Useful when there are separate (sub)domains or hosts for Git and the API endpoint: defaults to the hostname found in the Git URL
- `GITLAB_CLIENT_ID`: a custom Client-ID generated by the GitLab OAuth 2.0 application. Defaults to the Client-ID for GitLab.com.
- `GITLAB_REPO`: Default GitLab repository used for commands accepting the `--repo` option. Only used if no `--repo` option is given.
- `GITLAB_GROUP`: Default GitLab group used for listing merge requests, issues and variables. Only used if no `--group` option is given.
- `REMOTE_ALIAS` or `GIT_REMOTE_URL_VAR`: `git remote` variable or alias that contains the GitLab URL.

View File

@ -129,6 +129,19 @@ var DeleteIssue = func(client *gitlab.Client, projectID interface{}, issueID int
return nil
}
var CreateIssueAwardEmoji = func(client *gitlab.Client, projectID interface{}, mrID int, opts *gitlab.CreateAwardEmojiOptions) (*gitlab.AwardEmoji, error) {
if client == nil {
client = apiClient.Lab()
}
emoji, _, err := client.AwardEmoji.CreateIssueAwardEmoji(projectID, mrID, opts)
if err != nil {
return emoji, err
}
return emoji, nil
}
var CreateIssueNote = func(client *gitlab.Client, projectID interface{}, mrID int, opts *gitlab.CreateIssueNoteOptions) (*gitlab.Note, error) {
if client == nil {
client = apiClient.Lab()

View File

@ -195,6 +195,19 @@ var GetMRLinkedIssues = func(client *gitlab.Client, projectID interface{}, mrID
return mrIssues, nil
}
var CreateMRAwardEmoji = func(client *gitlab.Client, projectID interface{}, mrID int, opts *gitlab.CreateAwardEmojiOptions) (*gitlab.AwardEmoji, error) {
if client == nil {
client = apiClient.Lab()
}
emoji, _, err := client.AwardEmoji.CreateMergeRequestAwardEmoji(projectID, mrID, opts)
if err != nil {
return emoji, err
}
return emoji, nil
}
var CreateMRNote = func(client *gitlab.Client, projectID interface{}, mrID int, opts *gitlab.CreateMergeRequestNoteOptions) (*gitlab.Note, error) {
if client == nil {
client = apiClient.Lab()

View File

@ -11,6 +11,7 @@ import (
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/internal/config"
"gitlab.com/gitlab-org/cli/test"
)
func Test_groupGraphQLVariables(t *testing.T) {
@ -96,7 +97,7 @@ hosts:
username: monalisa
token: OTOKEN
`, "")()
t.Setenv("GITLAB_TOKEN", "")
test.ClearEnvironmentVariables(t)
api.SetUserAgent("v1.2.3", "darwin", "arm64")
versionString := "glab/v1.2.3 (darwin, arm64)"

View File

@ -16,6 +16,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
"github.com/zalando/go-keyring"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/internal/config"
@ -90,13 +91,15 @@ func NewCmdLogin(f *cmdutils.Factory) *cobra.Command {
}
}
if !opts.Interactive {
if opts.Hostname == "" {
opts.Hostname = glinstance.Default()
}
if !opts.Interactive && opts.Hostname == "" {
opts.Hostname = glinstance.Default()
}
return loginRun()
if err := loginRun(opts); err != nil {
return cmdutils.WrapError(err, "Could not sign in!")
}
return nil
},
}
@ -108,7 +111,7 @@ func NewCmdLogin(f *cmdutils.Factory) *cobra.Command {
return cmd
}
func loginRun() error {
func loginRun(opts *LoginOptions) error {
c := opts.IO.Color()
cfg, err := opts.Config()
if err != nil {
@ -175,6 +178,8 @@ func loginRun() error {
return fmt.Errorf("could not prompt: %w", err)
}
}
} else {
isSelfHosted = glinstance.IsSelfHosted(hostname)
}
fmt.Fprintf(opts.IO.StdErr, "- Logging into %s\n", hostname)
@ -211,36 +216,24 @@ func loginRun() error {
}
}
loginType := 0
var loginType string
if opts.Interactive {
if isSelfHosted {
err := survey.AskOne(&survey.Select{
Message: "How would you like to login?",
Options: []string{
"Token",
},
}, &loginType)
if err != nil {
return fmt.Errorf("could not get login type: %w", err)
}
} else {
err := survey.AskOne(&survey.Select{
Message: "How would you like to login?",
Options: []string{
"Token",
"Web",
},
}, &loginType)
if err != nil {
return fmt.Errorf("could not get login type: %w", err)
}
err := survey.AskOne(&survey.Select{
Message: "How would you like to sign in?",
Options: []string{
"Token",
"Web",
},
}, &loginType)
if err != nil {
return fmt.Errorf("could not get login type: %w", err)
}
}
var token string
if loginType == 0 {
token, err = showTokenPrompt(hostname)
if strings.EqualFold(loginType, "token") {
token, err = showTokenPrompt(opts.IO, hostname)
if err != nil {
return err
}
@ -389,9 +382,9 @@ func getAccessTokenTip(hostname string) string {
The minimum required scopes are 'api' and 'write_repository'.`, glHostname)
}
func showTokenPrompt(hostname string) (string, error) {
fmt.Fprintln(opts.IO.StdErr)
fmt.Fprintln(opts.IO.StdErr, heredoc.Doc(getAccessTokenTip(hostname)))
func showTokenPrompt(io *iostreams.IOStreams, hostname string) (string, error) {
fmt.Fprintln(io.StdErr)
fmt.Fprintln(io.StdErr, heredoc.Doc(getAccessTokenTip(hostname)))
var token string
err := survey.AskOne(&survey.Password{

View File

@ -0,0 +1,66 @@
package award_emoji
import (
"errors"
"fmt"
"gitlab.com/gitlab-org/cli/commands/issue/issueutils"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/pkg/utils"
"github.com/spf13/cobra"
gitlab "github.com/xanzy/go-gitlab"
)
func NewCmdAwardEmoji(f *cmdutils.Factory) *cobra.Command {
issueAwardEmojiCreateCmd := &cobra.Command{
Use: "award-emoji <issue-id>",
Aliases: []string{"comment"},
Short: "Award an emoji to an issue on GitLab",
Long: ``,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
out := f.IO.StdOut
apiClient, err := f.HttpClient()
if err != nil {
return err
}
issue, repo, err := issueutils.IssueFromArg(apiClient, f.BaseRepo, args[0])
if err != nil {
return err
}
name, _ := cmd.Flags().GetString("name")
if name == "" {
name = utils.Editor(utils.EditorOptions{
Label: "Award Emoji Name:",
Help: "Enter the award emoji's name. ",
FileName: "ISSUE_AWARD_EMOJI_EDITMSG",
})
}
if name == "" {
return errors.New("aborted... Award Emoji name is empty")
}
emoji, err := api.CreateIssueAwardEmoji(apiClient, repo.FullName(), issue.IID, &gitlab.CreateAwardEmojiOptions{
Name: name,
})
if err != nil {
return err
}
fmt.Fprintf(out, "Added award emoji %d\n", emoji.AwardableID)
return nil
},
}
issueAwardEmojiCreateCmd.Flags().StringP("name", "n", "", "Award Emoji name")
return issueAwardEmojiCreateCmd
}

View File

@ -3,6 +3,7 @@ package issue
import (
"github.com/MakeNowJust/heredoc"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
issueAwardEmojiCmd "gitlab.com/gitlab-org/cli/commands/issue/award-emoji"
issueBoardCmd "gitlab.com/gitlab-org/cli/commands/issue/board"
issueCloseCmd "gitlab.com/gitlab-org/cli/commands/issue/close"
issueCreateCmd "gitlab.com/gitlab-org/cli/commands/issue/create"
@ -40,6 +41,7 @@ func NewCmdIssue(f *cmdutils.Factory) *cobra.Command {
cmdutils.EnableRepoOverride(issueCmd, f)
issueCmd.AddCommand(issueAwardEmojiCmd.NewCmdAwardEmoji(f))
issueCmd.AddCommand(issueCloseCmd.NewCmdClose(f))
issueCmd.AddCommand(issueBoardCmd.NewCmdBoard(f))
issueCmd.AddCommand(issueCreateCmd.NewCmdCreate(f))

View File

@ -0,0 +1,66 @@
package award_emoji
import (
"errors"
"fmt"
"gitlab.com/gitlab-org/cli/commands/mr/mrutils"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/pkg/utils"
"github.com/spf13/cobra"
gitlab "github.com/xanzy/go-gitlab"
)
func NewCmdAwardEmoji(f *cmdutils.Factory) *cobra.Command {
mrAwardEmojiCreateCmd := &cobra.Command{
Use: "award-emoji <id>",
Aliases: []string{"comment"},
Short: "Award an emoji to an merge request on GitLab",
Long: ``,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
out := f.IO.StdOut
apiClient, err := f.HttpClient()
if err != nil {
return err
}
mr, repo, err := mrutils.MRFromArgs(f, args, "any")
if err != nil {
return err
}
name, _ := cmd.Flags().GetString("name")
if name == "" {
name = utils.Editor(utils.EditorOptions{
Label: "Award Emoji Name:",
Help: "Enter the award emoji's name. ",
FileName: "MR_AWARD_EMOJI_EDITMSG",
})
}
if name == "" {
return errors.New("aborted... Award Emoji name is empty")
}
emoji, err := api.CreateMRAwardEmoji(apiClient, repo.FullName(), mr.IID, &gitlab.CreateAwardEmojiOptions{
Name: name,
})
if err != nil {
return err
}
fmt.Fprintf(out, "Added award emoji %d\n", emoji.AwardableID)
return nil
},
}
mrAwardEmojiCreateCmd.Flags().StringP("name", "n", "", "Award Emoji name")
return mrAwardEmojiCreateCmd
}

View File

@ -10,7 +10,7 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/avast/retry-go"
"github.com/avast/retry-go/v4"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/mr/mrutils"
"gitlab.com/gitlab-org/cli/pkg/prompt"

View File

@ -5,6 +5,7 @@ import (
"gitlab.com/gitlab-org/cli/commands/cmdutils"
mrApproveCmd "gitlab.com/gitlab-org/cli/commands/mr/approve"
mrApproversCmd "gitlab.com/gitlab-org/cli/commands/mr/approvers"
mrAwardEmojiCmd "gitlab.com/gitlab-org/cli/commands/mr/award-emoji"
mrCheckoutCmd "gitlab.com/gitlab-org/cli/commands/mr/checkout"
mrCloseCmd "gitlab.com/gitlab-org/cli/commands/mr/close"
mrCreateCmd "gitlab.com/gitlab-org/cli/commands/mr/create"
@ -50,6 +51,7 @@ func NewCmdMR(f *cmdutils.Factory) *cobra.Command {
mrCmd.AddCommand(mrApproveCmd.NewCmdApprove(f))
mrCmd.AddCommand(mrApproversCmd.NewCmdApprovers(f))
mrCmd.AddCommand(mrAwardEmojiCmd.NewCmdAwardEmoji(f))
mrCmd.AddCommand(mrCheckoutCmd.NewCmdCheckout(f))
mrCmd.AddCommand(mrCloseCmd.NewCmdClose(f))
mrCmd.AddCommand(mrCreateCmd.NewCmdCreate(f, nil))

View File

@ -52,6 +52,9 @@ func NewCmdRoot(f *cmdutils.Factory, version, buildDate string) *cobra.Command {
GITLAB_HOST or GL_HOST: Specify the URL of the GitLab server if self-managed.
(Example: https://gitlab.example.com) Defaults to https://gitlab.com.
GITLAB_CLIENT_ID: Provide custom 'client_id' generated by GitLab OAuth 2.0 application.
Defaults to the 'client-id' for GitLab.com.
REMOTE_ALIAS or GIT_REMOTE_URL_VAR: A 'git remote' variable or alias that contains
the GitLab URL. Can be set in the config with 'glab config set remote_alias origin'.

4
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/MakeNowJust/heredoc v1.0.0
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/avast/retry-go v3.0.0+incompatible
github.com/avast/retry-go/v4 v4.6.0
github.com/briandowns/spinner v1.23.0
github.com/charmbracelet/glamour v0.6.0
github.com/dustin/go-humanize v1.0.1
@ -32,7 +32,7 @@ require (
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.3
github.com/stretchr/testify v1.9.0
github.com/tidwall/pretty v1.2.1
github.com/xanzy/go-gitlab v0.103.0
github.com/zalando/go-keyring v0.2.3

10
go.sum
View File

@ -50,8 +50,8 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
@ -317,8 +317,9 @@ github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -327,8 +328,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=

View File

@ -6,6 +6,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"gitlab.com/gitlab-org/cli/test"
"gopkg.in/yaml.v3"
)
@ -16,13 +17,6 @@ func eq(t *testing.T, got interface{}, expected interface{}) {
}
}
func clearEnvironmentVariables(t *testing.T) {
// prevent using environment variables for test
t.Setenv("GITLAB_TOKEN", "")
t.Setenv("VISUAL", "")
t.Setenv("EDITOR", "")
}
func Test_parseConfig(t *testing.T) {
defer StubConfig(`---
hosts:
@ -31,7 +25,7 @@ hosts:
token: OTOKEN
aliases:
`, "")()
clearEnvironmentVariables(t)
test.ClearEnvironmentVariables(t)
config, err := ParseConfig("config.yml")
eq(t, err, nil)
@ -53,7 +47,7 @@ hosts:
username: monalisa
token: OTOKEN
`, "")()
clearEnvironmentVariables(t)
test.ClearEnvironmentVariables(t)
config, err := ParseConfig("config.yml")
eq(t, err, nil)
@ -73,7 +67,7 @@ hosts:
token: OTOKEN
`, `
`)()
clearEnvironmentVariables(t)
test.ClearEnvironmentVariables(t)
config, err := ParseConfig("config.yml")
eq(t, err, nil)
@ -86,6 +80,8 @@ hosts:
}
func Test_parseConfig_Local(t *testing.T) {
test.ClearEnvironmentVariables(t)
defer StubConfig(`---
git_protocol: ssh
editor: vim
@ -105,6 +101,8 @@ local:
}
func Test_Get_configReadSequence(t *testing.T) {
test.ClearEnvironmentVariables(t)
defer StubConfig(`---
git_protocol: ssh
editor: vim

View File

@ -1,6 +1,8 @@
package config
import "strings"
import (
"strings"
)
// ConfigKeyEquivalence returns the equivalent key that's actually used in the config file
func ConfigKeyEquivalence(key string) string {
@ -19,6 +21,8 @@ func ConfigKeyEquivalence(key string) string {
return "remote_alias"
case "editor", "visual", "glab_editor":
return "editor"
case "client_id":
return "client_id"
default:
return key
}
@ -41,6 +45,8 @@ func EnvKeyEquivalence(key string) []string {
return []string{"GLAB_EDITOR", "VISUAL", "EDITOR"}
case "remote_alias":
return []string{"GIT_REMOTE_URL_VAR", "GIT_REMOTE_ALIAS", "REMOTE_ALIAS", "REMOTE_NICKNAME", "GIT_REMOTE_NICKNAME"}
case "client_id":
return []string{"GITLAB_CLIENT_ID"}
default:
return []string{strings.ToUpper(key)}
}

View File

@ -2,6 +2,7 @@ package config
import (
"bytes"
"os"
"path/filepath"
"testing"
@ -93,7 +94,7 @@ func Test_defaultConfig(t *testing.T) {
editor, err := cfg.Get("", "editor")
assert.Nil(t, err)
assert.Equal(t, "", editor)
assert.Equal(t, os.Getenv("EDITOR"), editor)
aliases, err := cfg.Aliases()
assert.Nil(t, err)

View File

@ -9,6 +9,7 @@ import (
const (
defaultHostname = "gitlab.com"
defaultProtocol = "https"
defaultClientId = "41d48f9422ebd655dd9cf2947d6979681dfaddc6d0c56f7628f6ada59559af1e"
)
var (
@ -26,6 +27,10 @@ func DefaultProtocol() string {
return defaultProtocol
}
func DefaultClientID() string {
return defaultClientId
}
// OverridableDefault is like Default, except it is overridable by the GITLAB_HOST environment variable
func OverridableDefault() string {
if hostnameOverride != "" {

View File

@ -10,7 +10,7 @@ import (
)
func Test_HelperFunctions(t *testing.T) {
// Base ios object that is modifiede as required
// Base ios object that is modified as required
ios := &IOStreams{
In: os.Stdin,
StdOut: NewColorable(os.Stdout),

View File

@ -11,6 +11,7 @@ import (
"time"
"gitlab.com/gitlab-org/cli/internal/config"
"gitlab.com/gitlab-org/cli/pkg/glinstance"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"gitlab.com/gitlab-org/cli/pkg/utils"
)
@ -20,11 +21,29 @@ const (
scopes = "openid+profile+read_user+write_repository+api"
)
const clientID = "41d48f9422ebd655dd9cf2947d6979681dfaddc6d0c56f7628f6ada59559af1e"
func oAuthClientID(cfg config.Config, hostname string) (string, error) {
if glinstance.IsSelfHosted(hostname) {
clientID, err := cfg.Get(hostname, "client_id")
if err != nil {
return "", err
}
if clientID == "" {
return "", fmt.Errorf("set 'client_id' first with `glab config set client_id <client_id> -g -h %s`", hostname)
}
return clientID, nil
}
return glinstance.DefaultClientID(), nil
}
func StartFlow(cfg config.Config, io *iostreams.IOStreams, hostname string) (string, error) {
authURL := fmt.Sprintf("https://%s/oauth/authorize", hostname)
clientID, err := oAuthClientID(cfg, hostname)
if err != nil {
return "", err
}
state := randomString()
codeVerifier := randomString()
codeChallenge := generateCodeChallenge(codeVerifier)
@ -32,7 +51,7 @@ func StartFlow(cfg config.Config, io *iostreams.IOStreams, hostname string) (str
"%s?client_id=%s&redirect_uri=%s&response_type=code&state=%s&scope=%s&code_challenge=%s&code_challenge_method=S256",
authURL, clientID, redirectURI, state, scopes, codeChallenge)
tokenCh := handleAuthRedirect(io, codeVerifier, hostname, "https")
tokenCh := handleAuthRedirect(io, codeVerifier, hostname, "https", clientID)
defer close(tokenCh)
browser, _ := cfg.Get(hostname, "browser")
@ -43,7 +62,7 @@ func StartFlow(cfg config.Config, io *iostreams.IOStreams, hostname string) (str
}
token := <-tokenCh
err := token.SetConfig(hostname, cfg)
err = token.SetConfig(hostname, cfg)
if err != nil {
return "", err
}
@ -51,14 +70,14 @@ func StartFlow(cfg config.Config, io *iostreams.IOStreams, hostname string) (str
return token.AccessToken, nil
}
func handleAuthRedirect(io *iostreams.IOStreams, codeVerifier, hostname, protocol string) chan *AuthToken {
func handleAuthRedirect(io *iostreams.IOStreams, codeVerifier, hostname, protocol, clientID string) chan *AuthToken {
tokenCh := make(chan *AuthToken)
server := &http.Server{Addr: ":7171"}
http.HandleFunc("/auth/redirect", func(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := requestToken(hostname, protocol, code, codeVerifier)
token, err := requestToken(hostname, protocol, clientID, code, codeVerifier)
if err != nil {
fmt.Fprintf(io.StdErr, "Error occured requesting access token %s", err)
tokenCh <- nil
@ -81,7 +100,7 @@ func handleAuthRedirect(io *iostreams.IOStreams, codeVerifier, hostname, protoco
return tokenCh
}
func requestToken(hostname, protocol, code, codeVerifier string) (*AuthToken, error) {
func requestToken(hostname, protocol, clientID, code, codeVerifier string) (*AuthToken, error) {
tokenURL := fmt.Sprintf("%s://%s/oauth/token", protocol, hostname)
form := url.Values{
@ -126,6 +145,11 @@ func RefreshToken(hostname string, cfg config.Config, protocol string) error {
return nil
}
clientID, err := oAuthClientID(cfg, hostname)
if err != nil {
return err
}
form := url.Values{
"client_id": []string{clientID},
"grant_type": []string{"refresh_token"},

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/cli/pkg/glinstance"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
)
@ -32,11 +33,12 @@ func TestHandleAuthRedirect(t *testing.T) {
"token": "access_token",
"oauth2_code_verifier": "123",
"oauth2_expiry_date": "13 Mar 23 15:47 GMT",
"client_id": "321",
}
ios, _, _, _ := iostreams.Test()
tokenCh := handleAuthRedirect(ios, "123", hostname, "http")
tokenCh := handleAuthRedirect(ios, "123", hostname, "http", "abc")
defer close(tokenCh)
time.Sleep(1 * time.Second)
@ -69,6 +71,7 @@ func TestRefreshToken(t *testing.T) {
"token": "access_token",
"oauth2_code_verifier": "123",
"oauth2_expiry_date": "13 Mar 23 15:47 GMT",
"client_id": "321",
}
err := RefreshToken(hostname, cfg, "http")
@ -87,3 +90,51 @@ func TestRefreshToken(t *testing.T) {
_, err = time.Parse(time.RFC822, expiryDateString)
require.Nil(t, err)
}
func TestClientID(t *testing.T) {
testCasesTable := []struct {
name string
hostname string
configClientID string
expectedClientID string
}{
{
name: "managed",
hostname: glinstance.Default(),
configClientID: "",
expectedClientID: glinstance.DefaultClientID(),
},
{
name: "self-managed-complete",
hostname: "salsa.debian.org",
configClientID: "321",
expectedClientID: "321",
},
}
for _, testCase := range testCasesTable {
t.Run(testCase.name, func(t *testing.T) {
cfg := stubConfig{
hosts: map[string]map[string]string{
testCase.hostname: {
"client_id": testCase.configClientID,
},
},
}
clientID, err := oAuthClientID(cfg, testCase.hostname)
assert.NoError(t, err)
assert.Equal(t, testCase.expectedClientID, clientID)
})
}
t.Run("invalid self-managed config", func(t *testing.T) {
cfg := stubConfig{
hosts: map[string]map[string]string{
"salsa.debian.org": {},
},
}
clientID, err := oAuthClientID(cfg, "salsa.debian.org")
assert.Error(t, err)
assert.Empty(t, clientID)
})
}

View File

@ -106,6 +106,15 @@ func ExpectLines(t T, output string, lines ...string) {
}
}
func ClearEnvironmentVariables(t *testing.T) {
// prevent using environment variables for test
t.Setenv("GITLAB_TOKEN", "")
t.Setenv("VISUAL", "")
t.Setenv("EDITOR", "")
t.Setenv("GITLAB_ACCESS_TOKEN", "")
t.Setenv("OAUTH_TOKEN", "")
}
func GetHostOrSkip(t testing.TB) string {
t.Helper()
glTestHost := os.Getenv("GITLAB_TEST_HOST")