From 9b34b6bfec79831670d6a8295917e5bd3654d8bc Mon Sep 17 00:00:00 2001 From: Erik LaBianca Date: Fri, 16 Jan 2026 18:28:51 -0500 Subject: [PATCH] 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 --- internal/rig/manager.go | 50 ++++++++++++++++++++++++++++++++-- internal/rig/manager_test.go | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 20ca50f3..a7e82833 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -24,6 +24,52 @@ var ( 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 %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). type RigConfig struct { 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) _ = os.RemoveAll(bareRepoPath) 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 { 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") diff --git a/internal/rig/manager_test.go b/internal/rig/manager_test.go index be345581..797ad56c 100644 --- a/internal/rig/manager_test.go +++ b/internal/rig/manager_test.go @@ -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) + } + }) + } +}