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
48 changes: 30 additions & 18 deletions services/cloner.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import (
"strings"

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
memorystorage "github.com/go-git/go-git/v5/storage/memory"
memorystorage "github.com/go-git/go-git/v5/storage/memory"
)

type Cloner interface {
Expand All @@ -33,11 +34,7 @@ func (gc *GitCloner) CloneRepository(repoURL, outputDir string) error {
}

if gc.isSSH(repoURL) {
sshKeyPath, err := gc.getSSHKeyPath()
if err != nil {
return fmt.Errorf("failed to get SSH key path: %w", err)
}
auth, err := ssh.NewPublicKeysFromFile("git", sshKeyPath, "")
auth, err := gc.getSSHAuth()
if err != nil {
return fmt.Errorf("failed to create SSH auth method: %v", err)
}
Expand All @@ -64,11 +61,7 @@ func (gc *GitCloner) CloneRepositoryBranch(repoURL, branch, outputDir string) er
}

if gc.isSSH(repoURL) {
sshKeyPath, err := gc.getSSHKeyPath()
if err != nil {
return fmt.Errorf("failed to get SSH key path: %w", err)
}
auth, err := ssh.NewPublicKeysFromFile("git", sshKeyPath, "")
auth, err := gc.getSSHAuth()
if err != nil {
return fmt.Errorf("failed to create SSH auth method: %v", err)
}
Expand All @@ -88,11 +81,7 @@ func (gc *GitCloner) ListRemoteBranches(repoURL string) ([]string, error) {
listOpts := &git.ListOptions{}

if gc.isSSH(repoURL) {
sshKeyPath, err := gc.getSSHKeyPath()
if err != nil {
return nil, fmt.Errorf("failed to get SSH key path: %w", err)
}
auth, err := ssh.NewPublicKeysFromFile("git", sshKeyPath, "")
auth, err := gc.getSSHAuth()
if err != nil {
return nil, fmt.Errorf("failed to create SSH auth method: %v", err)
}
Expand Down Expand Up @@ -134,6 +123,29 @@ func (gc *GitCloner) SetSSHKeyPath(path string) {
gc.SSHKeyPath = path
}

// getSSHAuth returns an SSH auth method. It tries ssh-agent first (when SSH_AUTH_SOCK
// is set and keys are loaded), then falls back to reading the key file directly.
func (gc *GitCloner) getSSHAuth() (transport.AuthMethod, error) {
// Try ssh-agent first — handles passphrase-protected keys already unlocked in the agent
if os.Getenv("SSH_AUTH_SOCK") != "" {
auth, err := ssh.NewSSHAgentAuth("git")
if err == nil {
return auth, nil
}
}

// Fall back to key file with empty passphrase
sshKeyPath, err := gc.getSSHKeyPath()
if err != nil {
return nil, err
}
auth, err := ssh.NewPublicKeysFromFile("git", sshKeyPath, "")
if err != nil {
return nil, fmt.Errorf("ssh-agent not available and key file %s requires a passphrase (add your key to ssh-agent with: ssh-add %s)", sshKeyPath, sshKeyPath)
}
return auth, nil
}

func (gc *GitCloner) isSSH(repoURL string) bool {
return strings.HasPrefix(repoURL, "git@") || strings.HasPrefix(repoURL, "ssh://")
}
Expand Down
33 changes: 33 additions & 0 deletions services/cloner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,39 @@ func TestSetSSHKeyPath(t *testing.T) {
}
}

func TestGetSSHAuth_UsesAgentWhenAvailable(t *testing.T) {
gc := &GitCloner{}

// When SSH_AUTH_SOCK is set and agent is running, getSSHAuth should succeed
if os.Getenv("SSH_AUTH_SOCK") == "" {
t.Skip("SSH_AUTH_SOCK not set, skipping ssh-agent test")
}

auth, err := gc.getSSHAuth()
if err != nil {
t.Fatalf("expected ssh-agent auth to succeed, got: %v", err)
}
if auth == nil {
t.Fatal("expected non-nil auth method")
}
}

func TestGetSSHAuth_FallsBackToKeyFile(t *testing.T) {
gc := &GitCloner{}

// Unset SSH_AUTH_SOCK to force key file fallback
origSock := os.Getenv("SSH_AUTH_SOCK")
os.Unsetenv("SSH_AUTH_SOCK")
defer os.Setenv("SSH_AUTH_SOCK", origSock)

// Without agent and with passphrase-protected keys, this should fail
// with a helpful error message
_, err := gc.getSSHAuth()
// It may succeed if the user has unprotected keys, or fail — both are valid
// We just verify it doesn't panic
_ = err
}

func TestIsSSH(t *testing.T) {
gc := &GitCloner{}

Expand Down