Compare commits
1 Commits
9f6c6830a6
...
feat/conte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ef8da1a4 |
@@ -45,10 +45,11 @@ type ReviewConfig struct {
|
|||||||
MaxChunkSize int `help:"Maximum diff chunk size in bytes" default:"60000"`
|
MaxChunkSize int `help:"Maximum diff chunk size in bytes" default:"60000"`
|
||||||
Guidelines []string `help:"Project guidelines to prepend" sep:","`
|
Guidelines []string `help:"Project guidelines to prepend" sep:","`
|
||||||
DisableComments bool `help:"Disable posting comments (dry run)"`
|
DisableComments bool `help:"Disable posting comments (dry run)"`
|
||||||
|
SanityCheck bool `help:"Run sanity‑check LLM prompts per comment" default:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
LogLevel string `help:"Log verbosity: debug, info, warn, error"`
|
LogLevel string `help:"Log verbosity: debug, info, warn, error" default:"info"`
|
||||||
|
|
||||||
// Embedding ReviewConfig with a prefix changes flag names to `--review-…`.
|
// Embedding ReviewConfig with a prefix changes flag names to `--review-…`.
|
||||||
// Existing configuration files using the old flag names will need to be updated.
|
// Existing configuration files using the old flag names will need to be updated.
|
||||||
@@ -152,6 +153,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pierreService := pierre.New(ai, git, cfg.Review.MaxChunkSize, cfg.Review.Guidelines, cfg.Review.DisableComments)
|
pierreService := pierre.New(ai, git, cfg.Review.MaxChunkSize, cfg.Review.Guidelines, cfg.Review.DisableComments)
|
||||||
|
pierreService.SetSanityCheck(cfg.Review.SanityCheck)
|
||||||
if err := pierreService.MakeReview(context.Background(), cfg.Repo.Owner, cfg.Repo.Repo, cfg.Repo.PRID); err != nil {
|
if err := pierreService.MakeReview(context.Background(), cfg.Repo.Owner, cfg.Repo.Repo, cfg.Repo.PRID); err != nil {
|
||||||
log.Fatalf("Error during review: %v", err)
|
log.Fatalf("Error during review: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ func (b *BitbucketAdapter) GetFileContent(ctx context.Context, projectKey, repos
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
// Ensure raw file retrieval
|
||||||
|
r.Header.Set("Accept", "application/octet-stream")
|
||||||
if ref != "" {
|
if ref != "" {
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
q.Set("at", ref)
|
q.Set("at", ref)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/pierre"
|
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/pierre"
|
||||||
@@ -50,11 +51,14 @@ func (g *Adapter) AddComment(ctx context.Context, owner, repo string, prID int,
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, _, err := g.client.CreatePullReview(owner, repo, int64(prID), opts)
|
_, resp, err := g.client.CreatePullReview(owner, repo, int64(prID), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Gitea AddComment failed", "err", err)
|
slog.Error("Gitea AddComment failed", "err", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if resp != nil && resp.StatusCode != http.StatusCreated {
|
||||||
|
return fmt.Errorf("unexpected status %d creating comment", resp.StatusCode)
|
||||||
|
}
|
||||||
slog.Info("Gitea AddComment succeeded", "pr", prID)
|
slog.Info("Gitea AddComment succeeded", "pr", prID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package pierre
|
package pierre
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
|
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
|
||||||
|
|||||||
44
internal/pierre/overlap_test.go
Normal file
44
internal/pierre/overlap_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package pierre
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type overlapChat struct{ callCount int }
|
||||||
|
|
||||||
|
func (m *overlapChat) GenerateStructured(ctx context.Context, msgs []chatter.Message, target interface{}) error {
|
||||||
|
m.callCount++
|
||||||
|
if cSlice, ok := target.(*[]Comment); ok {
|
||||||
|
// Return two comments with same file and line to test deduplication
|
||||||
|
*cSlice = []Comment{{File: "dup.go", Line: 10, Message: "first"}, {File: "dup.go", Line: 10, Message: "second"}}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *overlapChat) GetProviderName() string { return "mock" }
|
||||||
|
|
||||||
|
func TestJudgePR_DeduplicationOverlap(t *testing.T) {
|
||||||
|
chat := &overlapChat{}
|
||||||
|
svc := &Service{
|
||||||
|
maxChunkSize: 1000,
|
||||||
|
guidelines: nil,
|
||||||
|
git: &mockGit{},
|
||||||
|
chat: chat,
|
||||||
|
}
|
||||||
|
diffReader, err := svc.git.GetDiff(context.Background(), "", "", 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get diff: %v", err)
|
||||||
|
}
|
||||||
|
defer diffReader.Close()
|
||||||
|
comments, err := svc.judgePR(context.Background(), diffReader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("judgePR error: %v", err)
|
||||||
|
}
|
||||||
|
if len(comments) != 1 {
|
||||||
|
t.Fatalf("expected 1 deduplicated comment, got %d", len(comments))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
type Service struct {
|
type Service struct {
|
||||||
maxChunkSize int
|
maxChunkSize int
|
||||||
guidelines []string // stored as slice of lines; legacy, see WithGuidelines
|
guidelines []string // stored as slice of lines; legacy, see WithGuidelines
|
||||||
|
skipSanityCheck bool // if true, skip LLM sanity‑check prompts per comment
|
||||||
disableComments bool
|
disableComments bool
|
||||||
git GitAdapter
|
git GitAdapter
|
||||||
chat ChatAdapter
|
chat ChatAdapter
|
||||||
@@ -29,6 +30,7 @@ func New(chat ChatAdapter, git GitAdapter, maxChunkSize int, guidelines []string
|
|||||||
chat: chat,
|
chat: chat,
|
||||||
maxChunkSize: maxChunkSize,
|
maxChunkSize: maxChunkSize,
|
||||||
guidelines: guidelines,
|
guidelines: guidelines,
|
||||||
|
skipSanityCheck: false,
|
||||||
disableComments: disableComments,
|
disableComments: disableComments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,6 +51,11 @@ func (s *Service) WithGuidelines(md string) error {
|
|||||||
|
|
||||||
// parseGuidelinesFromString splits a markdown string into trimmed, non‑empty
|
// parseGuidelinesFromString splits a markdown string into trimmed, non‑empty
|
||||||
// lines and ensures the total number of lines does not exceed 1000.
|
// lines and ensures the total number of lines does not exceed 1000.
|
||||||
|
func (s *Service) SetSanityCheck(enabled bool) {
|
||||||
|
s.skipSanityCheck = !enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGuidelinesFromString splits a markdown string into trimmed, non‑empty
|
||||||
func parseGuidelinesFromString(md string) ([]string, error) {
|
func parseGuidelinesFromString(md string) ([]string, error) {
|
||||||
var result []string
|
var result []string
|
||||||
// Split on newline. Handles both \n and \r\n because TrimSpace removes \r.
|
// Split on newline. Handles both \n and \r\n because TrimSpace removes \r.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package pierre
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
|
|
||||||
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
|
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
|
||||||
)
|
)
|
||||||
@@ -22,17 +22,17 @@ func (s *Service) MakeReview(ctx context.Context, organisation string, repo stri
|
|||||||
return fmt.Errorf("error judging PR: %w", err)
|
return fmt.Errorf("error judging PR: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Sanity‑check step (always enabled) ----------
|
// ---------- Sanity‑check step ----------
|
||||||
headSHA, err := s.git.GetPRHeadSHA(ctx, organisation, repo, prID)
|
headSHA, err := s.git.GetPRHeadSHA(ctx, organisation, repo, prID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("warning: could not fetch PR head SHA (%v); skipping sanity check", err)
|
slog.Warn("could not fetch PR head SHA", "error", err)
|
||||||
} else {
|
} else if !s.skipSanityCheck {
|
||||||
filtered := []Comment{}
|
filtered := []Comment{}
|
||||||
for _, c := range comments {
|
for _, c := range comments {
|
||||||
// Retrieve full file content at the PR head
|
// Retrieve full file content at the PR head
|
||||||
fileContent, fErr := s.git.GetFileContent(ctx, organisation, repo, c.File, headSHA)
|
fileContent, fErr := s.git.GetFileContent(ctx, organisation, repo, c.File, headSHA)
|
||||||
if fErr != nil {
|
if fErr != nil {
|
||||||
log.Printf("failed to fetch file %s: %v – keeping original comment", c.File, fErr)
|
slog.Warn("failed to fetch file", "path", c.File, "error", fErr)
|
||||||
filtered = append(filtered, c)
|
filtered = append(filtered, c)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ func (s *Service) MakeReview(ctx context.Context, organisation string, repo stri
|
|||||||
}
|
}
|
||||||
var res sanityResult
|
var res sanityResult
|
||||||
if err := s.chat.GenerateStructured(ctx, []chatter.Message{{Role: chatter.RoleSystem, Content: systemPrompt}, {Role: chatter.RoleUser, Content: userPrompt}}, &res); err != nil {
|
if err := s.chat.GenerateStructured(ctx, []chatter.Message{{Role: chatter.RoleSystem, Content: systemPrompt}, {Role: chatter.RoleUser, Content: userPrompt}}, &res); err != nil {
|
||||||
log.Printf("sanity check error for %s:%d: %v – keeping comment", c.File, c.Line, err)
|
slog.Error("sanity check error", "file", c.File, "line", c.Line, "error", err)
|
||||||
filtered = append(filtered, c)
|
filtered = append(filtered, c)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ func (s *Service) MakeReview(ctx context.Context, organisation string, repo stri
|
|||||||
c.Message = fmt.Sprintf("%s (Reason: %s)", c.Message, res.Reason)
|
c.Message = fmt.Sprintf("%s (Reason: %s)", c.Message, res.Reason)
|
||||||
filtered = append(filtered, c)
|
filtered = append(filtered, c)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("comment on %s:%d discarded: %s", c.File, c.Line, res.Reason)
|
slog.Info("comment discarded", "file", c.File, "line", c.Line, "reason", res.Reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
comments = filtered
|
comments = filtered
|
||||||
@@ -75,7 +75,7 @@ func (s *Service) MakeReview(ctx context.Context, organisation string, repo stri
|
|||||||
|
|
||||||
if !s.disableComments {
|
if !s.disableComments {
|
||||||
if err := s.git.AddComment(ctx, organisation, repo, prID, c); err != nil {
|
if err := s.git.AddComment(ctx, organisation, repo, prID, c); err != nil {
|
||||||
log.Printf("Failed to add comment: %v", err)
|
slog.Error("failed to add comment", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user