Compare commits

..

1 Commits

Author SHA1 Message Date
u80864958
ac5ff7aeeb feat(pierre): add diff chunking and configurable review settings 2026-02-13 17:04:22 +01:00
4 changed files with 32 additions and 12 deletions

View File

@@ -37,13 +37,18 @@ type LLMConfig struct {
Model string `help:"Model to use" env:"LLM_MODEL"`
}
// ReviewConfig holds the reviewspecific CLI options.
// The `default:"60000"` tag sets an integer default of 60KB Kong parses the string value into the int field, which can be confusing for readers.
type ReviewConfig struct {
MaxChunkChars int `help:"Maximum diff chunk size in characters (default 60000)" default:"60000"`
Guidelines []string `help:"Project-specific review guidelines"`
DisableComments bool `help:"Do not post comments to the Git provider (dryrun mode)"`
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 {
// 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-"`
@@ -124,7 +129,7 @@ func main() {
log.Fatalf("Error initializing AI: %v", err)
}
pierreService := pierre.New(ai, git, cfg.Review.MaxChunkChars, cfg.Review.Guidelines, cfg.Review.DisableComments)
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)
}

View File

@@ -9,6 +9,8 @@ import (
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
)
const defaultChunkSize = 60000
type Comment struct {
File string `json:"file"`
Line int `json:"line"`
@@ -24,7 +26,7 @@ func (s *Service) judgePR(ctx context.Context, diff io.Reader) (comments []Comme
// Determine chunk size (use default if not set)
maxSize := s.maxChunkSize
if maxSize <= 0 {
maxSize = 60000 // default 60KB ~ 15k tokens
maxSize = defaultChunkSize // default 60KB ~ 15k tokens
}
chunks := splitDiffIntoChunks(diffBytes, maxSize)
@@ -33,7 +35,7 @@ func (s *Service) judgePR(ctx context.Context, diff io.Reader) (comments []Comme
// Build optional guidelines text
guidelinesText := ""
if len(s.guidelines) > 0 {
guidelinesText = "\nProject guidelines:\n"
guidelinesText = "Project guidelines:\n"
for _, g := range s.guidelines {
guidelinesText += "- " + g + "\n"
}
@@ -102,9 +104,13 @@ func splitDiffIntoChunks(diff []byte, maxSize int) []string {
// Split further by hunks
hunks := strings.Split(seg, "\n@@ ")
for j, h := range hunks {
hseg := h
if j != 0 {
hseg = "@@ " + h
var hseg string
if j == 0 {
// First hunk segment already contains the preceding content (including any needed newline)
hseg = h
} else {
// Subsequent hunks need the leading newline and "@@ " marker restored
hseg = "\n@@ " + h
}
if current.Len()+len(hseg) > maxSize && current.Len() > 0 {
chunks = append(chunks, current.String())
@@ -129,4 +135,3 @@ func splitDiffIntoChunks(diff []byte, maxSize int) []string {
}
return chunks
}

View File

@@ -12,9 +12,10 @@ import (
)
// mockChat implements the ChatAdapter interface for testing.
type mockChat struct{}
type mockChat struct{ callCount int }
func (m *mockChat) GenerateStructured(ctx context.Context, msgs []chatter.Message, target interface{}) error {
m.callCount++
if cSlice, ok := target.(*[]Comment); ok {
*cSlice = []Comment{{File: "file.go", Line: 1, Message: "test comment"}}
return nil
@@ -106,11 +107,12 @@ func TestSplitDiffIntoChunks(t *testing.T) {
}
func TestJudgePR_ChunkAggregationAndDeduplication(t *testing.T) {
chatMock := &mockChat{}
svc := &Service{
maxChunkSize: 50,
guidelines: nil,
git: &mockGit{},
chat: &mockChat{},
chat: chatMock,
}
diffReader, err := svc.git.GetDiff(context.Background(), "", "", 0)
if err != nil {
@@ -124,4 +126,7 @@ func TestJudgePR_ChunkAggregationAndDeduplication(t *testing.T) {
if got, want := len(comments), 1; got != want {
t.Fatalf("expected %d comment after deduplication, got %d", want, got)
}
if chatMock.callCount != 2 {
t.Fatalf("expected mockChat to be called for each chunk (2), got %d", chatMock.callCount)
}
}

View File

@@ -7,6 +7,11 @@ import (
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
)
// Service holds the core collaborators and configuration for Pierre.
// The order of the fields is intentional: configuration fields first (used
// during initialization) followed by the adapters. This prevents accidental
// changes to the serialized layout if encoding/gob or encoding/json is used
// elsewhere in the future.
type Service struct {
maxChunkSize int
guidelines []string