Skip to content
Open
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
8 changes: 8 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/antlss/gitlab-review-agent/internal/handler/webhook"
"github.com/antlss/gitlab-review-agent/internal/handler/worker"
"github.com/antlss/gitlab-review-agent/internal/pkg/queue"
"github.com/antlss/gitlab-review-agent/internal/pkg/store"
)

func main() {
Expand All @@ -41,6 +42,13 @@ func main() {
}
slog.Info("starting ai-review-agent server", "store_driver", cfg.Store.Driver)

// Eagerly initialize the store container. provideStores registers the
// individual store interfaces (ReviewJobStore, ReplyJobStore, ...) as a
// side effect, and it is lazy — nothing else invokes *store.Stores before
// the worker pool needs those interfaces, so without this the server panics
// with "could not find service ReviewJobStore". The CLI already does this.
_ = do.MustInvoke[*store.Stores](injector)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Expand Down
27 changes: 26 additions & 1 deletion internal/pkg/git/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"context"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -95,7 +96,7 @@ func (m *Manager) ReleaseGitLock(_ context.Context, projectID int64) {
// without forcing full working tree materialization for large repositories.
func (m *Manager) FetchAndCheckout(ctx context.Context, projectID int64, projectPath string, mrIID int64, targetBranch, headSHA string) error {
repoPath := m.RepoPath(projectID)
cloneURL := fmt.Sprintf("%s/%s.git", m.gitlabURL, projectPath)
cloneURL := m.authCloneURL(projectPath)

cloned, err := m.ensureFullClone(ctx, repoPath, cloneURL)
if err != nil {
Expand Down Expand Up @@ -562,11 +563,34 @@ func (m *Manager) goGitAuth() *githttp.BasicAuth {
}
}

// authCloneURL builds the HTTPS clone URL with oauth2 basic-auth credentials
// embedded (https://oauth2:<token>@host/path.git). GitLab's git-over-HTTP smart
// protocol does not honor the "PRIVATE-TOKEN" http.extraHeader used for API
// calls, so a bare URL makes the git CLI prompt for a username and fail in a
// non-interactive context. Embedding oauth2:<token> is the auth method GitLab
// accepts for git transport. Falls back to the plain URL if the base can't be
// parsed or no token is set.
func (m *Manager) authCloneURL(projectPath string) string {
plain := fmt.Sprintf("%s/%s.git", m.gitlabURL, projectPath)
if m.gitlabToken == "" {
return plain
}
u, err := url.Parse(m.gitlabURL)
if err != nil || u.Host == "" {
return plain
}
u.User = url.UserPassword("oauth2", m.gitlabToken)
return fmt.Sprintf("%s/%s.git", u.String(), strings.TrimPrefix(projectPath, "/"))
}

// GitEnv returns environment variables for git commands that inject the GitLab
// token, http buffer, and HTTP/1.1 settings via GIT_CONFIG environment variables.
func (m *Manager) GitEnv() []string {
// GIT_TERMINAL_PROMPT=0: never prompt for credentials in this non-interactive
// context — fail fast instead of hanging when auth is missing/rejected.
if m.gitlabToken == "" {
return append(os.Environ(),
"GIT_TERMINAL_PROMPT=0",
"GIT_CONFIG_COUNT=4",
"GIT_CONFIG_KEY_0=http.postBuffer",
"GIT_CONFIG_VALUE_0=524288000",
Expand All @@ -579,6 +603,7 @@ func (m *Manager) GitEnv() []string {
)
}
return append(os.Environ(),
"GIT_TERMINAL_PROMPT=0",
"GIT_CONFIG_COUNT=5",
"GIT_CONFIG_KEY_0=http.extraHeader",
fmt.Sprintf("GIT_CONFIG_VALUE_0=PRIVATE-TOKEN: %s", m.gitlabToken),
Expand Down