175 lines
6.0 KiB
Go
175 lines
6.0 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"log"
|
||
"log/slog"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
|
||
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/gitadapters/bitbucket"
|
||
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/gitadapters/gitea"
|
||
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/pierre"
|
||
"github.com/alecthomas/kong"
|
||
kongyaml "github.com/alecthomas/kong-yaml"
|
||
)
|
||
|
||
type BitbucketConfig struct {
|
||
BaseURL string `help:"Bitbucket Base URL (e.g. https://bitbucket.example.com)" env:"BITBUCKET_URL"`
|
||
Token string `help:"Bearer Token" env:"BITBUCKET_TOKEN"`
|
||
}
|
||
|
||
type GiteaConfig struct {
|
||
BaseURL string `help:"Gitea Base URL (e.g. https://gitea.com)" env:"GITEA_URL"`
|
||
Token string `help:"API Token" env:"GITEA_TOKEN"`
|
||
}
|
||
|
||
type RepoArgs struct {
|
||
Owner string `arg:"" help:"Project Key or Owner" env:"PIERRE_OWNER"`
|
||
Repo string `arg:"" help:"Repository Slug" env:"PIERRE_REPO"`
|
||
PRID int `arg:"" help:"Pull Request ID" name:"pr"`
|
||
}
|
||
|
||
type LLMConfig struct {
|
||
Provider string `help:"Provider for llm (ollama or gemini)" required:"" env:"LLM_PROVIDER"`
|
||
BaseURL string `help:"Endpoint for provider (only for ollama)" env:"LLM_BASE_URL"`
|
||
APIKey string `help:"APIKey for provider" env:"LLM_API_KEY"`
|
||
Model string `help:"Model to use" env:"LLM_MODEL"`
|
||
}
|
||
|
||
// ReviewConfig holds the review‑specific CLI options.
|
||
// The `default:"60000"` tag sets an integer default of 60 KB – Kong parses the string value into the int field, which can be confusing for readers.
|
||
type ReviewConfig struct {
|
||
MaxChunkSize int `help:"Maximum diff chunk size in bytes" default:"60000"`
|
||
Guidelines []string `help:"Project guidelines to prepend" sep:","`
|
||
DisableComments bool `help:"Disable posting comments (dry run)"`
|
||
}
|
||
|
||
type Config struct {
|
||
LogLevel string `help:"Log verbosity: debug, info, warn, error"`
|
||
// Deprecated flags (no prefix). Retained for backward compatibility.
|
||
// These will be mapped to the embedded ReviewConfig if provided.
|
||
MaxChunkSize int `help:"Deprecated: use --review-max-chunk-size"`
|
||
Guidelines []string `help:"Deprecated: use --review-guidelines" sep:","`
|
||
DisableComments bool `help:"Deprecated: use --review-disable-comments"`
|
||
|
||
// Embedding ReviewConfig with a prefix changes flag names to `--review-…`.
|
||
// Existing configuration files using the old flag names will need to be updated.
|
||
// Consider keeping backwards compatibility if required.
|
||
Review ReviewConfig `embed:"" prefix:"review-"`
|
||
GitProvider string `help:"Git provider (bitbucket or gitea)" env:"GIT_PROVIDER"`
|
||
Bitbucket BitbucketConfig `embed:"" prefix:"bitbucket-"`
|
||
Gitea GiteaConfig `embed:"" prefix:"gitea-"`
|
||
Repo RepoArgs `embed:""`
|
||
LLM LLMConfig `embed:"" prefix:"llm-"`
|
||
Config kong.ConfigFlag `help:"Path to a YAML config file"`
|
||
}
|
||
|
||
func main() {
|
||
|
||
cfg := &Config{}
|
||
home, err := os.UserHomeDir()
|
||
if err != nil {
|
||
log.Fatalf("could not find home directory: %v", err)
|
||
}
|
||
|
||
defaultConfig := filepath.Join(home, ".config", "pierre", "config.yaml")
|
||
|
||
// Parse flags, env vars, and config files
|
||
kong.Parse(cfg,
|
||
kong.Name("pierre"),
|
||
kong.Description("AI-powered Pull Request reviewer"),
|
||
kong.UsageOnError(),
|
||
kong.Configuration(kongyaml.Loader, "config.yaml", defaultConfig),
|
||
)
|
||
|
||
// Configure global slog logger based on the --log-level flag
|
||
lvl := slog.LevelInfo
|
||
switch strings.ToLower(cfg.LogLevel) {
|
||
case "debug":
|
||
lvl = slog.LevelDebug
|
||
case "info":
|
||
lvl = slog.LevelInfo
|
||
case "warn":
|
||
lvl = slog.LevelWarn
|
||
case "error":
|
||
lvl = slog.LevelError
|
||
}
|
||
// Logs are sent to stderr so that stdout can be safely piped.
|
||
// The review output (fmt.Printf) stays on stdout unchanged.
|
||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}))
|
||
slog.SetDefault(logger)
|
||
|
||
// Backwards compatibility: map deprecated flag values (if any) to the embedded ReviewConfig.
|
||
if cfg.MaxChunkSize != 0 {
|
||
cfg.Review.MaxChunkSize = cfg.MaxChunkSize
|
||
}
|
||
if len(cfg.Guidelines) > 0 {
|
||
cfg.Review.Guidelines = cfg.Guidelines
|
||
}
|
||
if cfg.DisableComments {
|
||
cfg.Review.DisableComments = cfg.DisableComments
|
||
}
|
||
|
||
// Auto-detect provider
|
||
provider := cfg.GitProvider
|
||
if provider == "" {
|
||
if cfg.Bitbucket.BaseURL != "" && cfg.Gitea.BaseURL == "" {
|
||
provider = "bitbucket"
|
||
} else if cfg.Gitea.BaseURL != "" && cfg.Bitbucket.BaseURL == "" {
|
||
provider = "gitea"
|
||
} else if cfg.Bitbucket.BaseURL != "" && cfg.Gitea.BaseURL != "" {
|
||
log.Fatal("Multiple git providers configured. Please specify one using --git-provider.")
|
||
} else {
|
||
log.Fatal("No git provider configured. Please provide Bitbucket or Gitea configuration.")
|
||
}
|
||
}
|
||
|
||
var git pierre.GitAdapter
|
||
|
||
switch provider {
|
||
case "bitbucket":
|
||
if cfg.Bitbucket.BaseURL == "" {
|
||
log.Fatal("Bitbucket Base URL is required when using bitbucket provider.")
|
||
}
|
||
git = bitbucket.NewBitbucket(cfg.Bitbucket.BaseURL, cfg.Bitbucket.Token)
|
||
case "gitea":
|
||
if cfg.Gitea.BaseURL == "" {
|
||
log.Fatal("Gitea Base URL is required when using gitea provider.")
|
||
}
|
||
var err error
|
||
git, err = gitea.New(cfg.Gitea.BaseURL, cfg.Gitea.Token)
|
||
if err != nil {
|
||
log.Fatalf("Error initializing Gitea adapter: %v", err)
|
||
}
|
||
default:
|
||
log.Fatalf("Unknown git provider: %s", provider)
|
||
}
|
||
|
||
// Initialize AI Adapter
|
||
var ai pierre.ChatAdapter
|
||
|
||
switch cfg.LLM.Provider {
|
||
case "gemini":
|
||
ai, err = chatter.NewGeminiAdapter(context.Background(), cfg.LLM.APIKey, cfg.LLM.Model)
|
||
case "ollama":
|
||
ai, err = chatter.NewOllamaAdapter(cfg.LLM.BaseURL, cfg.LLM.Model)
|
||
case "openai":
|
||
ai = chatter.NewOpenAIAdapter(cfg.LLM.APIKey, cfg.LLM.Model, cfg.LLM.BaseURL)
|
||
|
||
default:
|
||
log.Fatalf("%s is not a valid llm provider", cfg.LLM.Provider)
|
||
}
|
||
|
||
if err != nil {
|
||
log.Fatalf("Error initializing AI: %v", err)
|
||
}
|
||
|
||
pierreService := pierre.New(ai, git, cfg.Review.MaxChunkSize, cfg.Review.Guidelines, cfg.Review.DisableComments)
|
||
if err := pierreService.MakeReview(context.Background(), cfg.Repo.Owner, cfg.Repo.Repo, cfg.Repo.PRID); err != nil {
|
||
log.Fatalf("Error during review: %v", err)
|
||
}
|
||
}
|