diff --git a/cmd/glab/main.go b/cmd/glab/main.go index 9fd6dc63065078e2b4b0918a312b69ba7815e7f1..a78f0747742b0b8a461fffa11a714f3c12d3ba6e 100644 --- a/cmd/glab/main.go +++ b/cmd/glab/main.go @@ -58,8 +58,17 @@ func main() { debug = debugModeCfg == "true" || debugModeCfg == "1" } + // Resolve repository overrides. + // Overrides happen through the global `-R` / `--repo` command line flags + // or via GITLAB_REPO environment variable. + repository, err := cmdutils.ParseRepoOverrideEarly(os.Args) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(2) + } + // Initialize factory and iostreams - cmdFactory := cmdutils.NewFactory( + cmdFactory, err := cmdutils.NewFactory( iostreams.New( iostreams.WithStdin(os.Stdin, iostreams.IsTerminal(os.Stdin)), iostreams.WithStdout(iostreams.NewColorable(os.Stdout), iostreams.IsTerminal(os.Stdout)), @@ -90,9 +99,14 @@ func main() { }, ), true, + repository, cfg, api.BuildInfo{Version: version, Commit: commit, Platform: platform, Architecture: runtime.GOARCH}, ) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to create command factory: %s\n", err) + os.Exit(2) + } setupSurveyCore(cmdFactory.IO()) diff --git a/commands/cmdtest/helper.go b/commands/cmdtest/helper.go index dd7c93589164c56f790ecdaa3a8a0f098bd4c525..f04adadc283e5b2deae981c84dc0ed0b7b98a946 100644 --- a/commands/cmdtest/helper.go +++ b/commands/cmdtest/helper.go @@ -255,6 +255,15 @@ type CmdFunc func(cmdutils.Factory) *cobra.Command // FactoryOption is a function that configures a Factory type FactoryOption func(f *Factory) +// WithApiClientStub configures the Factory with a specific API client +func WithApiClient(client *api.Client) FactoryOption { + return func(f *Factory) { + f.ApiClientStub = func(repoHost string, cfg config.Config) (*api.Client, error) { + return client, nil + } + } +} + // WithGitLabClient configures the Factory with a specific GitLab client func WithGitLabClient(client *gitlab.Client) FactoryOption { return func(f *Factory) { @@ -330,6 +339,13 @@ func NewTestFactory(ios *iostreams.IOStreams, opts ...FactoryOption) *Factory { // Create a default factory f := &Factory{ IOStub: ios, + ApiClientStub: func(repoHost string, cfg config.Config) (*api.Client, error) { + a, err := TestClient(&http.Client{}, "", repoHost, false) + if err != nil { + return nil, err + } + return a, nil + }, HttpClientStub: func() (*gitlab.Client, error) { return &gitlab.Client{}, nil }, diff --git a/commands/cmdutils/early_repository_flag_parser.go b/commands/cmdutils/early_repository_flag_parser.go new file mode 100644 index 0000000000000000000000000000000000000000..9beabeb7df938ce255a9ba9059e5ca96e1fc2d65 --- /dev/null +++ b/commands/cmdutils/early_repository_flag_parser.go @@ -0,0 +1,50 @@ +package cmdutils + +import ( + "fmt" + "os" + "strings" +) + +// ParseRepoOverrideEarly extracts the repo override from command line arguments +// before full command parsing. This enables early repository resolution. +func ParseRepoOverrideEarly(args []string) (string, error) { + // Environment variable takes precedence when no flag is provided + repo, gotRepo := os.LookupEnv("GITLAB_REPO") + + // Parse command line arguments for -R or --repo +loop: + for i, arg := range args { + switch { + case arg == "-R" || arg == "--repo": + // Flag with separate value: -R value or --repo value + if i+1 < len(args) { + repo = args[i+1] + gotRepo = true + } + break loop + case strings.HasPrefix(arg, "--repo="): + // Flag with inline value: --repo=value + repo = strings.TrimPrefix(arg, "--repo=") + gotRepo = true + break loop + } + } + + if !gotRepo { + return "", nil + } + + // Validate repo override value + repo = strings.TrimSpace(repo) + if repo == "" { + return "", fmt.Errorf("the repo override flag is provided without a value") + } + + if strings.HasPrefix(repo, "-") { + return "", fmt.Errorf("the repo override flag is provided without a valid value %q. Could this be another flag?", repo) + } + + // Return environment variable if no flag found + return repo, nil +} diff --git a/commands/cmdutils/factory.go b/commands/cmdutils/factory.go index a4c61a3168b72d18ea91f3cbd30820b013d1daf7..05d1329614b6f923faeb2b99d8c8829a044694e2 100644 --- a/commands/cmdutils/factory.go +++ b/commands/cmdutils/factory.go @@ -3,7 +3,6 @@ package cmdutils import ( "fmt" "strings" - "sync" gitlab "gitlab.com/gitlab-org/api/client-go" "gitlab.com/gitlab-org/cli/api" @@ -18,7 +17,6 @@ import ( // Factory is a way to obtain core tools for the commands. // Safe for concurrent use. type Factory interface { - RepoOverride(repo string) ApiClient(repoHost string, cfg config.Config) (*api.Client, error) HttpClient() (*gitlab.Client, error) BaseRepo() (glrepo.Interface, error) @@ -33,29 +31,62 @@ type Factory interface { type DefaultFactory struct { io *iostreams.IOStreams config config.Config - resolveRepos bool buildInfo api.BuildInfo defaultHostname string defaultProtocol string - - mu sync.Mutex // protects the fields below - repoOverride string + baseRepo glrepo.Interface } -func NewFactory(io *iostreams.IOStreams, resolveRepos bool, cfg config.Config, buildInfo api.BuildInfo) *DefaultFactory { +func NewFactory(io *iostreams.IOStreams, resolveRepos bool, repository string, cfg config.Config, buildInfo api.BuildInfo) (*DefaultFactory, error) { f := &DefaultFactory{ io: io, config: cfg, - resolveRepos: resolveRepos, buildInfo: buildInfo, defaultHostname: glinstance.DefaultHostname, defaultProtocol: glinstance.DefaultProtocol, } - baseRepo, err := f.BaseRepo() - if err == nil { - f.defaultHostname = baseRepo.RepoHost() + // resolve repository + var baseRepo glrepo.Interface + if repository != "" { + r, err := glrepo.FromFullName(repository, f.defaultHostname) + if err != nil { + return nil, fmt.Errorf("failed to get full name from provided repository %q for default hostname %q", repository, f.defaultHostname) + } + baseRepo = r + } else { + remotes, err := f.Remotes() + if err != nil { + return nil, fmt.Errorf("failed to get remotes: %w", err) + } + if resolveRepos { + baseRepo = remotes[0] + } else { + // TODO: is the code below even necessary? Can repo.RepoHost() be an empty string?! + repoHost := f.defaultHostname + if remotes[0].RepoHost() != "" { + repoHost = remotes[0].RepoHost() + } + ac, err := api.NewClientWithCfg(f.defaultProtocol, repoHost, cfg, false, f.buildInfo.UserAgent()) + if err != nil { + return nil, fmt.Errorf("failed to create new API client to reesolve repository: %w", err) + } + httpClient := ac.Lab() + repoContext, err := glrepo.ResolveRemotesToRepos(remotes, httpClient, "", f.defaultHostname) + if err != nil { + return nil, fmt.Errorf("failed to resolve remotes to repositories: %w", err) + } + r, err := repoContext.BaseRepo(f.io.PromptEnabled()) + if err != nil { + return nil, fmt.Errorf("failed to get base repository from resolved remote repositories: %w", err) + } + baseRepo = r + } } + + f.baseRepo = baseRepo + f.defaultHostname = baseRepo.RepoHost() + // Fetch the custom host config from env vars, then local config.yml, then global config,yml. customGLHost, _ := cfg.Get("", "host") if customGLHost != "" { @@ -67,19 +98,13 @@ func NewFactory(io *iostreams.IOStreams, resolveRepos bool, cfg config.Config, b f.defaultHostname = customGLHost } - return f + return f, nil } func (f *DefaultFactory) DefaultHostname() string { return f.defaultHostname } -func (f *DefaultFactory) RepoOverride(repo string) { - f.mu.Lock() - defer f.mu.Unlock() - f.repoOverride = repo -} - func (f *DefaultFactory) ApiClient(repoHost string, cfg config.Config) (*api.Client, error) { if repoHost == "" { repoHost = f.defaultHostname @@ -94,33 +119,7 @@ func (f *DefaultFactory) ApiClient(repoHost string, cfg config.Config) (*api.Cli func (f *DefaultFactory) HttpClient() (*gitlab.Client, error) { cfg := f.Config() - f.mu.Lock() - override := f.repoOverride - f.mu.Unlock() - var repo glrepo.Interface - if override != "" { - var err error - repo, err = glrepo.FromFullName(override, f.defaultHostname) - if err != nil { - return nil, err // return the error if repo was overridden. - } - } else { - remotes, err := f.Remotes() - if err != nil { - // use default hostname if remote resolver fails - repo = glrepo.NewWithHost("", "", f.defaultHostname) - } else { - repo = remotes[0] - } - } - - // TODO: is the code below even necessary? Can repo.RepoHost() be an empty string?! - repoHost := f.defaultHostname - if repo.RepoHost() != "" { - repoHost = repo.RepoHost() - } - - c, err := api.NewClientWithCfg(f.defaultProtocol, repoHost, cfg, false, f.buildInfo.UserAgent()) + c, err := api.NewClientWithCfg(f.defaultProtocol, f.baseRepo.RepoHost(), cfg, false, f.buildInfo.UserAgent()) if err != nil { return nil, err } @@ -129,39 +128,7 @@ func (f *DefaultFactory) HttpClient() (*gitlab.Client, error) { } func (f *DefaultFactory) BaseRepo() (glrepo.Interface, error) { - f.mu.Lock() - override := f.repoOverride - f.mu.Unlock() - if override != "" { - return glrepo.FromFullName(override, f.defaultHostname) - } - remotes, err := f.Remotes() - if err != nil { - return nil, err - } - if !f.resolveRepos { - return remotes[0], nil - } - cfg := f.Config() - // TODO: is the code below even necessary? Can repo.RepoHost() be an empty string?! - repoHost := f.defaultHostname - if remotes[0].RepoHost() != "" { - repoHost = remotes[0].RepoHost() - } - ac, err := api.NewClientWithCfg(f.defaultProtocol, repoHost, cfg, false, f.buildInfo.UserAgent()) - if err != nil { - return nil, err - } - httpClient := ac.Lab() - repoContext, err := glrepo.ResolveRemotesToRepos(remotes, httpClient, "", f.defaultHostname) - if err != nil { - return nil, err - } - baseRepo, err := repoContext.BaseRepo(f.io.PromptEnabled()) - if err != nil { - return nil, err - } - return baseRepo, nil + return f.baseRepo, nil } func (f *DefaultFactory) Remotes() (glrepo.Remotes, error) { diff --git a/commands/cmdutils/repo_override.go b/commands/cmdutils/repo_override.go index 3c485eb26caf69db7b8bebd4d8f779984abd8c61..f7580d0779f3310d07ed3880fa29bff5699bbb5a 100644 --- a/commands/cmdutils/repo_override.go +++ b/commands/cmdutils/repo_override.go @@ -1,8 +1,6 @@ package cmdutils import ( - "os" - "github.com/spf13/cobra" ) @@ -16,45 +14,10 @@ func EnableRepoOverride(cmd *cobra.Command, f Factory) { if flag := cmd.PersistentFlags().Lookup("repo"); flag != nil { flag.Hidden = false } - - cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { - repoOverride, err := cmd.Flags().GetString("repo") - if err != nil { - return err - } - if repoFromEnv := os.Getenv("GITLAB_REPO"); repoOverride == "" && repoFromEnv != "" { - repoOverride = repoFromEnv - } - if repoOverride != "" { - f.RepoOverride(repoOverride) - } - return nil - } } // AddGlobalRepoOverride adds the -R flag globally but keeps it hidden func AddGlobalRepoOverride(cmd *cobra.Command, f Factory) { cmd.PersistentFlags().StringP("repo", "R", "", "Select another repository. Can use either `OWNER/REPO` or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL.") _ = cmd.PersistentFlags().MarkHidden("repo") - - originalPreRunE := cmd.PersistentPreRunE - cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { - if originalPreRunE != nil { - if err := originalPreRunE(cmd, args); err != nil { - return err - } - } - - repoOverride, err := cmd.Flags().GetString("repo") - if err != nil { - return err - } - if repoFromEnv := os.Getenv("GITLAB_REPO"); repoOverride == "" && repoFromEnv != "" { - repoOverride = repoFromEnv - } - if repoOverride != "" { - f.RepoOverride(repoOverride) - } - return nil - } } diff --git a/commands/duo/ask/ask_integration_test.go b/commands/duo/ask/ask_integration_test.go index de8b75853d58421cd069bb2a88bd5d3bfeef520a..c578e44015dc564985298005637bc07bd1d008bc 100644 --- a/commands/duo/ask/ask_integration_test.go +++ b/commands/duo/ask/ask_integration_test.go @@ -24,7 +24,8 @@ func TestAskGit_Integration(t *testing.T) { cfg, err := config.Init() require.NoError(t, err) io, _, stdout, stderr := cmdtest.TestIOStreams() - f := cmdutils.NewFactory(io, false, cfg, api.BuildInfo{}) + f, err := cmdutils.NewFactory(io, false, "", cfg, api.BuildInfo{}) + require.NoError(t, err) cmd := NewCmdAsk(f) cli := "--git how to create a branch" diff --git a/commands/issue/create/issue_create_integration_test.go b/commands/issue/create/issue_create_integration_test.go index 0c5356f0a08c8d2b15539c969b1bf99a9b96b229..8d613796081338d8fb3fa2adc417f07d00aed2e9 100644 --- a/commands/issue/create/issue_create_integration_test.go +++ b/commands/issue/create/issue_create_integration_test.go @@ -64,8 +64,8 @@ func Test_IssueCreate_Integration(t *testing.T) { cfg, err := config.Init() require.NoError(t, err) ios, _, stdout, stderr := cmdtest.TestIOStreams(cmdtest.WithTestIOStreamsAsTTY(true)) - f := cmdutils.NewFactory(ios, false, cfg, api.BuildInfo{}) - f.RepoOverride(glTestHost + "/cli-automated-testing/test") + f, err := cmdutils.NewFactory(ios, false, glTestHost + "/cli-automated-testing/test", cfg, api.BuildInfo{}) + require.NoError(t, err) cmd := NewCmdCreate(f) cmd.Flags().StringP("repo", "R", "", "") @@ -139,8 +139,8 @@ func Test_IssueCreate_With_Recover_Integration(t *testing.T) { cfg, err := config.Init() require.NoError(t, err) ios, _, stdout, stderr := cmdtest.TestIOStreams(cmdtest.WithTestIOStreamsAsTTY(true)) - f := cmdutils.NewFactory(ios, false, cfg, api.BuildInfo{}) - f.RepoOverride(glTestHost + "/cli-automated-testing/test") + f, err := cmdutils.NewFactory(ios, false, glTestHost+"/cli-automated-testing/test", cfg, api.BuildInfo{}) + require.NoError(t, err) oldCreateRun := createRun diff --git a/commands/issue/update/issue_update_integration_test.go b/commands/issue/update/issue_update_integration_test.go index 2509f77fecd0ea5062f6d0b4b4035058155ac398..9df5a198345ccb41c4e9bd78e093b66830b6c9c8 100644 --- a/commands/issue/update/issue_update_integration_test.go +++ b/commands/issue/update/issue_update_integration_test.go @@ -96,8 +96,8 @@ func TestNewCmdUpdate_Integration(t *testing.T) { cfg, err := config.Init() require.NoError(t, err) ios, _, stdout, stderr := cmdtest.TestIOStreams(cmdtest.WithTestIOStreamsAsTTY(true)) - f := cmdutils.NewFactory(ios, false, cfg, api.BuildInfo{}) - f.RepoOverride(glTestHost + "/cli-automated-testing/test") + f, err := cmdutils.NewFactory(ios, false, glTestHost+"/cli-automated-testing/test", cfg, api.BuildInfo{}) + require.NoError(t, err) cmd := NewCmdUpdate(f) cmd.Flags().StringP("repo", "R", "", "") diff --git a/commands/project/archive/repo_archive.go b/commands/project/archive/repo_archive.go index b266ac86b167a3cacc6a5203ec659055e06b48a3..749fd47ef0eaddb39cf59944a711c7ea695337a5 100644 --- a/commands/project/archive/repo_archive.go +++ b/commands/project/archive/repo_archive.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" gitlab "gitlab.com/gitlab-org/api/client-go" "gitlab.com/gitlab-org/cli/commands/cmdutils" + "gitlab.com/gitlab-org/cli/internal/glrepo" ) func NewCmdArchive(f cmdutils.Factory) *cobra.Command { @@ -44,21 +45,29 @@ func NewCmdArchive(f cmdutils.Factory) *cobra.Command { var name string var err error + var gitlabClient *gitlab.Client + var repo glrepo.Interface if len(args) != 0 { - f.RepoOverride(args[0]) + // repository is coming from command args, not -R + apiClient, err := f.ApiClient(args[0], f.Config()) + if err != nil { + return err + } if len(args) > 1 { name = args[1] } - } - - apiClient, err := f.HttpClient() - if err != nil { - return err - } - repo, err := f.BaseRepo() - if err != nil { - return err + gitlabClient = apiClient.Lab() + repo, err = glrepo.FromFullName(args[0], f.DefaultHostname()) + if err != nil { + return err + } + } else { + gitlabClient, err = f.HttpClient() + if err != nil { + return err + } + repo, _ = f.BaseRepo() } format, _ := cmd.Flags().GetString("format") @@ -80,7 +89,7 @@ func NewCmdArchive(f cmdutils.Factory) *cobra.Command { archiveName = name + "." + ext } - bt, _, err := apiClient.Repositories.Archive(repo.FullName(), l) + bt, _, err := gitlabClient.Repositories.Archive(repo.FullName(), l) if err != nil { return fmt.Errorf("failed to get archive: %v", err) } diff --git a/commands/project/transfer/project_transfer.go b/commands/project/transfer/project_transfer.go index 3fa283ac4bee5a404c3f86d3b9bdea33ed90e610..4eab295d1d396963ac77c66e684829593613182e 100644 --- a/commands/project/transfer/project_transfer.go +++ b/commands/project/transfer/project_transfer.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" gitlab "gitlab.com/gitlab-org/api/client-go" "gitlab.com/gitlab-org/cli/commands/cmdutils" + "gitlab.com/gitlab-org/cli/internal/glrepo" ) func NewCmdTransfer(f cmdutils.Factory) *cobra.Command { @@ -20,18 +21,26 @@ func NewCmdTransfer(f cmdutils.Factory) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { var err error + var gitlabClient *gitlab.Client + var repo glrepo.Interface if len(args) != 0 { - f.RepoOverride(args[0]) - } - - apiClient, err := f.HttpClient() - if err != nil { - return err - } + // repository is coming from command args, not -R + apiClient, err := f.ApiClient(args[0], f.Config()) + if err != nil { + return err + } - repo, err := f.BaseRepo() - if err != nil { - return err + gitlabClient = apiClient.Lab() + repo, err = glrepo.FromFullName(args[0], f.DefaultHostname()) + if err != nil { + return err + } + } else { + gitlabClient, err = f.HttpClient() + if err != nil { + return err + } + repo, _ = f.BaseRepo() } dontPromptForConfirmation, err := cmd.Flags().GetBool("yes") @@ -69,7 +78,7 @@ func NewCmdTransfer(f cmdutils.Factory) *cobra.Command { opt := &gitlab.TransferProjectOptions{} opt.Namespace = targetNamespace - project, _, err := apiClient.Projects.TransferProject(repo.FullName(), opt) + project, _, err := gitlabClient.Projects.TransferProject(repo.FullName(), opt) if err != nil { return err } diff --git a/commands/root_test.go b/commands/root_test.go index 0798971b5ec4277620ab0e0aa02c31ee9aae9147..a2c2ad4e3a9d9a360f71b17855960655b790fbb2 100644 --- a/commands/root_test.go +++ b/commands/root_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/cli/api" "gitlab.com/gitlab-org/cli/commands/cmdtest" "gitlab.com/gitlab-org/cli/commands/cmdutils" @@ -30,7 +31,9 @@ func TestRootVersion(t *testing.T) { old := os.Stdout // keep backup of the real stdout r, w, _ := os.Pipe() os.Stdout = w - rootCmd := NewCmdRoot(cmdutils.NewFactory(setupIOStreams(), false, config.NewBlankConfig(), api.BuildInfo{Version: "v1.0.0", Commit: "abcdefgh"})) + f, err := cmdutils.NewFactory(setupIOStreams(), false, "", config.NewBlankConfig(), api.BuildInfo{Version: "v1.0.0", Commit: "abcdefgh"}) + require.NoError(t, err) + rootCmd := NewCmdRoot(f) assert.Nil(t, rootCmd.Flag("version").Value.Set("true")) assert.Nil(t, rootCmd.Execute()) @@ -43,7 +46,9 @@ func TestRootNoArg(t *testing.T) { old := os.Stdout // keep backup of the real stdout r, w, _ := os.Pipe() os.Stdout = w - rootCmd := NewCmdRoot(cmdutils.NewFactory(setupIOStreams(), false, config.NewBlankConfig(), api.BuildInfo{Version: "v1.0.0", Commit: "abcdefgh"})) + f, err := cmdutils.NewFactory(setupIOStreams(), false, "", config.NewBlankConfig(), api.BuildInfo{Version: "v1.0.0", Commit: "abcdefgh"}) + require.NoError(t, err) + rootCmd := NewCmdRoot(f) assert.Nil(t, rootCmd.Execute()) out := test.ReturnBuffer(old, r, w) diff --git a/commands/update/check_update.go b/commands/update/check_update.go index 5c633e77ec8dd715ab1c9509d3013c52a3850a24..0313832193ebb3dd34cf35f40575b7b487fc0a1a 100644 --- a/commands/update/check_update.go +++ b/commands/update/check_update.go @@ -7,6 +7,7 @@ import ( "time" "gitlab.com/gitlab-org/cli/internal/config" + "gitlab.com/gitlab-org/cli/internal/glrepo" "gitlab.com/gitlab-org/cli/pkg/utils" "gitlab.com/gitlab-org/cli/commands/cmdutils" @@ -55,21 +56,21 @@ func CheckUpdate(f cmdutils.Factory, silentSuccess bool) error { } // We set the project to the `glab` project to check for `glab` updates - f.RepoOverride(defaultProjectURL) - repo, err := f.BaseRepo() + repo, err := glrepo.FromFullName(defaultProjectURL, f.DefaultHostname()) if err != nil { return err } - apiClient, err := f.HttpClient() + apiClient, err := f.ApiClient(repo.RepoHost(), f.Config()) if err != nil { return err } + gitlabClient := apiClient.Lab() // Since the `gitlab.com/gitlab-org/cli` is public, we remove the token // for this single request. When users have a `GITLAB_TOKEN` set with a // token for GitLab Self-Managed or GitLab Dedicated, we shouldn't use it // to authenticate to gitlab.com. - releases, _, err := apiClient.Releases.ListReleases( + releases, _, err := gitlabClient.Releases.ListReleases( repo.FullName(), &gitlab.ListReleasesOptions{ListOptions: gitlab.ListOptions{Page: 1, PerPage: 1}}, gitlab.WithToken(gitlab.PrivateToken, "")) if err != nil { return fmt.Errorf("failed checking for glab updates: %s", err.Error()) diff --git a/commands/update/check_update_test.go b/commands/update/check_update_test.go index 351010c2deff5de5e1eb101eb38cec563ffbf109..df4c629cff42f85d0c6159a5f230807ac58a5917 100644 --- a/commands/update/check_update_test.go +++ b/commands/update/check_update_test.go @@ -26,7 +26,12 @@ func runCommand(rt http.RoundTripper, version string) (*test.CmdOut, error) { return nil, err } - factory := cmdtest.NewTestFactory(ios, cmdtest.WithGitLabClient(tc.Lab()), cmdtest.WithBuildInfo(api.BuildInfo{Version: version})) + factory := cmdtest.NewTestFactory( + ios, + cmdtest.WithApiClient(tc), + cmdtest.WithGitLabClient(tc.Lab()), + cmdtest.WithBuildInfo(api.BuildInfo{Version: version}), + ) _, _ = factory.HttpClient() cmd := NewCheckUpdateCmd(factory)