Compare commits
5 Commits
issue-70
...
6f86e66943
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f86e66943 | |||
| 019b815280 | |||
| c27dfd0f08 | |||
| 1b6c37605f | |||
| 036e96d9b7 |
+16
-7
@@ -65,7 +65,7 @@ func main() {
|
||||
conventionsFile := flag.String("conventions-file", envOrDefault("CONVENTIONS_FILE", ""), "Conventions file path in repo (e.g. CLAUDE.md)")
|
||||
systemPromptFile := flag.String("system-prompt-file", envOrDefault("SYSTEM_PROMPT_FILE", ""), "Local file with additional system prompt instructions")
|
||||
patternsRepo := flag.String("patterns-repo", envOrDefault("PATTERNS_REPO", ""), "Repo with language patterns (e.g. rodin/elixir-patterns)")
|
||||
patternsFiles := flag.String("patterns-files", envOrDefault("PATTERNS_FILES", "README.md"), "Comma-separated file paths to fetch from patterns repo")
|
||||
patternsFiles := flag.String("patterns-files", envOrDefault("PATTERNS_FILES", ""), "Comma-separated file paths to fetch from patterns repo (empty = all files)")
|
||||
dryRun := flag.Bool("dry-run", false, "Print review to stdout instead of posting")
|
||||
llmTemp := flag.Float64("llm-temperature", envOrDefaultFloat("LLM_TEMPERATURE", 0), "LLM temperature (0 = server default)")
|
||||
llmTimeout := flag.Int("llm-timeout", envOrDefaultInt("LLM_TIMEOUT", 300), "LLM request timeout in seconds (default 300)")
|
||||
@@ -523,11 +523,25 @@ func fetchFileContext(ctx context.Context, client *gitea.Client, owner, repo, re
|
||||
// patternsRepo is comma-separated list of owner/name repos.
|
||||
// patternsFiles is comma-separated list of file paths or directories.
|
||||
// If a path ends with / or is a directory, all files within it are fetched recursively.
|
||||
// If patternsFiles is empty, all files from the repo root are fetched.
|
||||
func fetchPatterns(ctx context.Context, client *gitea.Client, patternsRepo, patternsFiles string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
repos := strings.Split(patternsRepo, ",")
|
||||
paths := strings.Split(patternsFiles, ",")
|
||||
|
||||
// Build the list of paths to fetch
|
||||
var paths []string
|
||||
if patternsFiles == "" {
|
||||
// Empty patternsFiles means "fetch all files from repo root"
|
||||
paths = []string{""}
|
||||
} else {
|
||||
for _, p := range strings.Split(patternsFiles, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, repoRef := range repos {
|
||||
if ctx.Err() != nil {
|
||||
@@ -548,11 +562,6 @@ func fetchPatterns(ctx context.Context, client *gitea.Client, patternsRepo, patt
|
||||
var repoSkippedFiles []string
|
||||
|
||||
for _, path := range paths {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
files, err := client.GetAllFilesInPath(ctx, owner, repo, path)
|
||||
if err != nil {
|
||||
slog.Warn("could not fetch patterns", "path", path, "repo", repoRef, "error", err)
|
||||
|
||||
@@ -504,6 +504,52 @@ func TestIsPatternFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildPatternPaths verifies the path-building logic for fetchPatterns.
|
||||
// Empty patternsFiles means "fetch all from root" (represented as [""]).
|
||||
func TestBuildPatternPaths(t *testing.T) {
|
||||
buildPaths := func(patternsFiles string) []string {
|
||||
if patternsFiles == "" {
|
||||
return []string{""}
|
||||
}
|
||||
var paths []string
|
||||
for _, p := range strings.Split(patternsFiles, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{"empty fetches root", "", []string{""}},
|
||||
{"single file", "README.md", []string{"README.md"}},
|
||||
{"multiple files", "README.md,PATTERNS.md", []string{"README.md", "PATTERNS.md"}},
|
||||
{"trims whitespace", " foo.md , bar.md ", []string{"foo.md", "bar.md"}},
|
||||
{"skips empty between commas", "foo.md,,bar.md", []string{"foo.md", "bar.md"}},
|
||||
{"directory path", "patterns/", []string{"patterns/"}},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := buildPaths(tc.input)
|
||||
if len(got) != len(tc.want) {
|
||||
t.Errorf("buildPaths(%q) = %v, want %v", tc.input, got, tc.want)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tc.want[i] {
|
||||
t.Errorf("buildPaths(%q)[%d] = %q, want %q", tc.input, i, got[i], tc.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateCIStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
+12
-1
@@ -434,6 +434,8 @@ type ContentEntry struct {
|
||||
|
||||
// ListContents lists files and directories at a given path in a repo.
|
||||
// Pass an empty path to list the repository root.
|
||||
// If the path points to a file (not a directory), Gitea returns a single
|
||||
// object instead of an array; this method normalizes both cases to a slice.
|
||||
func (c *Client) ListContents(ctx context.Context, owner, repo, path string) ([]ContentEntry, error) {
|
||||
// Normalize "." to empty string — Gitea API rejects "." with 500
|
||||
if path == "." {
|
||||
@@ -451,7 +453,16 @@ func (c *Client) ListContents(ctx context.Context, owner, repo, path string) ([]
|
||||
}
|
||||
var entries []ContentEntry
|
||||
if err := json.Unmarshal(body, &entries); err != nil {
|
||||
return nil, fmt.Errorf("parse contents JSON: %w", err)
|
||||
// Gitea returns a single object (not an array) when path is a file
|
||||
var single ContentEntry
|
||||
if err2 := json.Unmarshal(body, &single); err2 != nil {
|
||||
return nil, fmt.Errorf("parse contents JSON: %w", err)
|
||||
}
|
||||
// Guard against empty/malformed responses
|
||||
if single.Name == "" && single.Path == "" {
|
||||
return nil, fmt.Errorf("parse contents JSON: empty response for path %q", path)
|
||||
}
|
||||
entries = []ContentEntry{single}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
+31
-2
@@ -304,11 +304,40 @@ func TestListContents_DotPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListContents_FilePath(t *testing.T) {
|
||||
// Gitea returns a single object (not an array) when path is a file
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/repos/owner/repo/contents/README.md" {
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Single object, not an array
|
||||
fmt.Fprintf(w, `{"name":"README.md","path":"README.md","type":"file"}`)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
entries, err := client.ListContents(context.Background(), "owner", "repo", "README.md")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("expected 1 entry, got %d", len(entries))
|
||||
}
|
||||
if entries[0].Name != "README.md" {
|
||||
t.Errorf("expected README.md, got %s", entries[0].Name)
|
||||
}
|
||||
if entries[0].Type != "file" {
|
||||
t.Errorf("expected type file, got %s", entries[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllFilesInPath_File(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/v1/repos/owner/repo/contents/README.md" {
|
||||
// Gitea returns 404 for contents API on files (it's not a dir)
|
||||
http.NotFound(w, r)
|
||||
// Gitea returns a single object (not array) when path is a file
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"name":"README.md","path":"README.md","type":"file"}`)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api/v1/repos/owner/repo/raw/README.md" {
|
||||
|
||||
Reference in New Issue
Block a user