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
56 changes: 52 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ retention conventions, see [docs/review-guidance.md](docs/review-guidance.md).

## Authentication And Setup

`cr` v1 supports GitHub personal access token authentication for Git-host
operations. The credential key for GitHub PATs is `git_token`.
`cr` supports GitHub (personal access token or GitHub App) and GitLab
(personal access token) authentication for Git-host operations. The credential
key for Git-host PATs is `git_token`.

Quick setup with adapter-managed LLM credentials:

Expand Down Expand Up @@ -256,6 +257,52 @@ read runtime tokens directly from arbitrary environment variables. Reviewer
credentials use a separate credential name that also stores key `git_token`;
keep it distinct from the user Git credential in the same store.

### GitLab

GitLab merge requests (gitlab.com and self-managed instances) are reviewed
with the same commands as GitHub pull requests. A GitLab profile sets
`git.provider: gitlab` and authenticates with a personal access token holding
the `api` scope, stored under the same `git_token` credential key:

```yaml
profiles:
gitlab-work:
git:
provider: gitlab
host: gitlab.example.com
auth_mode: pat
credential:
store: local-os
name: codereview/gitlab-work
```

```bash
printf '%s' "$GITLAB_TOKEN" | cr set-credential \
--store local-os \
--name codereview/gitlab-work \
--key git_token \
--stdin

cr review https://gitlab.example.com/group/subgroup/project/-/merge_requests/42
```

`git.provider` defaults to `github` when omitted, so existing configs are
unaffected. GitLab specifics worth knowing:

- Inline findings post as diff discussions and thread resolution uses the
discussions API, so `resolve_threads` works as on GitHub.
- GitLab has no review-object equivalent of a GitHub review: an `approve`
verdict calls the approvals API and the review summary posts as a
merge-request note; `request_changes` revokes any active approval by the
posting identity before posting the summary.
- `pat` is the only supported `auth_mode` for GitLab; there is no GitHub App
equivalent.
- The `cr init` wizard does not offer GitLab yet — add the profile to
`config.yml` directly (validated on load) and store the token with
`cr set-credential`.
- Reading merge-request diffs requires GitLab 15.7 or newer for the raw diff
endpoint; older instances fall back to per-file diff reconstruction.

### Org Deployment / MDM

