fix(rig): suggest SSH URL when HTTPS auth fails (#577)
When `gt rig add` fails due to GitHub password auth being disabled, provide a helpful error message that: - Explains that GitHub no longer supports password authentication - Suggests the equivalent SSH URL for GitHub/GitLab repos - Falls back to generic SSH suggestion for other hosts Also adds tests for the URL conversion function. Fixes #548 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,52 @@ var (
|
|||||||
ErrRigExists = errors.New("rig already exists")
|
ErrRigExists = errors.New("rig already exists")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// wrapCloneError wraps clone errors with helpful suggestions.
|
||||||
|
// Detects common auth failures and suggests SSH as an alternative.
|
||||||
|
func wrapCloneError(err error, gitURL string) error {
|
||||||
|
errStr := err.Error()
|
||||||
|
|
||||||
|
// Check for GitHub password auth failure
|
||||||
|
if strings.Contains(errStr, "Password authentication is not supported") ||
|
||||||
|
strings.Contains(errStr, "Authentication failed") {
|
||||||
|
// Check if they used HTTPS
|
||||||
|
if strings.HasPrefix(gitURL, "https://") {
|
||||||
|
// Try to suggest the SSH equivalent
|
||||||
|
sshURL := convertToSSH(gitURL)
|
||||||
|
if sshURL != "" {
|
||||||
|
return fmt.Errorf("creating bare repo: %w\n\nHint: GitHub no longer supports password authentication.\nTry using SSH instead:\n gt rig add <name> %s", err, sshURL)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("creating bare repo: %w\n\nHint: GitHub no longer supports password authentication.\nTry using an SSH URL (git@github.com:owner/repo.git) or a personal access token.", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("creating bare repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToSSH converts an HTTPS GitHub/GitLab URL to SSH format.
|
||||||
|
// Returns empty string if conversion is not possible.
|
||||||
|
func convertToSSH(httpsURL string) string {
|
||||||
|
// Handle GitHub: https://github.com/owner/repo.git -> git@github.com:owner/repo.git
|
||||||
|
if strings.HasPrefix(httpsURL, "https://github.com/") {
|
||||||
|
path := strings.TrimPrefix(httpsURL, "https://github.com/")
|
||||||
|
if !strings.HasSuffix(path, ".git") {
|
||||||
|
path += ".git"
|
||||||
|
}
|
||||||
|
return "git@github.com:" + path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle GitLab: https://gitlab.com/owner/repo.git -> git@gitlab.com:owner/repo.git
|
||||||
|
if strings.HasPrefix(httpsURL, "https://gitlab.com/") {
|
||||||
|
path := strings.TrimPrefix(httpsURL, "https://gitlab.com/")
|
||||||
|
if !strings.HasSuffix(path, ".git") {
|
||||||
|
path += ".git"
|
||||||
|
}
|
||||||
|
return "git@gitlab.com:" + path
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// RigConfig represents the rig-level configuration (config.json at rig root).
|
// RigConfig represents the rig-level configuration (config.json at rig root).
|
||||||
type RigConfig struct {
|
type RigConfig struct {
|
||||||
Type string `json:"type"` // "rig"
|
Type string `json:"type"` // "rig"
|
||||||
@@ -285,12 +331,12 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
|
|||||||
fmt.Printf(" Warning: could not use local repo reference: %v\n", err)
|
fmt.Printf(" Warning: could not use local repo reference: %v\n", err)
|
||||||
_ = os.RemoveAll(bareRepoPath)
|
_ = os.RemoveAll(bareRepoPath)
|
||||||
if err := m.git.CloneBare(opts.GitURL, bareRepoPath); err != nil {
|
if err := m.git.CloneBare(opts.GitURL, bareRepoPath); err != nil {
|
||||||
return nil, fmt.Errorf("creating bare repo: %w", err)
|
return nil, wrapCloneError(err, opts.GitURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := m.git.CloneBare(opts.GitURL, bareRepoPath); err != nil {
|
if err := m.git.CloneBare(opts.GitURL, bareRepoPath); err != nil {
|
||||||
return nil, fmt.Errorf("creating bare repo: %w", err)
|
return nil, wrapCloneError(err, opts.GitURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf(" ✓ Created shared bare repo\n")
|
fmt.Printf(" ✓ Created shared bare repo\n")
|
||||||
|
|||||||
@@ -690,3 +690,56 @@ func TestSplitCompoundWord(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConvertToSSH(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
https string
|
||||||
|
wantSSH string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GitHub with .git suffix",
|
||||||
|
https: "https://github.com/owner/repo.git",
|
||||||
|
wantSSH: "git@github.com:owner/repo.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitHub without .git suffix",
|
||||||
|
https: "https://github.com/owner/repo",
|
||||||
|
wantSSH: "git@github.com:owner/repo.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitHub with org/subpath",
|
||||||
|
https: "https://github.com/myorg/myproject.git",
|
||||||
|
wantSSH: "git@github.com:myorg/myproject.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitLab with .git suffix",
|
||||||
|
https: "https://gitlab.com/owner/repo.git",
|
||||||
|
wantSSH: "git@gitlab.com:owner/repo.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitLab without .git suffix",
|
||||||
|
https: "https://gitlab.com/owner/repo",
|
||||||
|
wantSSH: "git@gitlab.com:owner/repo.git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unknown host returns empty",
|
||||||
|
https: "https://bitbucket.org/owner/repo.git",
|
||||||
|
wantSSH: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-HTTPS URL returns empty",
|
||||||
|
https: "git@github.com:owner/repo.git",
|
||||||
|
wantSSH: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := convertToSSH(tt.https)
|
||||||
|
if got != tt.wantSSH {
|
||||||
|
t.Errorf("convertToSSH(%q) = %q, want %q", tt.https, got, tt.wantSSH)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user