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
2 changes: 1 addition & 1 deletion cmd/azdo/azdo.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func mainRun() exitCode {

stderr := iostrms.ErrOut

zap.L().Sugar().Debugf("Processing error %v", err)
zap.L().Sugar().Debugf("Processing error %T, %v", err, err)

if errors.Is(err, cmdutil.ErrSilent) { //nolint:golint,gocritic
return exitError
Expand Down
3 changes: 2 additions & 1 deletion internal/cmd/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"
"github.com/tmeckel/azdo-cli/internal/cmd/auth/gitcredential"
"github.com/tmeckel/azdo-cli/internal/cmd/auth/login"
"github.com/tmeckel/azdo-cli/internal/cmd/auth/logout"
"github.com/tmeckel/azdo-cli/internal/cmd/auth/setupgit"
"github.com/tmeckel/azdo-cli/internal/cmd/auth/status"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
Expand All @@ -20,7 +21,7 @@ func NewCmdAuth(ctx util.CmdContext) *cobra.Command {

cmd.AddCommand(gitcredential.NewCmdGitCredential(ctx))
cmd.AddCommand(login.NewCmdLogin(ctx))
// cmd.AddCommand(logout.NewCmdLogout(ctx))
cmd.AddCommand(logout.NewCmdLogout(ctx))
cmd.AddCommand(status.NewCmdStatus(ctx))
cmd.AddCommand(setupgit.NewCmdSetupGit(ctx))

Expand Down
162 changes: 157 additions & 5 deletions internal/cmd/auth/logout/logout.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,167 @@
package logout

import (
"errors"
"fmt"
"strings"

"github.com/MakeNowJust/heredoc"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/config"
"github.com/tmeckel/azdo-cli/internal/git"
"go.uber.org/zap"
)

type logoutOptions struct {
organizationName string
}

func NewCmdLogout(ctx util.CmdContext) *cobra.Command {
return nil
opts := &logoutOptions{}

cmd := &cobra.Command{
Use: "logout",
Args: cobra.ExactArgs(0),
Short: "Log out of a Azure DevOps organization",
Long: heredoc.Doc(`Remove authentication for a Azure DevOps organization.

This command removes the authentication configuration for an organization either specified
interactively or via --organization.
`),
Example: heredoc.Doc(`
$ azdo auth logout
# => select what host to log out of via a prompt

$ azdo auth logout --hostname enterprise.internal
# => log out of specified host
`),
RunE: func(cmd *cobra.Command, args []string) error {
iostrms, err := ctx.IOStreams()
if err != nil {
return err
}

if opts.organizationName == "" && !iostrms.CanPrompt() {
return util.FlagErrorf("--organization required when not running interactively")
}

return logoutRun(ctx, opts)
},
}

cmd.Flags().StringVarP(&opts.organizationName, "organization", "o", "", "The Azure DevOps organization to log out of")

return cmd
}

// Logout must
// 1. Check if the organization set as default, if yes: clear default
// 2. Remove global credential helper (azdo auth setup-git)
// 3. Remove the organization from the config
func logoutRun(ctx util.CmdContext, opts *logoutOptions) (err error) {
logger := zap.L().Sugar()

cfg, err := ctx.Config()
if err != nil {
return util.FlagErrorf("error getting io configuration: %w", err)
}
iostrms, err := ctx.IOStreams()
if err != nil {
return
}
p, err := ctx.Prompter()
if err != nil {
return fmt.Errorf("error getting io propter: %w", err)
}

cs := iostrms.ColorScheme()
authCfg := cfg.Authentication()

organizations := authCfg.GetOrganizations()
organizationName := opts.organizationName

if len(organizations) == 0 {
fmt.Fprintf(
iostrms.ErrOut,
"You are %s logged into any Azure DevOps organizations.\n",
cs.Red("not"),
)

return util.ErrSilent
}

if organizationName == "" {
if len(organizations) == 1 {
organizationName = organizations[0]
} else {
selected, err := p.Select(
"What organization do you want to log out of?", "", organizations)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
organizationName = organizations[selected]
}
} else {
if !lo.Contains(organizations, opts.organizationName) {
fmt.Fprintf(
iostrms.ErrOut,
"You are %s logged in to the Azure DevOps organization %q.\n",
cs.Red("not"),
opts.organizationName,
)
return util.ErrSilent
}
}

// Logout must
// 1. Check if the organization set as default, if yes: clear default
defaultOrganization, err := authCfg.GetDefaultOrganization()
if err != nil {
return err
}
if defaultOrganization == organizationName {
result, err := p.Confirm(fmt.Sprintf("%q is the current default organization. Perform logout?", organizationName), false)
if err != nil {
return err
}
if !result {
return nil
}
err = authCfg.SetDefaultOrganization("")
if err != nil {
if !errors.Is(err, &config.KeyNotFoundError{}) {
return err
}
}
}
// 2. Remove global credential helper (azdo auth setup-git) if it exists
organizationURL, err := authCfg.GetURL(organizationName)
if err != nil {
return err
}

rctx, err := ctx.Context()
if err != nil {
return
}

gitClient, err := ctx.GitClient()
if err != nil {
return
}

credHelperKey := fmt.Sprintf("credential.%s", strings.TrimSuffix(organizationURL, "/"))
preConfigureCmd, err := gitClient.Command(rctx, "config", "--global", "--remove-section", credHelperKey)
if err != nil {
return err
}
if _, err = preConfigureCmd.Output(); err != nil {
logger.Debugf("failed to execute command. Error type %T; %+v", err, err)

var ge *git.Error
if !errors.As(err, &ge) || (ge.ExitCode != 5 && ge.ExitCode != 128) {
return err
}
}

// 3. Remove the organization from the config
return authCfg.Logout(organizationName)
}
5 changes: 1 addition & 4 deletions internal/config/auth_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,7 @@ func (c *authConfig) Logout(organizationName string) (err error) {
if err != nil {
return
}
err = keyring.Delete(keyringServiceName(organizationName), "")
if err != nil {
return
}
_ = keyring.Delete(keyringServiceName(organizationName), "")
return c.cfg.Write()
}

Expand Down
4 changes: 2 additions & 2 deletions internal/git/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ type Error struct {

func (ge *Error) Error() string {
if ge.Stderr == "" {
return fmt.Sprintf("failed to run git: %v", ge.err)
return fmt.Sprintf("failed to run git (exit code %d): %v", ge.ExitCode, ge.err)
}
return fmt.Sprintf("failed to run git: %s", ge.Stderr)
return fmt.Sprintf("failed to run git (exit code %d): %s", ge.ExitCode, ge.Stderr)
}

func (ge *Error) Unwrap() error {
Expand Down
21 changes: 9 additions & 12 deletions internal/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/tmeckel/azdo-cli/internal/util"
"go.uber.org/zap"
)

// Runnable is typically an exec.Cmd or its stub in tests
Expand All @@ -31,9 +29,9 @@ type cmdWithStderr struct {
}

func (c cmdWithStderr) Output() ([]byte, error) {
if isVerbose, _ := util.IsDebugEnabled(); isVerbose {
_ = printArgs(os.Stderr, c.Cmd.Args)
}
logger := zap.L().Sugar()
logger.Debugf("Executing command with output: %s", formatArgs(c.Cmd.Args))

out, err := c.Cmd.Output()
if c.Cmd.Stderr != nil || err == nil {
return out, err
Expand All @@ -50,9 +48,9 @@ func (c cmdWithStderr) Output() ([]byte, error) {
}

func (c cmdWithStderr) Run() error {
if isVerbose, _ := util.IsDebugEnabled(); isVerbose {
_ = printArgs(os.Stderr, c.Cmd.Args)
}
logger := zap.L().Sugar()
logger.Debugf("Executing command ignoring output: %q", formatArgs(c.Cmd.Args))

if c.Cmd.Stderr != nil {
return c.Cmd.Run()
}
Expand Down Expand Up @@ -88,11 +86,10 @@ func (e CmdError) Unwrap() error {
return e.Err
}

func printArgs(w io.Writer, args []string) error {
func formatArgs(args []string) string {
if len(args) > 0 {
// print commands, but omit the full path to an executable
args = append([]string{filepath.Base(args[0])}, args[1:]...)
}
_, err := fmt.Fprintf(w, "%v\n", args)
return err
return fmt.Sprintf("%v", args)
}