For managed rollouts, ship non-secret config and pre-stage secrets in the
Expand Down Expand Up @@ -1043,8 +1090,9 @@ provenance, and trust note when repo-local agents are considered.
cr review <PR> [flags]
```

Runs the automated review pipeline for a GitHub pull request URL. The PR host
must match the active profile's `git.host`.
Runs the automated review pipeline for a GitHub pull request URL or a GitLab
merge request URL. The PR host must match the active profile's `git.host`, and
the URL's provider family must match the profile's `git.provider`.

Modes:

Expand Down
27 changes: 15 additions & 12 deletions internal/app/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/open-cli-collective/codereview-cli/internal/gitexec"
"github.com/open-cli-collective/codereview-cli/internal/gitprovider"
githubprovider "github.com/open-cli-collective/codereview-cli/internal/gitprovider/github"
"github.com/open-cli-collective/codereview-cli/internal/gitproviders"
"github.com/open-cli-collective/codereview-cli/internal/hooks"
"github.com/open-cli-collective/codereview-cli/internal/ledger"
"github.com/open-cli-collective/codereview-cli/internal/llm"
Expand Down Expand Up @@ -91,7 +92,7 @@ type SelectionOpenRequest struct {

// GitProviderFactory builds a provider and the credential used to authenticate
// it.
type GitProviderFactory func(config.GitConfig, credentials.Reader, githubprovider.Options) (gitprovider.GitProvider, gitprovider.Credential, error)
type GitProviderFactory func(config.GitConfig, credentials.Reader, gitproviders.Options) (gitprovider.GitProvider, gitprovider.Credential, error)

// PostingIdentityResolver resolves the identity used for live review writes.
type PostingIdentityResolver func(context.Context, gitprovider.GitProvider, gitprovider.Credential, credentials.Reader, config.Profile) (gitprovider.Identity, error)
Expand All @@ -103,7 +104,7 @@ type AdapterFactory func(config.LLMConfig, credentials.Reader) (llm.Adapter, err
// defaults.
type Dependencies struct {
NewGitProvider GitProviderFactory
NewGitCommand func(string, gitexec.TokenSource) (func(context.Context, string, ...string) ([]byte, error), error)
NewGitCommand func(string, gitexec.TokenSource, string) (func(context.Context, string, ...string) ([]byte, error), error)
ResolveRepoRoot func(context.Context) (string, error)
ResolvePostingIdentity PostingIdentityResolver
NewAdapter AdapterFactory
Expand All @@ -114,16 +115,14 @@ type Dependencies struct {

func (d Dependencies) withDefaults() Dependencies {
if d.NewGitProvider == nil {
d.NewGitProvider = func(git config.GitConfig, store credentials.Reader, opts githubprovider.Options) (gitprovider.GitProvider, gitprovider.Credential, error) {
return githubprovider.NewFromGitConfig(git, store, opts)
}
d.NewGitProvider = gitproviders.New
}
if d.ResolvePostingIdentity == nil {
d.ResolvePostingIdentity = resolvePostingIdentity
}
if d.NewGitCommand == nil {
d.NewGitCommand = func(host string, tokens gitexec.TokenSource) (func(context.Context, string, ...string) ([]byte, error), error) {
client, err := gitexec.New(host, tokens)
d.NewGitCommand = func(host string, tokens gitexec.TokenSource, username string) (func(context.Context, string, ...string) ([]byte, error), error) {
client, err := gitexec.NewWithUsername(host, tokens, username)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -169,7 +168,7 @@ func Open(ctx context.Context, req OpenRequest) (Runtime, error) {
cleanup()
return Runtime{}, err
}
gitCommand, err := deps.NewGitCommand(profile.Git.Host, repositoryTokenSource(repoProvider, repoCredential))
gitCommand, err := deps.NewGitCommand(profile.Git.Host, repositoryTokenSource(repoProvider, repoCredential), gitproviders.GitBasicAuthUsername(profile.Git.ProviderKind()))
if err != nil {
cleanup()
return Runtime{}, err
Expand Down Expand Up @@ -295,6 +294,7 @@ func gitConfigForReviewerAuth(profile config.Profile) config.GitConfig {
githubApp = &app
}
return config.GitConfig{
Provider: profile.Git.Provider,
Host: profile.Git.Host,
AuthMode: profile.ReviewerCredentials.AuthMode,
Credential: profile.ReviewerCredentials.Credential,
Expand Down Expand Up @@ -362,14 +362,17 @@ func normalizeRuntimeProfile(profile config.Profile) config.Profile {
}).Profiles["runtime"]
}

func gitProviderOptions(profile config.Profile, git config.GitConfig, ref gitprovider.PRRef) githubprovider.Options {
opts := githubprovider.Options{}
func gitProviderOptions(profile config.Profile, git config.GitConfig, ref gitprovider.PRRef) gitproviders.Options {
opts := gitproviders.Options{}
if git.ProviderKind() != config.GitProviderGitHub {
return opts
}
if installationID := config.PinnedGitHubAppInstallationIDForGit(profile, git); installationID != "" {
opts.InstallationID = installationID
opts.GitHub.InstallationID = installationID
return opts
}
if strings.TrimSpace(ref.Owner) != "" && strings.TrimSpace(ref.Repo) != "" {
opts.InstallationLookup = &githubprovider.InstallationLookup{
opts.GitHub.InstallationLookup = &githubprovider.InstallationLookup{
Owner: ref.Owner,
Repo: ref.Repo,
}
Expand Down
3 changes: 2 additions & 1 deletion internal/app/runtime_selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/open-cli-collective/codereview-cli/internal/config"
"github.com/open-cli-collective/codereview-cli/internal/gitproviders"
"github.com/open-cli-collective/codereview-cli/internal/pipeline"
)

Expand Down Expand Up @@ -38,7 +39,7 @@ func OpenSelection(ctx context.Context, req SelectionOpenRequest) (SelectionRunt
cleanup()
return SelectionRuntime{}, err
}
gitCommand, err := deps.NewGitCommand(profile.Git.Host, repositoryTokenSource(provider, credential))
gitCommand, err := deps.NewGitCommand(profile.Git.Host, repositoryTokenSource(provider, credential), gitproviders.GitBasicAuthUsername(profile.Git.ProviderKind()))
if err != nil {
cleanup()
return SelectionRuntime{}, err
Expand Down
Loading
Loading