From e538f31a5dd18d1afffb9fe7d6de4a6ec8444fcf Mon Sep 17 00:00:00 2001 From: Neha Sherpa Date: Wed, 11 Feb 2026 14:53:13 -0800 Subject: [PATCH 1/2] fix: Fix git fetch auth errors --- internal/gitclone/command.go | 17 +++++++- internal/gitclone/command_test.go | 68 +++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/internal/gitclone/command.go b/internal/gitclone/command.go index e003a7e..d85be9d 100644 --- a/internal/gitclone/command.go +++ b/internal/gitclone/command.go @@ -15,8 +15,10 @@ import ( func (r *Repository) gitCommand(ctx context.Context, args ...string) (*exec.Cmd, error) { repoURL := r.upstreamURL modifiedURL := repoURL + var token string if r.credentialProvider != nil && strings.Contains(repoURL, "github.com") { - token, err := r.credentialProvider.GetTokenForURL(ctx, repoURL) + var err error + token, err = r.credentialProvider.GetTokenForURL(ctx, repoURL) if err == nil && token != "" { modifiedURL = injectTokenIntoURL(repoURL, token) } @@ -32,6 +34,19 @@ func (r *Repository) gitCommand(ctx context.Context, args ...string) (*exec.Cmd, if len(configArgs) > 0 { allArgs = append(allArgs, configArgs...) } + + // Add credential helper configuration if we have a token + // This ensures git uses our GitHub App token for authentication + // even when the URL is read from .git/config (e.g., for git remote update) + if token != "" { + // Use a credential helper that approves all requests with our token + // The '!f() { ... }; f' syntax runs an inline shell function + // We use printf to safely output the token without shell interpretation issues + escapedToken := strings.ReplaceAll(token, "'", "'\\''") + credHelper := "!f() { test \"$1\" = get && echo username=x-access-token && printf 'password=%s\\n' '" + escapedToken + "'; }; f" + allArgs = append(allArgs, "-c", "credential.helper="+credHelper) + } + allArgs = append(allArgs, args...) // Replace URL in args if it was modified for authentication diff --git a/internal/gitclone/command_test.go b/internal/gitclone/command_test.go index c5757ce..4f9465a 100644 --- a/internal/gitclone/command_test.go +++ b/internal/gitclone/command_test.go @@ -2,6 +2,7 @@ package gitclone //nolint:testpackage // Internal functions need to be tested import ( "context" + "strings" "testing" "github.com/alecthomas/assert/v2" @@ -76,3 +77,70 @@ func TestGitCommandWithEmptyURL(t *testing.T) { assert.Equal(t, "git", cmd.Args[0]) assert.Equal(t, "version", cmd.Args[len(cmd.Args)-1]) } + +type mockCredentialProvider struct { + token string + err error +} + +func (m *mockCredentialProvider) GetTokenForURL(_ context.Context, _ string) (string, error) { + return m.token, m.err +} + +func TestGitCommandWithCredentialProvider(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + token string + expectHelper bool + expectedToken string + }{ + { + name: "WithValidToken", + token: "ghp_test123456", + expectHelper: true, + expectedToken: "ghp_test123456", + }, + { + name: "WithTokenContainingSingleQuote", + token: "token'with'quotes", + expectHelper: true, + expectedToken: "token'with'quotes", + }, + { + name: "WithEmptyToken", + token: "", + expectHelper: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &Repository{ + upstreamURL: "https://github.com/user/repo", + credentialProvider: &mockCredentialProvider{ + token: tt.token, + }, + } + + cmd, err := repo.gitCommand(ctx, "version") + assert.NoError(t, err) + assert.NotZero(t, cmd) + + if tt.expectHelper { + found := false + for i, arg := range cmd.Args { + if arg == "-c" && i+1 < len(cmd.Args) { + if strings.Contains(cmd.Args[i+1], "credential.helper=") { + found = true + assert.True(t, strings.Contains(cmd.Args[i+1], "username=x-access-token")) + break + } + } + } + assert.True(t, found, "expected credential.helper to be configured") + } + }) + } +} From d4d5a877748831eccfd27c91bec8a9114c17bf5d Mon Sep 17 00:00:00 2001 From: Neha Sherpa Date: Wed, 11 Feb 2026 15:15:11 -0800 Subject: [PATCH 2/2] fix: Clean up --- internal/gitclone/command.go | 49 ++++-------------------------------- 1 file changed, 5 insertions(+), 44 deletions(-) diff --git a/internal/gitclone/command.go b/internal/gitclone/command.go index d85be9d..d4728c0 100644 --- a/internal/gitclone/command.go +++ b/internal/gitclone/command.go @@ -5,7 +5,6 @@ package gitclone import ( "bufio" "context" - "net/url" "os/exec" "strings" @@ -14,15 +13,14 @@ import ( func (r *Repository) gitCommand(ctx context.Context, args ...string) (*exec.Cmd, error) { repoURL := r.upstreamURL - modifiedURL := repoURL var token string if r.credentialProvider != nil && strings.Contains(repoURL, "github.com") { var err error token, err = r.credentialProvider.GetTokenForURL(ctx, repoURL) - if err == nil && token != "" { - modifiedURL = injectTokenIntoURL(repoURL, token) - } // If error getting token, fall back to original URL (system credentials) + if err != nil { + token = "" + } } configArgs, err := getInsteadOfDisableArgsForURL(ctx, repoURL) @@ -36,12 +34,9 @@ func (r *Repository) gitCommand(ctx context.Context, args ...string) (*exec.Cmd, } // Add credential helper configuration if we have a token - // This ensures git uses our GitHub App token for authentication - // even when the URL is read from .git/config (e.g., for git remote update) + // This ensures git uses the GitHub App token for authentication + // for all operations (clone, fetch, remote update, etc.) if token != "" { - // Use a credential helper that approves all requests with our token - // The '!f() { ... }; f' syntax runs an inline shell function - // We use printf to safely output the token without shell interpretation issues escapedToken := strings.ReplaceAll(token, "'", "'\\''") credHelper := "!f() { test \"$1\" = get && echo username=x-access-token && printf 'password=%s\\n' '" + escapedToken + "'; }; f" allArgs = append(allArgs, "-c", "credential.helper="+credHelper) @@ -49,43 +44,9 @@ func (r *Repository) gitCommand(ctx context.Context, args ...string) (*exec.Cmd, allArgs = append(allArgs, args...) - // Replace URL in args if it was modified for authentication - if modifiedURL != repoURL { - for i, arg := range allArgs { - if arg == repoURL { - allArgs[i] = modifiedURL - } - } - } - return exec.CommandContext(ctx, "git", allArgs...), nil } -// Converts https://github.com/org/repo to https://x-access-token:TOKEN@github.com/org/repo -func injectTokenIntoURL(rawURL, token string) string { - if token == "" { - return rawURL - } - - u, err := url.Parse(rawURL) - if err != nil { - return rawURL - } - - // Only inject token for GitHub URLs - if !strings.Contains(u.Host, "github.com") { - return rawURL - } - - // Upgrade http to https for security - if u.Scheme == "http" { - u.Scheme = "https" - } - - u.User = url.UserPassword("x-access-token", token) - return u.String() -} - func getInsteadOfDisableArgsForURL(ctx context.Context, targetURL string) ([]string, error) { if targetURL == "" { return nil, nil