github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/auth/shared/git_credential.go (about) 1 package shared 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "path/filepath" 9 "strings" 10 11 "github.com/MakeNowJust/heredoc" 12 "github.com/ungtb10d/cli/v2/git" 13 "github.com/ungtb10d/cli/v2/internal/ghinstance" 14 "github.com/google/shlex" 15 ) 16 17 type GitCredentialFlow struct { 18 Executable string 19 Prompter Prompt 20 GitClient *git.Client 21 22 shouldSetup bool 23 helper string 24 scopes []string 25 } 26 27 func (flow *GitCredentialFlow) Prompt(hostname string) error { 28 var gitErr error 29 flow.helper, gitErr = gitCredentialHelper(flow.GitClient, hostname) 30 if isOurCredentialHelper(flow.helper) { 31 flow.scopes = append(flow.scopes, "workflow") 32 return nil 33 } 34 35 result, err := flow.Prompter.Confirm("Authenticate Git with your GitHub credentials?", true) 36 if err != nil { 37 return err 38 } 39 flow.shouldSetup = result 40 41 if flow.shouldSetup { 42 if isGitMissing(gitErr) { 43 return gitErr 44 } 45 flow.scopes = append(flow.scopes, "workflow") 46 } 47 48 return nil 49 } 50 51 func (flow *GitCredentialFlow) Scopes() []string { 52 return flow.scopes 53 } 54 55 func (flow *GitCredentialFlow) ShouldSetup() bool { 56 return flow.shouldSetup 57 } 58 59 func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error { 60 return flow.gitCredentialSetup(hostname, username, authToken) 61 } 62 63 func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error { 64 gitClient := flow.GitClient 65 ctx := context.Background() 66 67 if flow.helper == "" { 68 credHelperKeys := []string{ 69 gitCredentialHelperKey(hostname), 70 } 71 72 gistHost := strings.TrimSuffix(ghinstance.GistHost(hostname), "/") 73 if strings.HasPrefix(gistHost, "gist.") { 74 credHelperKeys = append(credHelperKeys, gitCredentialHelperKey(gistHost)) 75 } 76 77 var configErr error 78 79 for _, credHelperKey := range credHelperKeys { 80 if configErr != nil { 81 break 82 } 83 // first use a blank value to indicate to git we want to sever the chain of credential helpers 84 preConfigureCmd, err := gitClient.Command(ctx, "config", "--global", "--replace-all", credHelperKey, "") 85 if err != nil { 86 configErr = err 87 break 88 } 89 if _, err = preConfigureCmd.Output(); err != nil { 90 configErr = err 91 break 92 } 93 94 // second configure the actual helper for this host 95 configureCmd, err := gitClient.Command(ctx, 96 "config", "--global", "--add", 97 credHelperKey, 98 fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)), 99 ) 100 if err != nil { 101 configErr = err 102 } else { 103 _, configErr = configureCmd.Output() 104 } 105 } 106 107 return configErr 108 } 109 110 // clear previous cached credentials 111 rejectCmd, err := gitClient.Command(ctx, "credential", "reject") 112 if err != nil { 113 return err 114 } 115 116 rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(` 117 protocol=https 118 host=%s 119 `, hostname)) 120 121 _, err = rejectCmd.Output() 122 if err != nil { 123 return err 124 } 125 126 approveCmd, err := gitClient.Command(ctx, "credential", "approve") 127 if err != nil { 128 return err 129 } 130 131 approveCmd.Stdin = bytes.NewBufferString(heredoc.Docf(` 132 protocol=https 133 host=%s 134 username=%s 135 password=%s 136 `, hostname, username, password)) 137 138 _, err = approveCmd.Output() 139 if err != nil { 140 return err 141 } 142 143 return nil 144 } 145 146 func gitCredentialHelperKey(hostname string) string { 147 host := strings.TrimSuffix(ghinstance.HostPrefix(hostname), "/") 148 return fmt.Sprintf("credential.%s.helper", host) 149 } 150 151 func gitCredentialHelper(gitClient *git.Client, hostname string) (helper string, err error) { 152 ctx := context.Background() 153 helper, err = gitClient.Config(ctx, gitCredentialHelperKey(hostname)) 154 if helper != "" { 155 return 156 } 157 helper, err = gitClient.Config(ctx, "credential.helper") 158 return 159 } 160 161 func isOurCredentialHelper(cmd string) bool { 162 if !strings.HasPrefix(cmd, "!") { 163 return false 164 } 165 166 args, err := shlex.Split(cmd[1:]) 167 if err != nil || len(args) == 0 { 168 return false 169 } 170 171 return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh" 172 } 173 174 func isGitMissing(err error) bool { 175 if err == nil { 176 return false 177 } 178 var errNotInstalled *git.NotInstalled 179 return errors.As(err, &errNotInstalled) 180 } 181 182 func shellQuote(s string) string { 183 if strings.ContainsAny(s, " $\\") { 184 return "'" + s + "'" 185 } 186 return s 187 }